From 46199897223a43248b0bed63a9c91e6e178dbc7a Mon Sep 17 00:00:00 2001 From: Glauber Costa Date: Sun, 26 Jul 2026 09:50:39 -0700 Subject: [PATCH] bindings/c: implement sqlite3_set_authorizer, sqlite3_db_config, sqlite3_extended_result_codes All three were panicking stubs or missing; a Rust panic across the extern "C" boundary aborts the host process, and PHP's ext/sqlite3 calls sqlite3_set_authorizer unconditionally when opening a connection. - sqlite3_set_authorizer: refused with SQLITE_ERROR. turso_core has no prepare-time authorization hook, so a stored callback would never be invoked and callers (e.g. PHP, which routes its open_basedir checks through the authorizer) would silently lose the enforcement they registered for. PHP discards this return value at open, so refusing does not break it. To be implemented for real once core grows an authorization hook. - sqlite3_db_config: variadic entry point in a new C shim (varargs.c) since C variadics are not expressible in stable Rust and variadic arguments use a different ABI from named arguments on Apple arm64. Decodes the (int, int*) shape shared by boolean ops and forwards to a Rust backend; ops with other shapes are rejected without reading the va_list. SQLITE_DBCONFIG_DEFENSIVE is refused with SQLITE_ERROR rather than accepted-and-ignored: the engine is unconditionally defensive today (sqlite_schema DML is rejected with no writable_schema unlock, PRAGMA schema_version=N is a deliberate no-op, PRAGMA journal_mode=OFF is unsupported, and there are no shadow tables), so there is nothing to toggle and a caller that checks the return is not misled. Accepting DEFENSIVE becomes possible once each of those features gains a per-connection defensive gate. PHP ignores this return value. Other ops return SQLITE_ERROR. - The exported sqlite3_db_config symbol is a naked-function trampoline that tail-jumps to the C implementation: rustc exports only Rust items from cdylibs (version script on ELF, exported-symbols list on Apple), so the public symbol must be a Rust item. The tail jump hands the untouched variadic call frame to the C function, and the sym reference pins varargs.c's object into the link. Deletable once rust-lang/rust#155697 lands. - sqlite3_extended_result_codes: toggles the connection's err_mask. The default mask now matches SQLite (extended codes disabled), and sqlite3_extended_errcode no longer masks its result: it reports the extended code regardless of the setting. - Constants (authorizer return/action codes, SQLITE_DBCONFIG_*) are defined in lib.rs and mirrored in the header, all verified numerically against SQLite's sqlite.h.in. Tests in sqlite3_tests.c cover the registration contract for the authorizer (accepted by SQLite, refused by Turso, execution unaffected either way), the DEFENSIVE set/query/reset cycle on SQLite and refusal with untouched out-pointer on Turso including the exact NULL-out-pointer call PHP makes at open, unknown-op rejection, and errcode masking around a UNIQUE violation with extended codes off, on, and off again. The same binary passes linked against both turso_sqlite3 and libsqlite3. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 1 + bindings/c/Cargo.toml | 1 + bindings/c/build.rs | 11 ++ bindings/c/include/sqlite3.h | 69 +++++++++++- bindings/c/src/lib.rs | 179 +++++++++++++++++++++++++++++-- bindings/c/src/varargs.c | 49 +++++++++ bindings/c/tests/sqlite3_tests.c | 178 +++++++++++++++++++++++++++++- 7 files changed, 478 insertions(+), 10 deletions(-) create mode 100644 bindings/c/src/varargs.c diff --git a/Cargo.lock b/Cargo.lock index f02ee0bcda0..26013747499 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7614,6 +7614,7 @@ dependencies = [ name = "turso_sqlite3" version = "0.8.0-pre.2" dependencies = [ + "cc", "env_logger", "libc", "tempfile", diff --git a/bindings/c/Cargo.toml b/bindings/c/Cargo.toml index bddfe737f00..378f04a6658 100644 --- a/bindings/c/Cargo.toml +++ b/bindings/c/Cargo.toml @@ -41,3 +41,4 @@ name = "compat" path = "tests/compat/mod.rs" [build-dependencies] +cc = "1" diff --git a/bindings/c/build.rs b/bindings/c/build.rs index c00f4d67979..9a7cd31ffa9 100644 --- a/bindings/c/build.rs +++ b/bindings/c/build.rs @@ -1,3 +1,14 @@ fn main() { println!("cargo:rustc-link-search=native=target/debug"); + println!("cargo:rerun-if-changed=src/varargs.c"); + // varargs.c exists because some sqlite3 entry points (db_config, and + // eventually mprintf/snprintf) are C-variadic, which stable Rust cannot + // define. Faking them with fixed args breaks on Apple arm64, where + // variadic arguments are passed on the stack but named arguments in + // registers — the callee would read garbage. The exported sqlite3_* + // symbols themselves are naked-function trampolines in lib.rs; see the + // comment there. + cc::Build::new() + .file("src/varargs.c") + .compile("turso_sqlite3_varargs"); } diff --git a/bindings/c/include/sqlite3.h b/bindings/c/include/sqlite3.h index 05a5db88ea1..bd19c9999c0 100644 --- a/bindings/c/include/sqlite3.h +++ b/bindings/c/include/sqlite3.h @@ -19,6 +19,8 @@ #define SQLITE_CANTOPEN 14 +#define SQLITE_CONSTRAINT 19 + #define SQLITE_MISUSE 21 #define SQLITE_ROW 100 @@ -87,7 +89,49 @@ void sqlite3_progress_handler(sqlite3 *_db, int _n, int (*_callback)(void *), vo int sqlite3_busy_timeout(sqlite3 *_db, int _ms); -int sqlite3_set_authorizer(sqlite3 *_db, int (*_callback)(void), void *_context); +int sqlite3_set_authorizer(sqlite3 *db, + int (*xAuth)(void*, int, const char*, const char*, const char*, const char*), + void *pUserData); + +/* Authorizer callback return codes */ +#define SQLITE_DENY 1 +#define SQLITE_IGNORE 2 + +/* Authorizer action codes (third parameter meanings per SQLite docs) */ +#define SQLITE_CREATE_INDEX 1 +#define SQLITE_CREATE_TABLE 2 +#define SQLITE_CREATE_TEMP_INDEX 3 +#define SQLITE_CREATE_TEMP_TABLE 4 +#define SQLITE_CREATE_TEMP_TRIGGER 5 +#define SQLITE_CREATE_TEMP_VIEW 6 +#define SQLITE_CREATE_TRIGGER 7 +#define SQLITE_CREATE_VIEW 8 +#define SQLITE_DELETE 9 +#define SQLITE_DROP_INDEX 10 +#define SQLITE_DROP_TABLE 11 +#define SQLITE_DROP_TEMP_INDEX 12 +#define SQLITE_DROP_TEMP_TABLE 13 +#define SQLITE_DROP_TEMP_TRIGGER 14 +#define SQLITE_DROP_TEMP_VIEW 15 +#define SQLITE_DROP_TRIGGER 16 +#define SQLITE_DROP_VIEW 17 +#define SQLITE_INSERT 18 +#define SQLITE_PRAGMA 19 +#define SQLITE_READ 20 +#define SQLITE_SELECT 21 +#define SQLITE_TRANSACTION 22 +#define SQLITE_UPDATE 23 +#define SQLITE_ATTACH 24 +#define SQLITE_DETACH 25 +#define SQLITE_ALTER_TABLE 26 +#define SQLITE_REINDEX 27 +#define SQLITE_ANALYZE 28 +#define SQLITE_CREATE_VTABLE 29 +#define SQLITE_DROP_VTABLE 30 +#define SQLITE_FUNCTION 31 +#define SQLITE_SAVEPOINT 32 +#define SQLITE_COPY 0 +#define SQLITE_RECURSIVE 33 void *sqlite3_context_db_handle(void *_context); @@ -145,7 +189,28 @@ int64_t sqlite3_last_insert_rowid(sqlite3 *_db); void sqlite3_interrupt(sqlite3 *_db); -int sqlite3_db_config(sqlite3 *_db, int _op); +int sqlite3_db_config(sqlite3 *db, int op, ...); + +#define SQLITE_DBCONFIG_MAINDBNAME 1000 +#define SQLITE_DBCONFIG_LOOKASIDE 1001 +#define SQLITE_DBCONFIG_ENABLE_FKEY 1002 +#define SQLITE_DBCONFIG_ENABLE_TRIGGER 1003 +#define SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER 1004 +#define SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION 1005 +#define SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE 1006 +#define SQLITE_DBCONFIG_ENABLE_QPSG 1007 +#define SQLITE_DBCONFIG_TRIGGER_EQP 1008 +#define SQLITE_DBCONFIG_RESET_DATABASE 1009 +#define SQLITE_DBCONFIG_DEFENSIVE 1010 +#define SQLITE_DBCONFIG_WRITABLE_SCHEMA 1011 +#define SQLITE_DBCONFIG_LEGACY_ALTER_TABLE 1012 +#define SQLITE_DBCONFIG_DQS_DML 1013 +#define SQLITE_DBCONFIG_DQS_DDL 1014 +#define SQLITE_DBCONFIG_ENABLE_VIEW 1015 +#define SQLITE_DBCONFIG_LEGACY_FILE_FORMAT 1016 +#define SQLITE_DBCONFIG_TRUSTED_SCHEMA 1017 + +int sqlite3_extended_result_codes(sqlite3 *db, int onoff); sqlite3 *sqlite3_db_handle(sqlite3_stmt *_stmt); diff --git a/bindings/c/src/lib.rs b/bindings/c/src/lib.rs index a0ed9806d32..7b52d5a45ce 100644 --- a/bindings/c/src/lib.rs +++ b/bindings/c/src/lib.rs @@ -107,6 +107,66 @@ pub const LIBSQL_STMTSTATUS_BASE: ffi::c_int = 1024; pub const LIBSQL_STMTSTATUS_ROWS_READ: ffi::c_int = LIBSQL_STMTSTATUS_BASE + 1; pub const LIBSQL_STMTSTATUS_ROWS_WRITTEN: ffi::c_int = LIBSQL_STMTSTATUS_BASE + 2; +/* authorizer callback return codes */ +pub const SQLITE_DENY: ffi::c_int = 1; +pub const SQLITE_IGNORE: ffi::c_int = 2; + +/* authorizer action codes */ +pub const SQLITE_COPY: ffi::c_int = 0; +pub const SQLITE_CREATE_INDEX: ffi::c_int = 1; +pub const SQLITE_CREATE_TABLE: ffi::c_int = 2; +pub const SQLITE_CREATE_TEMP_INDEX: ffi::c_int = 3; +pub const SQLITE_CREATE_TEMP_TABLE: ffi::c_int = 4; +pub const SQLITE_CREATE_TEMP_TRIGGER: ffi::c_int = 5; +pub const SQLITE_CREATE_TEMP_VIEW: ffi::c_int = 6; +pub const SQLITE_CREATE_TRIGGER: ffi::c_int = 7; +pub const SQLITE_CREATE_VIEW: ffi::c_int = 8; +pub const SQLITE_DELETE: ffi::c_int = 9; +pub const SQLITE_DROP_INDEX: ffi::c_int = 10; +pub const SQLITE_DROP_TABLE: ffi::c_int = 11; +pub const SQLITE_DROP_TEMP_INDEX: ffi::c_int = 12; +pub const SQLITE_DROP_TEMP_TABLE: ffi::c_int = 13; +pub const SQLITE_DROP_TEMP_TRIGGER: ffi::c_int = 14; +pub const SQLITE_DROP_TEMP_VIEW: ffi::c_int = 15; +pub const SQLITE_DROP_TRIGGER: ffi::c_int = 16; +pub const SQLITE_DROP_VIEW: ffi::c_int = 17; +pub const SQLITE_INSERT: ffi::c_int = 18; +pub const SQLITE_PRAGMA: ffi::c_int = 19; +pub const SQLITE_READ: ffi::c_int = 20; +pub const SQLITE_SELECT: ffi::c_int = 21; +pub const SQLITE_TRANSACTION: ffi::c_int = 22; +pub const SQLITE_UPDATE: ffi::c_int = 23; +pub const SQLITE_ATTACH: ffi::c_int = 24; +pub const SQLITE_DETACH: ffi::c_int = 25; +pub const SQLITE_ALTER_TABLE: ffi::c_int = 26; +pub const SQLITE_REINDEX: ffi::c_int = 27; +pub const SQLITE_ANALYZE: ffi::c_int = 28; +pub const SQLITE_CREATE_VTABLE: ffi::c_int = 29; +pub const SQLITE_DROP_VTABLE: ffi::c_int = 30; +pub const SQLITE_FUNCTION: ffi::c_int = 31; +pub const SQLITE_SAVEPOINT: ffi::c_int = 32; +pub const SQLITE_RECURSIVE: ffi::c_int = 33; + +/* sqlite3_db_config operations */ +pub const SQLITE_DBCONFIG_MAINDBNAME: ffi::c_int = 1000; +pub const SQLITE_DBCONFIG_LOOKASIDE: ffi::c_int = 1001; +pub const SQLITE_DBCONFIG_ENABLE_FKEY: ffi::c_int = 1002; +pub const SQLITE_DBCONFIG_ENABLE_TRIGGER: ffi::c_int = 1003; +pub const SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER: ffi::c_int = 1004; +pub const SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION: ffi::c_int = 1005; +pub const SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE: ffi::c_int = 1006; +pub const SQLITE_DBCONFIG_ENABLE_QPSG: ffi::c_int = 1007; +pub const SQLITE_DBCONFIG_TRIGGER_EQP: ffi::c_int = 1008; +pub const SQLITE_DBCONFIG_RESET_DATABASE: ffi::c_int = 1009; +pub const SQLITE_DBCONFIG_DEFENSIVE: ffi::c_int = 1010; +pub const SQLITE_DBCONFIG_WRITABLE_SCHEMA: ffi::c_int = 1011; +pub const SQLITE_DBCONFIG_LEGACY_ALTER_TABLE: ffi::c_int = 1012; +pub const SQLITE_DBCONFIG_DQS_DML: ffi::c_int = 1013; +pub const SQLITE_DBCONFIG_DQS_DDL: ffi::c_int = 1014; +pub const SQLITE_DBCONFIG_ENABLE_VIEW: ffi::c_int = 1015; +pub const SQLITE_DBCONFIG_LEGACY_FILE_FORMAT: ffi::c_int = 1016; +pub const SQLITE_DBCONFIG_TRUSTED_SCHEMA: ffi::c_int = 1017; + pub struct sqlite3 { pub(crate) inner: Arc>, } @@ -136,7 +196,9 @@ impl sqlite3 { _db: db, conn, err_code: SQLITE_OK, - err_mask: 0xFFFFFFFFu32 as i32, + // Extended result codes are disabled by default, as in SQLite; + // sqlite3_extended_result_codes() widens the mask. + err_mask: 0xff, malloc_failed: false, e_open_state: SQLITE_STATE_OPEN, p_err: std::ptr::null_mut(), @@ -791,13 +853,33 @@ pub unsafe extern "C" fn sqlite3_busy_timeout(db: *mut sqlite3, ms: ffi::c_int) SQLITE_OK } +/// Callback type for sqlite3_set_authorizer: (userData, actionCode, arg1, +/// arg2, database, trigger-or-view) -> SQLITE_OK / SQLITE_DENY / SQLITE_IGNORE. +pub type sqlite3_authorizer_callback = unsafe extern "C" fn( + *mut ffi::c_void, + ffi::c_int, + *const ffi::c_char, + *const ffi::c_char, + *const ffi::c_char, + *const ffi::c_char, +) -> ffi::c_int; + +/// Refused with SQLITE_ERROR: turso_core has no prepare-time authorization +/// hook, so a stored callback would never be invoked and callers (e.g. PHP, +/// which routes its open_basedir checks through the authorizer) would +/// silently lose the enforcement they registered for. PHP ignores this +/// return value when it installs its authorizer at open, so refusing does +/// not break it. Implement for real once core grows an authorization hook. #[no_mangle] pub unsafe extern "C" fn sqlite3_set_authorizer( - _db: *mut sqlite3, - _callback: Option ffi::c_int>, + db: *mut sqlite3, + _callback: Option, _context: *mut ffi::c_void, ) -> ffi::c_int { - stub!(); + if db.is_null() { + return SQLITE_MISUSE; + } + SQLITE_ERROR } #[no_mangle] @@ -1403,9 +1485,66 @@ pub unsafe extern "C" fn sqlite3_interrupt(db: *mut sqlite3) { inner.conn.interrupt(); } +extern "C" { + /// Variadic implementation in varargs.c; decodes the va_list and calls + /// turso_db_config_int below. Declared argument-less because it is only + /// referenced as a `sym` jump target, never called from Rust. + fn turso_sqlite3_db_config_va(); +} + +/// Exported trampoline for the variadic sqlite3_db_config. rustc exports +/// only Rust items from cdylibs (version script on ELF, exported-symbols +/// list on Apple), so the public symbol must be defined here; the tail jump +/// hands the untouched variadic call frame to the C implementation, whose +/// own prologue then performs va_start as for a direct call. This also +/// pins varargs.c's object into the link. Once rustc can export +/// native-library symbols from cdylibs (rust-lang/rust#155697), this +/// trampoline can be deleted and the C function renamed sqlite3_db_config. +#[unsafe(naked)] #[no_mangle] -pub unsafe extern "C" fn sqlite3_db_config(_db: *mut sqlite3, _op: ffi::c_int) -> ffi::c_int { - stub!(); +pub unsafe extern "C" fn sqlite3_db_config(_db: *mut ffi::c_void, _op: ffi::c_int) -> ffi::c_int { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + core::arch::naked_asm!("jmp {}", sym turso_sqlite3_db_config_va); + #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] + core::arch::naked_asm!("b {}", sym turso_sqlite3_db_config_va); +} + +/// Non-variadic backend for sqlite3_db_config. The variadic C entry point in +/// varargs.c decodes the (int value, int *pOut) argument shape shared by all +/// boolean DBCONFIG ops and forwards here. For boolean ops, a negative value +/// queries the current setting without changing it, and the resulting state +/// is written through `p_out` when non-NULL, as in SQLite. +#[no_mangle] +#[deny(unsafe_op_in_unsafe_fn)] +pub unsafe extern "C" fn turso_db_config_int( + db: *mut sqlite3, + op: ffi::c_int, + value: ffi::c_int, + p_out: *mut ffi::c_int, +) -> ffi::c_int { + if db.is_null() { + return SQLITE_MISUSE; + } + let _ = (value, p_out); + match op { + // Refused rather than accepted-and-ignored, so a caller that checks + // the return cannot be misled into believing it toggled anything. + // The protections DEFENSIVE stands for are unconditionally on in + // turso_core today: + // - sqlite_schema DML is rejected with no writable_schema unlock + // (core/schema.rs, allow_user_dml) + // - PRAGMA schema_version=N is a deliberate no-op + // (core/translate/pragma.rs, SchemaVersion write arm) + // - PRAGMA journal_mode=OFF is unsupported and silently kept as-is + // (core/storage/journal_mode.rs, JournalMode::supported) + // - there are no SQLite-style shadow tables + // Callers that need file-corruption protection therefore already + // have it; PHP's ext/sqlite3 ignores this return value. Switching to + // accept + enforce is safe only if every feature above gains a + // per-connection defensive gate in core first. + SQLITE_DBCONFIG_DEFENSIVE => SQLITE_ERROR, + _ => SQLITE_ERROR, + } } #[no_mangle] @@ -1468,6 +1607,30 @@ pub unsafe extern "C" fn sqlite3_errcode(db: *mut sqlite3) -> ffi::c_int { db.err_code & db.err_mask } +/// Enables or disables extended result codes for the connection. When +/// disabled (the default), sqlite3_errcode and statement results report only +/// the primary result code; sqlite3_extended_errcode always reports the +/// extended code either way. +#[no_mangle] +#[deny(unsafe_op_in_unsafe_fn)] +pub unsafe extern "C" fn sqlite3_extended_result_codes( + db: *mut sqlite3, + onoff: ffi::c_int, +) -> ffi::c_int { + if db.is_null() { + return SQLITE_MISUSE; + } + // SAFETY: db checked non-null just above; caller guarantees it points to a + // live sqlite3 for the duration of the call. + let db: &sqlite3 = unsafe { &*db }; + let mut inner = match db.inner.lock() { + Ok(guard) => guard, + Err(_) => return SQLITE_MISUSE, + }; + inner.err_mask = if onoff != 0 { -1 } else { 0xff }; + SQLITE_OK +} + #[no_mangle] pub unsafe extern "C" fn sqlite3_errstr(_err: ffi::c_int) -> *const ffi::c_char { sqlite3_errstr_impl(_err) @@ -2758,7 +2921,9 @@ pub unsafe extern "C" fn sqlite3_extended_errcode(db: *mut sqlite3) -> ffi::c_in if db.malloc_failed { return SQLITE_NOMEM; } - db.err_code & db.err_mask + // The extended code is reported regardless of the + // sqlite3_extended_result_codes() setting; only sqlite3_errcode masks. + db.err_code } #[no_mangle] diff --git a/bindings/c/src/varargs.c b/bindings/c/src/varargs.c new file mode 100644 index 00000000000..fb44f5e87da --- /dev/null +++ b/bindings/c/src/varargs.c @@ -0,0 +1,49 @@ +/* +** Variadic C entry points that cannot be written in stable Rust (C variadics +** are a nightly-only Rust feature, and on some ABIs — e.g. Apple arm64 — +** variadic arguments are passed differently from named arguments, so the +** entry point must genuinely be variadic). +** +** Each function here decodes its va_list and forwards to a non-variadic +** `turso_*` function exported from the Rust side of the crate. The public +** sqlite3_* symbol itself is a naked-function trampoline in lib.rs that +** tail-jumps here: rustc only exports Rust items from cdylibs, so the +** exported symbol must be a Rust item, and a tail jump preserves the +** variadic call frame intact. +*/ +#include +#include + +typedef struct sqlite3 sqlite3; + +#define SQLITE_ERROR 1 + +extern int turso_db_config_int(sqlite3 *db, int op, int value, int *p_out); + +/* +** sqlite3_db_config() argument shapes, by op: +** SQLITE_DBCONFIG_MAINDBNAME (1000) takes (const char*) +** SQLITE_DBCONFIG_LOOKASIDE (1001) takes (void*, int, int) +** the boolean ops (1002..1017) take (int, int*) +** The va_list is decoded only for ops known to use the (int, int*) shape; +** any other op — including ops newer than this header — is rejected without +** reading the va_list, since decoding an unknown shape is undefined +** behavior when the caller passed something else. +*/ +#define DBCONFIG_BOOL_FIRST 1002 /* SQLITE_DBCONFIG_ENABLE_FKEY */ +#define DBCONFIG_BOOL_LAST 1017 /* SQLITE_DBCONFIG_TRUSTED_SCHEMA */ + +int turso_sqlite3_db_config_va(sqlite3 *db, int op, ...) { + va_list ap; + int value; + int *p_out; + + if (op < DBCONFIG_BOOL_FIRST || op > DBCONFIG_BOOL_LAST) { + return SQLITE_ERROR; + } + va_start(ap, op); + value = va_arg(ap, int); + p_out = va_arg(ap, int *); + va_end(ap); + return turso_db_config_int(db, op, value, p_out); +} diff --git a/bindings/c/tests/sqlite3_tests.c b/bindings/c/tests/sqlite3_tests.c index e22a8005f85..4b1bb2b4be6 100644 --- a/bindings/c/tests/sqlite3_tests.c +++ b/bindings/c/tests/sqlite3_tests.c @@ -21,6 +21,9 @@ void test_sqlite3_column_decltype(); void test_sqlite3_next_stmt(); void test_sqlite3_table_column_metadata(); void test_sqlite3_insert_returning(); +void test_sqlite3_set_authorizer(); +void test_sqlite3_db_config(); +void test_sqlite3_extended_result_codes(); int allocated = 0; @@ -41,6 +44,9 @@ int main(void) test_sqlite3_next_stmt(); test_sqlite3_table_column_metadata(); test_sqlite3_insert_returning(); + test_sqlite3_set_authorizer(); + test_sqlite3_db_config(); + test_sqlite3_extended_result_codes(); return 0; } @@ -791,4 +797,174 @@ void test_sqlite3_insert_returning() sqlite3_close(db); printf("test_sqlite3_insert_retuning test passed\n"); -} \ No newline at end of file +} +/* +** Authorizer registration contract. SQLite accepts registrations: one +** authorizer per connection, each call replaces the previous one, NULL +** clears it. Turso refuses with SQLITE_ERROR (no prepare-time +** authorization hook exists, and a stored-but-never-invoked callback would +** silently drop the enforcement callers registered for). Either way, +** statement execution must be unaffected. Enforcement cases (DENY/IGNORE) +** arrive together with the core authorization hook. +*/ +static int authorizer_allow(void *user_data, int action, + const char *arg1, const char *arg2, + const char *database, const char *trigger) +{ + (void)user_data; (void)action; (void)arg1; (void)arg2; + (void)database; (void)trigger; + return SQLITE_OK; +} + +static int authorizer_allow2(void *user_data, int action, + const char *arg1, const char *arg2, + const char *database, const char *trigger) +{ + (void)user_data; (void)action; (void)arg1; (void)arg2; + (void)database; (void)trigger; + return SQLITE_OK; +} + +void test_sqlite3_set_authorizer() +{ + sqlite3 *db; + char *err_msg = NULL; + int rc; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + + /* Registration is either accepted (SQLite) or refused (Turso); this is + ** the call PHP makes unconditionally at open, discarding the return. */ + rc = sqlite3_set_authorizer(db, authorizer_allow, NULL); + assert(rc == SQLITE_OK || rc == SQLITE_ERROR); + + /* A permissive (or refused) authorizer must not disturb execution. */ + rc = sqlite3_exec(db, "CREATE TABLE auth_t(a, b)", NULL, NULL, &err_msg); + assert(rc == SQLITE_OK); + if (err_msg) { sqlite3_free(err_msg); err_msg = NULL; } + + /* Each registration replaces the previous authorizer. */ + rc = sqlite3_set_authorizer(db, authorizer_allow2, (void *)&db); + assert(rc == SQLITE_OK || rc == SQLITE_ERROR); + + rc = sqlite3_exec(db, "INSERT INTO auth_t VALUES (1, 2)", NULL, NULL, &err_msg); + assert(rc == SQLITE_OK); + if (err_msg) { sqlite3_free(err_msg); err_msg = NULL; } + + /* A NULL callback clears the authorizer. */ + rc = sqlite3_set_authorizer(db, NULL, NULL); + assert(rc == SQLITE_OK || rc == SQLITE_ERROR); + + rc = sqlite3_exec(db, "SELECT a FROM auth_t", NULL, NULL, &err_msg); + assert(rc == SQLITE_OK); + if (err_msg) { sqlite3_free(err_msg); err_msg = NULL; } + + sqlite3_close(db); + printf("test_sqlite3_set_authorizer test passed\n"); +} + +void test_sqlite3_db_config() +{ + sqlite3 *db; + int value; + int rc; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + + /* SQLite accepts DEFENSIVE and reports state through the out-pointer. + ** Turso refuses it (SQLITE_ERROR, out-pointer untouched): its engine is + ** unconditionally defensive, so there is nothing to toggle — see + ** turso_db_config_int. Either way the out-pointer must never hold + ** garbage. */ + value = -99; + rc = sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 1, &value); + assert(rc == SQLITE_OK || rc == SQLITE_ERROR); + if (rc == SQLITE_OK) { + assert(value == 1); + + /* A negative value queries the current state without changing it. */ + value = -99; + rc = sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, -1, &value); + assert(rc == SQLITE_OK); + assert(value == 1); + + /* Turning it back off. */ + value = -99; + rc = sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 0, &value); + assert(rc == SQLITE_OK); + assert(value == 0); + } else { + assert(value == -99); + } + + /* A NULL out-pointer must be tolerated whatever the answer; this is the + ** exact call PHP's ext/sqlite3 makes at open when sqlite3.defensive=1, + ** and PHP ignores the return value. */ + rc = sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 1, (int *)0); + assert(rc == SQLITE_OK || rc == SQLITE_ERROR); + + /* Unknown ops are rejected. */ + rc = sqlite3_db_config(db, 9999, 0, (int *)0); + assert(rc == SQLITE_ERROR); + + sqlite3_close(db); + printf("test_sqlite3_db_config test passed\n"); +} + +void test_sqlite3_extended_result_codes() +{ + sqlite3 *db; + sqlite3_stmt *stmt; + char *err_msg = NULL; + int rc; + + rc = sqlite3_open(":memory:", &db); + assert(rc == SQLITE_OK); + + rc = sqlite3_exec(db, "CREATE TABLE erc_t(a INTEGER PRIMARY KEY, b UNIQUE)", + NULL, NULL, &err_msg); + assert(rc == SQLITE_OK); + if (err_msg) { sqlite3_free(err_msg); err_msg = NULL; } + rc = sqlite3_exec(db, "INSERT INTO erc_t VALUES (1, 1)", NULL, NULL, &err_msg); + assert(rc == SQLITE_OK); + if (err_msg) { sqlite3_free(err_msg); err_msg = NULL; } + + /* Extended result codes are disabled by default: sqlite3_errcode + ** reports only the primary code after a constraint violation. */ + rc = sqlite3_prepare_v2(db, "INSERT INTO erc_t VALUES (2, 1)", -1, &stmt, NULL); + assert(rc == SQLITE_OK); + rc = sqlite3_step(stmt); + assert((rc & 0xff) == SQLITE_CONSTRAINT); + sqlite3_finalize(stmt); + assert(sqlite3_errcode(db) == SQLITE_CONSTRAINT); + /* sqlite3_extended_errcode reports the extended code regardless of the + ** setting; its primary part is the constraint code either way. */ + assert((sqlite3_extended_errcode(db) & 0xff) == SQLITE_CONSTRAINT); + + /* Enabling widens what sqlite3_errcode reports; the primary part of + ** whatever it returns is still the constraint code. */ + rc = sqlite3_extended_result_codes(db, 1); + assert(rc == SQLITE_OK); + rc = sqlite3_prepare_v2(db, "INSERT INTO erc_t VALUES (3, 1)", -1, &stmt, NULL); + assert(rc == SQLITE_OK); + rc = sqlite3_step(stmt); + assert((rc & 0xff) == SQLITE_CONSTRAINT); + sqlite3_finalize(stmt); + assert((sqlite3_errcode(db) & 0xff) == SQLITE_CONSTRAINT); + assert((sqlite3_extended_errcode(db) & 0xff) == SQLITE_CONSTRAINT); + + /* Disabling narrows sqlite3_errcode back to the primary code. */ + rc = sqlite3_extended_result_codes(db, 0); + assert(rc == SQLITE_OK); + rc = sqlite3_prepare_v2(db, "INSERT INTO erc_t VALUES (4, 1)", -1, &stmt, NULL); + assert(rc == SQLITE_OK); + rc = sqlite3_step(stmt); + assert((rc & 0xff) == SQLITE_CONSTRAINT); + sqlite3_finalize(stmt); + assert(sqlite3_errcode(db) == SQLITE_CONSTRAINT); + + sqlite3_close(db); + printf("test_sqlite3_extended_result_codes test passed\n"); +}