/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ext/node_sqlite/database.rs
3 266 строк
95 KB
Bartek Iwańczuk
fix(ext/node): node:sqlite backup() and deserialize() argument validation (#36127)
17 июл 2026, 19:42
Не верифицирован
17 июл 2026, 19:42
f5ecd2e
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; use std::cell::Cell; use std::cell::RefCell; use std::ffi::CStr; use std::ffi::CString; use std::ffi::c_char; use std::ffi::c_void; use std::path::Path; use std::ptr::NonNull; use std::ptr::null; use std::rc::Rc; use deno_core::FromV8; use deno_core::GarbageCollected; use deno_core::OpState; use deno_core::convert::OptionUndefined; use deno_core::cppgc; use deno_core::op2; use deno_core::v8; use deno_core::v8_static_strings; use deno_permissions::OpenAccessKind; use deno_permissions::PermissionsContainer; use rusqlite::ffi as libsqlite3_sys; use rusqlite::ffi::SQLITE_DBCONFIG_DEFENSIVE; use rusqlite::ffi::SQLITE_DBCONFIG_DQS_DDL; use rusqlite::ffi::SQLITE_DBCONFIG_DQS_DML; use rusqlite::ffi::sqlite3_create_window_function; use rusqlite::ffi::sqlite3_db_filename; use rusqlite::limits::Limit; use super::Session; use super::SqliteError; use super::StatementSync; use super::session::SessionOptions; use super::sql_tag_store::SQLTagStore; use super::statement::InnerStatementPtr; use super::statement::check_error_code; use super::statement::check_error_code2; use super::validators; const SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION: i32 = 1005; const SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE: i32 = 1021; const MAX_SAFE_JS_INTEGER: i64 = 9_007_199_254_740_991; const NUM_LIMITS: usize = 11; /// Static mapping of JavaScript property names to SQLite limits. /// Order matches SQLite limit constant values (0-10). /// Keep in sync with LIMIT_NAMES in ext/node/polyfills/sqlite.ts. const LIMIT_MAPPING: [(&str, Limit); NUM_LIMITS] = [ ("length", Limit::SQLITE_LIMIT_LENGTH), ("sqlLength", Limit::SQLITE_LIMIT_SQL_LENGTH), ("column", Limit::SQLITE_LIMIT_COLUMN), ("exprDepth", Limit::SQLITE_LIMIT_EXPR_DEPTH), ("compoundSelect", Limit::SQLITE_LIMIT_COMPOUND_SELECT), ("vdbeOp", Limit::SQLITE_LIMIT_VDBE_OP), ("functionArg", Limit::SQLITE_LIMIT_FUNCTION_ARG), ("attach", Limit::SQLITE_LIMIT_ATTACHED), ("likePatternLength", Limit::SQLITE_LIMIT_LIKE_PATTERN_LENGTH), ("variableNumber", Limit::SQLITE_LIMIT_VARIABLE_NUMBER), ("triggerDepth", Limit::SQLITE_LIMIT_TRIGGER_DEPTH), ]; struct DatabaseSyncOptions { open: bool, enable_foreign_key_constraints: bool, read_only: bool, allow_extension: bool, enable_double_quoted_string_literals: bool, use_big_int_arguments: bool, allow_bare_named_params: bool, return_arrays: bool, allow_unknown_named_params: bool, is_defensive_mode: bool, timeout: u64, initial_limits: [Option<i32>; NUM_LIMITS], } impl<'a> FromV8<'a> for DatabaseSyncOptions { type Error = validators::Error; fn from_v8( scope: &mut v8::PinScope<'a, '_>, value: v8::Local<'a, v8::Value>, ) -> Result<Self, Self::Error> { use validators::Error; if value.is_undefined() { return Ok(Self::default()); } let Ok(obj) = v8::Local::<v8::Object>::try_from(value) else { return Err(Error::InvalidArgType( "The \"options\" argument must be an object.".into(), )); }; let mut options = Self::default(); v8_static_strings! { OPEN_STRING = "open", ENABLE_FOREIGN_KEY_CONSTRAINTS_STRING = "enableForeignKeyConstraints", READ_ONLY_STRING = "readOnly", ALLOW_EXTENSION_STRING = "allowExtension", ENABLE_DOUBLE_QUOTED_STRING_LITERALS_STRING = "enableDoubleQuotedStringLiterals", TIMEOUT_STRING = "timeout", READ_BIG_INTS = "readBigInts", RETURN_ARRAYS = "returnArrays", ALLOW_BARE_NAMED_PARAMS = "allowBareNamedParameters", ALLOW_UNKNOWN_NAMED_PARAMS = "allowUnknownNamedParameters", DEFENSIVE_STRING = "defensive", LIMITS_STRING = "limits", } let open_string = OPEN_STRING.v8_string(scope).unwrap(); if let Some(open) = obj.get(scope, open_string.into()) && !open.is_undefined() { options.open = v8::Local::<v8::Boolean>::try_from(open) .map_err(|_| { Error::InvalidArgType( "The \"options.open\" argument must be a boolean.".into(), ) })? .is_true(); } let read_only_string = READ_ONLY_STRING.v8_string(scope).unwrap(); if let Some(read_only) = obj.get(scope, read_only_string.into()) && !read_only.is_undefined() { options.read_only = v8::Local::<v8::Boolean>::try_from(read_only) .map_err(|_| { Error::InvalidArgType( "The \"options.readOnly\" argument must be a boolean.".into(), ) })? .is_true(); } let enable_foreign_key_constraints_string = ENABLE_FOREIGN_KEY_CONSTRAINTS_STRING .v8_string(scope) .unwrap(); if let Some(enable_foreign_key_constraints) = obj.get(scope, enable_foreign_key_constraints_string.into()) && !enable_foreign_key_constraints.is_undefined() { options.enable_foreign_key_constraints = v8::Local::<v8::Boolean>::try_from(enable_foreign_key_constraints) .map_err(|_| { Error::InvalidArgType("The \"options.enableForeignKeyConstraints\" argument must be a boolean.".into()) })? .is_true(); } let allow_extension_string = ALLOW_EXTENSION_STRING.v8_string(scope).unwrap(); if let Some(allow_extension) = obj.get(scope, allow_extension_string.into()) && !allow_extension.is_undefined() { options.allow_extension = v8::Local::<v8::Boolean>::try_from(allow_extension) .map_err(|_| { Error::InvalidArgType( "The \"options.allowExtension\" argument must be a boolean." .into(), ) })? .is_true(); } let enable_double_quoted_string_literals_string = ENABLE_DOUBLE_QUOTED_STRING_LITERALS_STRING .v8_string(scope) .unwrap(); if let Some(enable_double_quoted_string_literals) = obj.get(scope, enable_double_quoted_string_literals_string.into()) && !enable_double_quoted_string_literals.is_undefined() { options.enable_double_quoted_string_literals = v8::Local::<v8::Boolean>::try_from(enable_double_quoted_string_literals) .map_err(|_| { Error::InvalidArgType("The \"options.enableDoubleQuotedStringLiterals\" argument must be a boolean.".into()) })? .is_true(); } let timeout_string = TIMEOUT_STRING.v8_string(scope).unwrap(); if let Some(timeout) = obj.get(scope, timeout_string.into()) && !timeout.is_undefined() { let timeout = v8::Local::<v8::Integer>::try_from(timeout) .map_err(|_| { Error::InvalidArgType( "The \"options.timeout\" argument must be an integer.".into(), ) })? .value(); if timeout > 0 { options.timeout = timeout as u64; } } let read_big_ints_string = READ_BIG_INTS.v8_string(scope).unwrap(); if let Some(read_big_ints) = obj.get(scope, read_big_ints_string.into()) && !read_big_ints.is_undefined() { options.use_big_int_arguments = v8::Local::<v8::Boolean>::try_from(read_big_ints) .map_err(|_| { Error::InvalidArgType( "The \"options.readBigInts\" argument must be a boolean.".into(), ) })? .is_true(); } let return_arrays_string = RETURN_ARRAYS.v8_string(scope).unwrap(); if let Some(return_arrays) = obj.get(scope, return_arrays_string.into()) && !return_arrays.is_undefined() { options.return_arrays = v8::Local::<v8::Boolean>::try_from(return_arrays) .map_err(|_| { Error::InvalidArgType( "The \"options.returnArrays\" argument must be a boolean.".into(), ) })? .is_true(); } let allow_bare_named_params_string = ALLOW_BARE_NAMED_PARAMS.v8_string(scope).unwrap(); if let Some(allow_bare_named_params) = obj.get(scope, allow_bare_named_params_string.into()) && !allow_bare_named_params.is_undefined() { options.allow_bare_named_params = v8::Local::<v8::Boolean>::try_from(allow_bare_named_params) .map_err(|_| { Error::InvalidArgType("The \"options.allowBareNamedParameters\" argument must be a boolean.".into()) })? .is_true(); } let allow_unknown_named_params_string = ALLOW_UNKNOWN_NAMED_PARAMS.v8_string(scope).unwrap(); if let Some(allow_unknown_named_params) = obj.get(scope, allow_unknown_named_params_string.into()) && !allow_unknown_named_params.is_undefined() { options.allow_unknown_named_params = v8::Local::<v8::Boolean>::try_from(allow_unknown_named_params) .map_err(|_| { Error::InvalidArgType("The \"options.allowUnknownNamedParameters\" argument must be a boolean.".into()) })? .is_true(); } let defensive_string = DEFENSIVE_STRING.v8_string(scope).unwrap(); if let Some(is_defensive_mode) = obj.get(scope, defensive_string.into()) && !is_defensive_mode.is_undefined() { options.is_defensive_mode = v8::Local::<v8::Boolean>::try_from(is_defensive_mode) .map_err(|_| { Error::InvalidArgType( "The \"options.defensive\" argument must be a boolean.".into(), ) })? .is_true(); } let limits_string = LIMITS_STRING.v8_string(scope).unwrap(); if let Some(limits_value) = obj.get(scope, limits_string.into()) && !limits_value.is_undefined() { let limits_obj = v8::Local::<v8::Object>::try_from(limits_value) .map_err(|_| { Error::InvalidArgType( "The \"options.limits\" argument must be an object.".into(), ) })?; for (idx, &(js_name, _limit)) in LIMIT_MAPPING.iter().enumerate() { let key = v8::String::new(scope, js_name).ok_or({ Error::InvalidArgType("Failed to create limit key string.".into()) })?; if let Some(val) = limits_obj.get(scope, key.into()) && !val.is_undefined() { let int_val = v8::Local::<v8::Int32>::try_from(val).map_err(|_| { Error::InvalidArgType( format!( "The \"options.limits.{}\" argument must be an integer.", js_name ) .into(), ) })?; let limit_val = int_val.value(); if limit_val < 0 { return Err(Error::OutOfRange( format!( "The \"options.limits.{}\" argument must be non-negative.", js_name ) .into(), )); } options.initial_limits[idx] = Some(limit_val); } } } Ok(options) } } impl Default for DatabaseSyncOptions { fn default() -> Self { DatabaseSyncOptions { open: true, enable_foreign_key_constraints: true, read_only: false, allow_extension: false, enable_double_quoted_string_literals: false, use_big_int_arguments: false, return_arrays: false, allow_bare_named_params: true, allow_unknown_named_params: false, is_defensive_mode: true, timeout: 0, initial_limits: [None; NUM_LIMITS], } } } struct AggregateFunctionOption<'a> { deterministic: bool, direct_only: bool, use_big_int_arguments: bool, varargs: bool, start: v8::Local<'a, v8::Value>, step: v8::Local<'a, v8::Function>, result: Option<v8::Local<'a, v8::Function>>, inverse: Option<v8::Local<'a, v8::Function>>, } impl<'a> AggregateFunctionOption<'a> { fn from_value( scope: &mut v8::PinScope<'a, '_>, value: v8::Local<'a, v8::Value>, ) -> Result<Self, validators::Error> { use validators::Error; if !value.is_object() { return Err(Error::InvalidArgType( "The \"options\" argument must be an object.".into(), )); } v8_static_strings! { DETERMINISTIC_STRING = "deterministic", DIRECT_ONLY_STRING = "directOnly", USE_BIG_INT_ARGUMENTS_STRING = "useBigIntArguments", VARARGS_STRING = "varargs", START_STRING = "start", STEP_STRING = "step", RESULT_STRING = "result", INVERSE_STRING = "inverse", } let object = v8::Local::<v8::Object>::try_from(value).map_err(|_| { Error::InvalidArgType( "The \"options\" argument must be an object.".into(), ) })?; let start_key = START_STRING.v8_string(scope).unwrap(); let start_value = object .get(scope, start_key.into()) .ok_or(Error::V8Exception)?; if start_value.is_undefined() { return Err(Error::InvalidArgType("The \"options.start\" argument must be a function or a primitive value.".into())); } let step_key = STEP_STRING.v8_string(scope).unwrap(); let step_value = object .get(scope, step_key.into()) .ok_or(Error::V8Exception)?; let step_function = v8::Local::<v8::Function>::try_from(step_value) .map_err(|_| { Error::InvalidArgType( "The \"options.step\" argument must be a function.".into(), ) })?; let result_key = RESULT_STRING.v8_string(scope).unwrap(); let result_value = object .get(scope, result_key.into()) .ok_or(Error::V8Exception)?; let result_function = if result_value.is_undefined() { None } else { let func = v8::Local::<v8::Function>::try_from(result_value).map_err(|_| { Error::InvalidArgType( "The \"options.result\" argument must be a function.".into(), ) })?; Some(func) }; let mut deterministic = false; let mut use_big_int_arguments = false; let mut varargs = false; let mut direct_only = false; let deterministic_key = DETERMINISTIC_STRING.v8_string(scope).unwrap(); let deterministic_value = object .get(scope, deterministic_key.into()) .ok_or(Error::V8Exception)?; if !deterministic_value.is_undefined() { if !deterministic_value.is_boolean() { return Err(Error::InvalidArgType( "The \"options.deterministic\" argument must be a boolean.".into(), )); } deterministic = deterministic_value.boolean_value(scope); } let use_bigint_key = USE_BIG_INT_ARGUMENTS_STRING.v8_string(scope).unwrap(); let bigint_value = object .get(scope, use_bigint_key.into()) .ok_or(Error::V8Exception)?; if !bigint_value.is_undefined() { if !bigint_value.is_boolean() { return Err(Error::InvalidArgType( "The \"options.useBigIntArguments\" argument must be a boolean." .into(), )); } use_big_int_arguments = bigint_value.boolean_value(scope); } let varargs_key = VARARGS_STRING.v8_string(scope).unwrap(); let varargs_value = object .get(scope, varargs_key.into()) .ok_or(Error::V8Exception)?; if !varargs_value.is_undefined() { if !varargs_value.is_boolean() { return Err(Error::InvalidArgType( "The \"options.varargs\" argument must be a boolean.".into(), )); } varargs = varargs_value.boolean_value(scope); } let direct_only_key = DIRECT_ONLY_STRING.v8_string(scope).unwrap(); let direct_only_value = object .get(scope, direct_only_key.into()) .ok_or(Error::V8Exception)?; if !direct_only_value.is_undefined() { if !direct_only_value.is_boolean() { return Err(Error::InvalidArgType( "The \"options.directOnly\" argument must be a boolean.".into(), )); } direct_only = direct_only_value.boolean_value(scope); } let inverse_key = INVERSE_STRING.v8_string(scope).unwrap(); let inverse_value = object .get(scope, inverse_key.into()) .ok_or(Error::V8Exception)?; let inverse_function = if inverse_value.is_undefined() { None } else { let func = v8::Local::<v8::Function>::try_from(inverse_value).map_err(|_| { Error::InvalidArgType( "The \"options.inverse\" argument must be a function.".into(), ) })?; Some(func) }; Ok(AggregateFunctionOption { deterministic, direct_only, use_big_int_arguments, varargs, start: start_value, step: step_function, result: result_function, inverse: inverse_function, }) } } struct ApplyChangesetOptions<'a> { filter: Option<v8::Local<'a, v8::Value>>, on_conflict: Option<v8::Local<'a, v8::Value>>, } // Note: Can't use `FromV8` here because of lifetime issues with holding // Local references. impl<'a> ApplyChangesetOptions<'a> { fn from_value( scope: &mut v8::PinScope<'a, '_>, value: v8::Local<'a, v8::Value>, ) -> Result<Option<Self>, validators::Error> { use validators::Error; if value.is_undefined() { return Ok(None); } let obj = v8::Local::<v8::Object>::try_from(value).map_err(|_| { Error::InvalidArgType( "The \"options\" argument must be an object.".into(), ) })?; let mut options = Self { filter: None, on_conflict: None, }; v8_static_strings! { FILTER_STRING = "filter", ON_CONFLICT_STRING = "onConflict", } let filter_string = FILTER_STRING.v8_string(scope).unwrap(); if let Some(filter) = obj.get(scope, filter_string.into()) && !filter.is_undefined() { if !filter.is_function() { return Err(Error::InvalidArgType( "The \"options.filter\" argument must be a function.".into(), )); } options.filter = Some(filter); } let on_conflict_string = ON_CONFLICT_STRING.v8_string(scope).unwrap(); if let Some(on_conflict) = obj.get(scope, on_conflict_string.into()) && !on_conflict.is_undefined() { if !on_conflict.is_function() { return Err(Error::InvalidArgType( "The \"options.onConflict\" argument must be a function.".into(), )); } options.on_conflict = Some(on_conflict); } Ok(Some(options)) } } pub struct DatabaseSync { pub conn: Rc<RefCell<Option<rusqlite::Connection>>>, statements: Rc<RefCell<Vec<InnerStatementPtr>>>, options: DatabaseSyncOptions, location: String, ignore_next_sqlite_error: Rc<Cell<bool>>, authorizer_data: Rc<RefCell<Option<*mut AuthorizerData>>>, // Non-zero while a user-defined callback is on the stack; close() refuses // to free the connection in that state to avoid a SQLite VDBE use-after-free. callback_depth: Rc<Cell<usize>>, // Whether ATTACH DATABASE is held disabled because the process lacks full // permissions for the database path. Used to stop the `limits.attach` setter // from raising the cap and bypassing the boundary. disable_attach: Rc<Cell<bool>>, } struct CallbackDepthGuard { depth: Rc<Cell<usize>>, } impl CallbackDepthGuard { fn new(depth: &Rc<Cell<usize>>) -> Self { depth.set(depth.get().saturating_add(1)); Self { depth: Rc::clone(depth), } } } impl Drop for CallbackDepthGuard { fn drop(&mut self) { self.depth.set(self.depth.get().saturating_sub(1)); } } // SAFETY: we're sure this can be GCed unsafe impl GarbageCollected for DatabaseSync { fn trace(&self, _visitor: &mut deno_core::v8::cppgc::Visitor) {} fn get_name(&self) -> &'static std::ffi::CStr { c"DatabaseSync" } } fn set_db_config( conn: &rusqlite::Connection, config: i32, value: bool, ) -> bool { // SAFETY: call to sqlite3_db_config is safe because the connection // handle is valid and the parameters are correct. unsafe { let mut set = 0; let r = libsqlite3_sys::sqlite3_db_config( conn.handle(), config, value as i32, &mut set, ); if r != libsqlite3_sys::SQLITE_OK { panic!("Failed to set db config"); } set == value as i32 } } enum LimitCoercionError { NotANumber, NotAnInteger, Negative, } /// Coerce a v8 value to a valid SQLite limit integer. /// Accepts non-negative integers and positive Infinity (mapped to i32::MAX). fn coerce_limit_value( scope: &mut v8::PinScope<'_, '_>, value: v8::Local<v8::Value>, ) -> Result<i32, LimitCoercionError> { if !value.is_number() { return Err(LimitCoercionError::NotANumber); } let num_value = value.number_value(scope).unwrap_or(f64::NAN); if num_value.is_infinite() && num_value > 0.0 { return Ok(i32::MAX); } if !value.is_int32() { return Err(LimitCoercionError::NotAnInteger); } let int_val = value.int32_value(scope).unwrap_or(-1); if int_val < 0 { return Err(LimitCoercionError::Negative); } Ok(int_val) } /// Apply initial limits to a connection based on options fn apply_initial_limits( conn: &rusqlite::Connection, options: &DatabaseSyncOptions, disable_attach: bool, ) -> Result<(), SqliteError> { for (idx, &(_js_name, limit)) in LIMIT_MAPPING.iter().enumerate() { if let Some(value) = options.initial_limits[idx] { // When the process lacks full permissions for the database path, the // attach limit is a security cap held at 0: ATTACH DATABASE can reach // files outside the database path, so it must not be raised through the // user-controlled `limits.attach` option. Reject raising it above 0, // mirroring the `db.limits.attach` setter. A value of 0 (keeping it // disabled) is still accepted. if disable_attach && matches!(limit, Limit::SQLITE_LIMIT_ATTACHED) && value > 0 { return Err(SqliteError::AttachLimitDenied); } conn.set_limit(limit, value)?; } } Ok(()) } // Returns the open connection together with `disable_attach`: whether the // process lacks full permissions for the database path and so ATTACH DATABASE // is held disabled. Callers store this so the `limits.attach` setter cannot // later raise the cap and bypass the boundary. fn open_db( state: &mut OpState, location: &str, options: &DatabaseSyncOptions, ) -> Result<(rusqlite::Connection, bool), SqliteError> { let perms = state.borrow::<PermissionsContainer>(); let disable_attach = perms .check_has_all_permissions(Path::new(location)) .is_err(); if location == ":memory:" { let conn = rusqlite::Connection::open_in_memory()?; if disable_attach { assert!(set_db_config( &conn, SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE, false )); conn.set_limit(Limit::SQLITE_LIMIT_ATTACHED, 0)?; } if options.allow_extension { perms.check_ffi_all()?; } else { assert!(set_db_config( &conn, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, false )); } assert!(set_db_config( &conn, SQLITE_DBCONFIG_DEFENSIVE, options.is_defensive_mode, )); apply_initial_limits(&conn, options, disable_attach)?; return Ok((conn, disable_attach)); } let location = perms .check_open( Cow::Borrowed(Path::new(location)), match options.read_only { true => OpenAccessKind::ReadNoFollow, false => OpenAccessKind::ReadWriteNoFollow, }, Some("node:sqlite"), )? .into_path(); if options.read_only { let conn = rusqlite::Connection::open_with_flags( location, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY, )?; if disable_attach { assert!(set_db_config( &conn, SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE, false )); conn.set_limit(Limit::SQLITE_LIMIT_ATTACHED, 0)?; } if options.allow_extension { perms.check_ffi_all()?; } else { assert!(set_db_config( &conn, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, false )); } assert!(set_db_config( &conn, SQLITE_DBCONFIG_DEFENSIVE, options.is_defensive_mode, )); apply_initial_limits(&conn, options, disable_attach)?; return Ok((conn, disable_attach)); } let conn = rusqlite::Connection::open(location)?; conn.busy_timeout(std::time::Duration::from_millis(options.timeout))?; if options.allow_extension { perms.check_ffi_all()?; } else { assert!(set_db_config( &conn, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, false )); } if disable_attach { conn.set_limit(Limit::SQLITE_LIMIT_ATTACHED, 0)?; } assert!(set_db_config( &conn, SQLITE_DBCONFIG_DEFENSIVE, options.is_defensive_mode, )); apply_initial_limits(&conn, options, disable_attach)?; Ok((conn, disable_attach)) } fn is_open( scope: &mut v8::PinScope<'_, '_>, args: &v8::FunctionCallbackArguments, ) -> Result<(), SqliteError> { let this_ = args.this(); let db = cppgc::try_unwrap_cppgc_object::<DatabaseSync>(scope, this_.into()) .ok_or(SqliteError::AlreadyClosed)?; db.conn .borrow() .as_ref() .ok_or(SqliteError::AlreadyClosed)?; Ok(()) } // Represents a single connection to a SQLite database. #[op2] impl DatabaseSync { // Constructs a new `DatabaseSync` instance. // // A SQLite database can be stored in a file or in memory. To // use a file-backed database, the `location` should be a path. // To use an in-memory database, the `location` should be special // name ":memory:". #[constructor] #[cppgc] fn new( state: &mut OpState, #[string] location: String, #[scoped] options: DatabaseSyncOptions, ) -> Result<DatabaseSync, SqliteError> { let mut disable_attach = false; let db = if options.open { let (db, da) = open_db(state, &location, &options)?; disable_attach = da; if options.enable_foreign_key_constraints { db.execute("PRAGMA foreign_keys = ON", [])?; } else { db.execute("PRAGMA foreign_keys = OFF", [])?; } set_db_config( &db, SQLITE_DBCONFIG_DQS_DDL, options.enable_double_quoted_string_literals, ); set_db_config( &db, SQLITE_DBCONFIG_DQS_DML, options.enable_double_quoted_string_literals, ); Some(db) } else { None }; Ok(DatabaseSync { conn: Rc::new(RefCell::new(db)), statements: Rc::new(RefCell::new(Vec::new())), location, options, ignore_next_sqlite_error: Rc::new(Cell::new(false)), authorizer_data: Rc::new(RefCell::new(None)), callback_depth: Rc::new(Cell::new(0)), disable_attach: Rc::new(Cell::new(disable_attach)), }) } // Opens the database specified by `location` of this instance. // // This method should only be used when the database is not opened // via the constructor. An exception is thrown if the database is // already opened. #[fast] #[undefined] fn open(&self, state: &mut OpState) -> Result<(), SqliteError> { if self.conn.borrow().is_some() { return Err(SqliteError::AlreadyOpen); } let (db, disable_attach) = open_db(state, &self.location, &self.options)?; if self.options.enable_foreign_key_constraints { db.execute("PRAGMA foreign_keys = ON", [])?; } else { db.execute("PRAGMA foreign_keys = OFF", [])?; } set_db_config( &db, SQLITE_DBCONFIG_DQS_DDL, self.options.enable_double_quoted_string_literals, ); set_db_config( &db, SQLITE_DBCONFIG_DQS_DML, self.options.enable_double_quoted_string_literals, ); *self.conn.borrow_mut() = Some(db); self.disable_attach.set(disable_attach); Ok(()) } // Closes the database connection. An exception is thrown if the // database is not open. #[fast] #[undefined] fn close(&self) -> Result<(), SqliteError> { if self.conn.borrow().is_none() { return Err(SqliteError::AlreadyClosed); } // Refuse to close while a user-defined callback is on the SQLite stack — // freeing the in-flight statement would be a VDBE use-after-free. if self.callback_depth.get() > 0 { return Err(SqliteError::ActiveCallback); } // Finalize all prepared statements for stmt in self.statements.borrow_mut().drain(..) { match stmt.get() { None => continue, Some(ptr) => { // SAFETY: `ptr` is a valid statement handle. unsafe { libsqlite3_sys::sqlite3_finalize(ptr); }; stmt.set(None); } }; } { let mut authorizer_data = self.authorizer_data.borrow_mut(); if let Some(data_ptr) = authorizer_data.take() { // SAFETY: data_ptr was allocated in authorizer setup. unsafe { release_authorizer_data(data_ptr); } } } let _ = self.conn.borrow_mut().take(); Ok(()) } // This method allows one or more SQL statements to be executed // without returning any results. // // This method is a wrapper around sqlite3_exec(). #[fast] #[reentrant] #[validate(is_open)] #[undefined] fn exec( &self, #[validate(validators::sql_str)] #[string] sql: &str, ) -> Result<(), SqliteError> { let db = self.conn.borrow(); let db = db.as_ref().ok_or(SqliteError::InUse)?; if let Err(err) = db.execute_batch(sql) { if self.consume_ignore_next_sqlite_error() { return Ok(()); } return Err(err.into()); } Ok(()) } // Compiles an SQL statement into a prepared statement. // // This method is a wrapper around `sqlite3_prepare_v2()`. #[reentrant] #[validate(is_open)] #[cppgc] fn prepare( &self, scope: &mut v8::PinScope<'_, '_>, #[validate(validators::sql_str)] #[string] sql: &str, #[varargs] args: Option<&v8::FunctionCallbackArguments>, ) -> Result<StatementSync, SqliteError> { let db = self.conn.borrow(); let db = db.as_ref().ok_or(SqliteError::InUse)?; // SAFETY: lifetime of the connection is guaranteed by reference // counting. let raw_handle = unsafe { db.handle() }; let mut raw_stmt = std::ptr::null_mut(); // SAFETY: `sql` points to a valid memory location and its length // is correct. let r = unsafe { libsqlite3_sys::sqlite3_prepare_v2( raw_handle, sql.as_ptr() as *const _, sql.len() as i32, &mut raw_stmt, std::ptr::null_mut(), ) }; check_error_code(r, raw_handle)?; let mut allow_unknown_named_params = self.options.allow_unknown_named_params; let mut use_big_ints = self.options.use_big_int_arguments; let mut return_arrays = self.options.return_arrays; let mut allow_bare_named_params = self.options.allow_bare_named_params; // Parse options object (second argument) if let Some(args) = args { // args[0] is already consumed as `sql`, so options is args[1] if args.length() > 1 { let options = args.get(1); if !options.is_undefined() && !options.is_null() { let options = v8::Local::<v8::Object>::try_from(options).map_err(|_| { SqliteError::Validation(validators::Error::InvalidArgType( "The \"options\" argument must be an object.".into(), )) })?; v8_static_strings! { ALLOW_UNKNOWN = "allowUnknownNamedParameters", READ_BIG_INTS = "readBigInts", RETURN_ARRAYS = "returnArrays", ALLOW_BARE = "allowBareNamedParameters", } macro_rules! parse_bool_opt { ($key:expr, $name:expr, $target:ident) => { let key = $key.v8_string(scope).unwrap().into(); if let Some(val) = options.get(scope, key) { if !val.is_undefined() { $target = v8::Local::<v8::Boolean>::try_from(val) .map_err(|_| { SqliteError::Validation( validators::Error::InvalidArgType( concat!( "The \"", $name, "\" argument must be a boolean." ) .into(), ), ) })? .is_true(); } } }; } parse_bool_opt!( ALLOW_UNKNOWN, "options.allowUnknownNamedParameters", allow_unknown_named_params ); parse_bool_opt!(READ_BIG_INTS, "options.readBigInts", use_big_ints); parse_bool_opt!(RETURN_ARRAYS, "options.returnArrays", return_arrays); parse_bool_opt!( ALLOW_BARE, "options.allowBareNamedParameters", allow_bare_named_params ); } } } let stmt_cell = Rc::new(Cell::new(Some(raw_stmt))); self.statements.borrow_mut().push(stmt_cell.clone()); Ok(StatementSync { inner: stmt_cell, db: self.conn.clone(), statements: Rc::clone(&self.statements), ignore_next_sqlite_error: Rc::clone(&self.ignore_next_sqlite_error), return_arrays: Cell::new(return_arrays), use_big_ints: Cell::new(use_big_ints), allow_bare_named_params: Cell::new(allow_bare_named_params), allow_unknown_named_params: Cell::new(allow_unknown_named_params), is_iter_finished: Cell::new(false), iter_generation: Cell::new(0), iter_contexts: RefCell::new(Vec::new()), }) } #[fast] #[validate(is_open)] #[undefined] fn function<'a>( &self, scope: &mut v8::PinScope<'a, '_>, #[varargs] args: Option<&v8::FunctionCallbackArguments>, ) -> Result<(), SqliteError> { let Some(args) = args.filter(|args| args.length() > 0) else { return Err( validators::Error::InvalidArgType( "The \"name\" argument must be a string.".into(), ) .into(), ); }; if !args.get(0).is_string() { return Err( validators::Error::InvalidArgType( "The \"name\" argument must be a string.".into(), ) .into(), ); } let name = args.get(0).to_rust_string_lossy(scope); let (options_value, function_value) = if args.length() < 3 { (None, args.get(1)) } else { (Some(args.get(1)), args.get(2)) }; let Ok(function) = v8::Local::<v8::Function>::try_from(function_value) else { return Err( validators::Error::InvalidArgType( "The \"function\" argument must be a function.".into(), ) .into(), ); }; let mut use_big_int_arguments = false; let mut varargs = false; let mut deterministic = false; let mut direct_only = false; if let Some(value) = options_value && !value.is_undefined() { if value.is_null() || !value.is_object() { return Err( validators::Error::InvalidArgType( "The \"options\" argument must be an object.".into(), ) .into(), ); } let options = v8::Local::<v8::Object>::try_from(value).unwrap(); v8_static_strings! { USE_BIG_INT_ARGUMENTS = "useBigIntArguments", VARARGS = "varargs", DETERMINISTIC = "deterministic", DIRECT_ONLY = "directOnly", } let use_bigint_key = USE_BIG_INT_ARGUMENTS.v8_string(scope).unwrap(); let bigint_value = options .get(scope, use_bigint_key.into()) .ok_or(validators::Error::V8Exception)?; if !bigint_value.is_undefined() { if !bigint_value.is_boolean() { return Err( validators::Error::InvalidArgType( "The \"options.useBigIntArguments\" argument must be a boolean." .into(), ) .into(), ); } use_big_int_arguments = bigint_value.boolean_value(scope); } let varargs_key = VARARGS.v8_string(scope).unwrap(); let varargs_value = options .get(scope, varargs_key.into()) .ok_or(validators::Error::V8Exception)?; if !varargs_value.is_undefined() { if !varargs_value.is_boolean() { return Err( validators::Error::InvalidArgType( "The \"options.varargs\" argument must be a boolean.".into(), ) .into(), ); } varargs = varargs_value.boolean_value(scope); } let deterministic_key = DETERMINISTIC.v8_string(scope).unwrap(); let deterministic_value = options .get(scope, deterministic_key.into()) .ok_or(validators::Error::V8Exception)?; if !deterministic_value.is_undefined() { if !deterministic_value.is_boolean() { return Err( validators::Error::InvalidArgType( "The \"options.deterministic\" argument must be a boolean." .into(), ) .into(), ); } deterministic = deterministic_value.boolean_value(scope); } let direct_only_key = DIRECT_ONLY.v8_string(scope).unwrap(); let direct_only_value = options .get(scope, direct_only_key.into()) .ok_or(validators::Error::V8Exception)?; if !direct_only_value.is_undefined() { if !direct_only_value.is_boolean() { return Err( validators::Error::InvalidArgType( "The \"options.directOnly\" argument must be a boolean.".into(), ) .into(), ); } direct_only = direct_only_value.boolean_value(scope); } } v8_static_strings! { LENGTH = "length", } let argc = if varargs { -1 } else { let length_key = LENGTH.v8_string(scope).unwrap(); let length = function .get(scope, length_key.into()) .ok_or(validators::Error::V8Exception)?; length .int32_value(scope) .ok_or(validators::Error::V8Exception)? }; let db = self.conn.borrow(); let db = db.as_ref().ok_or(SqliteError::InUse)?; // SAFETY: lifetime of the connection is guaranteed by reference counting. let raw_handle = unsafe { db.handle() }; let name_cstring = CString::new(name)?; let callback = v8::Global::new(scope, function).into_raw(); let context = v8::Global::new(scope, scope.get_current_context()).into_raw(); let data = Box::new(CustomFunctionData { callback, context, use_big_int_arguments, ignore_next_sqlite_error: Rc::clone(&self.ignore_next_sqlite_error), callback_depth: Rc::clone(&self.callback_depth), }); let data_ptr = Box::into_raw(data); let mut text_rep = libsqlite3_sys::SQLITE_UTF8; if deterministic { text_rep |= libsqlite3_sys::SQLITE_DETERMINISTIC; } if direct_only { text_rep |= libsqlite3_sys::SQLITE_DIRECTONLY; } // SAFETY: `raw_handle` is a valid database handle. // `data_ptr` points to a valid memory location. // The v8 handles that are held in `CustomFunctionData` will be // dropped when the data is destroyed via `custom_function_destroy`. let result = unsafe { libsqlite3_sys::sqlite3_create_function_v2( raw_handle, name_cstring.as_ptr(), argc, text_rep, data_ptr as *mut c_void, Some(custom_function_handler), None, None, Some(custom_function_destroy), ) }; check_error_code(result, raw_handle)?; Ok(()) } // Applies a changeset to the database. // // This method is a wrapper around `sqlite3changeset_apply()`. #[fast] #[reentrant] fn apply_changeset<'a>( &self, scope: &mut v8::PinScope<'a, '_>, #[validate(validators::changeset_buffer)] #[buffer] changeset: &[u8], options: v8::Local<'a, v8::Value>, ) -> Result<bool, SqliteError> { let options = ApplyChangesetOptions::from_value(scope, options)?; struct HandlerCtx<'a, 'b, 'c> { scope: &'a mut v8::PinScope<'b, 'c>, conflict: Option<v8::Local<'b, v8::Function>>, filter: Option<v8::Local<'b, v8::Function>>, has_caught: bool, } // Conflict handler callback for `sqlite3changeset_apply()`. unsafe extern "C" fn conflict_handler( p_ctx: *mut c_void, e_conflict: i32, _: *mut libsqlite3_sys::sqlite3_changeset_iter, ) -> i32 { #[allow( clippy::undocumented_unsafe_blocks, reason = "safety comment on the containing block" )] unsafe { let ctx = &mut *(p_ctx as *mut HandlerCtx); if let Some(conflict) = &mut ctx.conflict { let recv = v8::undefined(ctx.scope).into(); let args = [v8::Integer::new(ctx.scope, e_conflict).into()]; v8::tc_scope!(tc_scope, ctx.scope); let ret = conflict .call(tc_scope, recv, &args) .unwrap_or_else(|| v8::undefined(tc_scope).into()); if tc_scope.has_caught() { tc_scope.rethrow(); return libsqlite3_sys::SQLITE_CHANGESET_ABORT; } const INVALID_VALUE: i32 = -1; if !ret.is_int32() { return INVALID_VALUE; } let value = ret .int32_value(tc_scope) .unwrap_or(libsqlite3_sys::SQLITE_CHANGESET_ABORT); return value; } libsqlite3_sys::SQLITE_CHANGESET_ABORT } } // Filter handler callback for `sqlite3changeset_apply()`. unsafe extern "C" fn filter_handler( p_ctx: *mut c_void, z_tab: *const c_char, ) -> i32 { #[allow( clippy::undocumented_unsafe_blocks, reason = "safety comment on the containing block" )] unsafe { let ctx = &mut *(p_ctx as *mut HandlerCtx); // If we've already caught an exception, don't call the filter again // to avoid overwriting the original exception. if ctx.has_caught { return 0; } if let Some(filter) = &mut ctx.filter { let tab = CStr::from_ptr(z_tab).to_str().unwrap(); let recv = v8::undefined(ctx.scope).into(); let args = [v8::String::new(ctx.scope, tab).unwrap().into()]; v8::tc_scope!(tc_scope, ctx.scope); let ret = filter .call(tc_scope, recv, &args) .unwrap_or_else(|| v8::undefined(tc_scope).into()); if tc_scope.has_caught() { ctx.has_caught = true; tc_scope.rethrow(); return 0; } return ret.boolean_value(tc_scope) as i32; } 1 } } let db = self.conn.borrow(); let db = db.as_ref().ok_or(SqliteError::AlreadyClosed)?; // It is safe to use scope in the handlers because they are never // called after the call to `sqlite3changeset_apply()`. let mut ctx = HandlerCtx { scope, conflict: None, filter: None, has_caught: false, }; if let Some(options) = options { if let Some(filter) = options.filter { let filter_cb: v8::Local<v8::Function> = filter .try_into() .map_err(|_| SqliteError::InvalidCallback("filter"))?; ctx.filter = Some(filter_cb); } if let Some(on_conflict) = options.on_conflict { let on_conflict_cb: v8::Local<v8::Function> = on_conflict .try_into() .map_err(|_| SqliteError::InvalidCallback("onConflict"))?; ctx.conflict = Some(on_conflict_cb); } } // SAFETY: lifetime of the connection is guaranteed by reference // counting. let raw_handle = unsafe { db.handle() }; // Block close() while filter/onConflict callbacks may be on the stack. let _apply_depth_guard = CallbackDepthGuard::new(&self.callback_depth); // SAFETY: `changeset` points to a valid memory location and its // length is correct. `ctx` is stack allocated and its lifetime is // longer than the call to `sqlite3changeset_apply()`. unsafe { let r = libsqlite3_sys::sqlite3changeset_apply( raw_handle, changeset.len() as i32, changeset.as_ptr() as *mut _, Some(filter_handler), Some(conflict_handler), &mut ctx as *mut _ as *mut c_void, ); if r == libsqlite3_sys::SQLITE_OK { return Ok(true); } else if r == libsqlite3_sys::SQLITE_ABORT { return Ok(false); } check_error_code2(r)?; Ok(false) } } // Loads a SQLite extension. // // This is a wrapper around `sqlite3_load_extension`. It requires FFI permission // to be granted and allowExtension must be set to true when opening the database. fn load_extension( &self, state: &mut OpState, #[validate(validators::path_str)] #[string] path: &str, #[string] entry_point: Option<String>, ) -> Result<(), SqliteError> { let db = self.conn.borrow(); let db = db.as_ref().ok_or(SqliteError::AlreadyClosed)?; if !self.options.allow_extension { return Err(SqliteError::LoadExensionFailed( "Cannot load SQLite extensions when allowExtension is not enabled" .to_string(), )); } state.borrow::<PermissionsContainer>().check_ffi_all()?; // SAFETY: lifetime of the connection is guaranteed by reference counting. let raw_handle = unsafe { db.handle() }; let path_cstring = std::ffi::CString::new(path.as_bytes())?; let entry_point_cstring = entry_point.map(|ep| std::ffi::CString::new(ep).unwrap_or_default()); let entry_point_ptr = match &entry_point_cstring { Some(cstr) => cstr.as_ptr(), None => std::ptr::null(), }; let mut err_msg: *mut c_char = std::ptr::null_mut(); // SAFETY: Using sqlite3_load_extension with proper error handling let result = unsafe { let res = libsqlite3_sys::sqlite3_load_extension( raw_handle, path_cstring.as_ptr(), entry_point_ptr, &mut err_msg, ); if res != libsqlite3_sys::SQLITE_OK { let error_message = if !err_msg.is_null() { let c_str = std::ffi::CStr::from_ptr(err_msg); let message = c_str.to_string_lossy().into_owned(); libsqlite3_sys::sqlite3_free(err_msg as *mut _); message } else { format!("Failed to load extension with error code: {}", res) }; return Err(SqliteError::LoadExensionFailed(error_message)); } res }; if result == libsqlite3_sys::SQLITE_OK { Ok(()) } else { Err(SqliteError::LoadExensionFailed( "Unknown error loading SQLite extension".to_string(), )) } } // Creates and attaches a session to the database. // // This method is a wrapper around `sqlite3session_create()` and // `sqlite3session_attach()`. #[cppgc] fn create_session( &self, #[scoped] options: OptionUndefined<SessionOptions>, ) -> Result<Session, SqliteError> { let options = options.0; let db = self.conn.borrow(); let db = db.as_ref().ok_or(SqliteError::AlreadyClosed)?; // SAFETY: lifetime of the connection is guaranteed by reference // counting. let raw_handle = unsafe { db.handle() }; let mut options = options; let z_db = options .as_mut() .and_then(|options| options.db.take()) .map(CString::new) .transpose()? .unwrap_or_else(|| c"main".to_owned()); let table = options .as_mut() .and_then(|options| options.table.take()) .map(CString::new) .transpose()?; let mut raw_session = std::ptr::null_mut(); // SAFETY: `z_db` points to a valid c-string. let r = unsafe { libsqlite3_sys::sqlite3session_create( raw_handle, z_db.as_ptr() as *const _, &mut raw_session, ) }; if r != libsqlite3_sys::SQLITE_OK { return Err(SqliteError::SessionCreateFailed); } let z_table = table.as_ref().map(|table| table.as_ptr()).unwrap_or(null()); let r = // SAFETY: `z_table` points to a valid c-string and `raw_session` // is a valid session handle. unsafe { libsqlite3_sys::sqlite3session_attach(raw_session, z_table) }; if r != libsqlite3_sys::SQLITE_OK { return Err(SqliteError::SessionCreateFailed); } Ok(Session { inner: raw_session, freed: Cell::new(false), db: self.conn.clone(), }) } fn location<'a>( &self, scope: &mut v8::PinScope<'a, '_>, name_value: v8::Local<'a, v8::Value>, ) -> Result<v8::Local<'a, v8::Value>, SqliteError> { let db = self.conn.borrow(); let db = db.as_ref().ok_or(SqliteError::AlreadyClosed)?; let name = if !name_value.is_undefined() { if !name_value.is_string() { return Err(SqliteError::Validation( validators::Error::InvalidArgType( "The \"dbName\" argument must be a string.".into(), ), )); } name_value.to_rust_string_lossy(scope) } else { "main".to_string() }; // SAFETY: lifetime of the connection is guaranteed by reference counting. let raw_handle = unsafe { db.handle() }; let name_cstring = CString::new(name)?; // SAFETY: `raw_handle` is a valid sqlite3 pointer. let db_filename = unsafe { sqlite3_db_filename(raw_handle, name_cstring.as_ptr()) }; if db_filename.is_null() { return Ok(v8::null(scope).into()); } // SAFETY: `db_filename` is a valid C string pointer. let filename_cstr = unsafe { CStr::from_ptr(db_filename).to_bytes() }; if filename_cstr.is_empty() { return Ok(v8::null(scope).into()); } let filename = v8::String::new_from_utf8( scope, filename_cstr, v8::NewStringType::Normal, ) .unwrap(); Ok(filename.into()) } #[fast] #[validate(is_open)] fn enable_defensive( &self, #[validate(validators::active_bool)] is_enabled: bool, ) -> Result<(), SqliteError> { let db = self.conn.borrow(); let conn = db.as_ref().ok_or(SqliteError::AlreadyClosed)?; assert!(set_db_config(conn, SQLITE_DBCONFIG_DEFENSIVE, is_enabled)); Ok(()) } // Sets an authorizer callback that SQLite will invoke whenever it attempts // to access data or modify the database schema through prepared statements. // This can be used to implement security policies, audit access, or restrict // certain operations. #[fast] #[validate(is_open)] #[undefined] fn set_authorizer<'a>( &self, scope: &mut v8::PinScope<'a, '_>, callback: v8::Local<'a, v8::Value>, ) -> Result<(), SqliteError> { let db = self.conn.borrow(); let conn = db.as_ref().ok_or(SqliteError::AlreadyClosed)?; // SAFETY: lifetime of the connection is guaranteed by reference counting. let raw_handle = unsafe { conn.handle() }; if callback.is_null() { // SAFETY: `raw_handle` is a valid database handle. let result = unsafe { libsqlite3_sys::sqlite3_set_authorizer( raw_handle, None, std::ptr::null_mut(), ) }; check_error_code(result, raw_handle)?; let mut authorizer_data = self.authorizer_data.borrow_mut(); if let Some(old_data_ptr) = authorizer_data.take() { // SAFETY: data_ptr was allocated in authorizer setup. unsafe { release_authorizer_data(old_data_ptr); } } return Ok(()); } let Ok(function) = v8::Local::<v8::Function>::try_from(callback) else { return Err( validators::Error::InvalidArgType( "The \"callback\" argument must be a function or null.".into(), ) .into(), ); }; let callback_global = v8::Global::new(scope, function).into_raw(); let context_global = v8::Global::new(scope, scope.get_current_context()).into_raw(); let data = Box::new(AuthorizerData { callback: callback_global, context: context_global, ignore_next_sqlite_error: Rc::clone(&self.ignore_next_sqlite_error), callback_depth: Rc::clone(&self.callback_depth), active_callbacks: Cell::new(0), free_after_callback: Cell::new(false), }); let data_ptr = Box::into_raw(data); // SAFETY: `raw_handle` is a valid database handle. // `data_ptr` points to a valid memory location. let result = unsafe { libsqlite3_sys::sqlite3_set_authorizer( raw_handle, Some(authorizer_callback), data_ptr as *mut c_void, ) }; if result != libsqlite3_sys::SQLITE_OK { // Clean up the data we allocated if setting the authorizer failed // SAFETY: we just allocated this data and it failed to be registered unsafe { free_authorizer_data(data_ptr); } check_error_code(result, raw_handle)?; } let mut authorizer_data = self.authorizer_data.borrow_mut(); if let Some(old_data_ptr) = authorizer_data.replace(data_ptr) { // SAFETY: data_ptr was allocated in authorizer setup. unsafe { release_authorizer_data(old_data_ptr); } } Ok(()) } #[getter] fn is_open(&self) -> bool { self.conn.borrow().is_some() } #[getter] fn is_transaction(&self) -> Result<bool, SqliteError> { let db = self.conn.borrow(); let db = db.as_ref().ok_or(SqliteError::AlreadyClosed)?; // SAFETY: lifetime of the connection is guaranteed by reference counting. let res = unsafe { libsqlite3_sys::sqlite3_get_autocommit(db.handle()) }; Ok(res == 0) } #[getter] #[cppgc] fn limits(&self) -> Result<DatabaseSyncLimits, SqliteError> { if self.conn.borrow().is_none() { return Err(SqliteError::AlreadyClosed); } Ok(DatabaseSyncLimits::create( Rc::clone(&self.conn), self.disable_attach.get(), )) } #[fast] #[validate(is_open)] fn aggregate<'a>( &self, scope: &mut v8::PinScope<'a, '_>, #[validate(validators::name_str)] #[string] name: &str, options: v8::Local<'a, v8::Value>, ) -> Result<(), SqliteError> { use std::cmp; let options = AggregateFunctionOption::from_value(scope, options)?; let mut argc = -1; if !options.varargs { v8_static_strings! { LENGTH = "length", } let length_key = LENGTH.v8_string(scope).unwrap(); let step_fn_length = options .step .get(scope, length_key.into()) .ok_or(validators::Error::V8Exception)? .int32_value(scope) .ok_or(validators::Error::V8Exception)?; // Subtract 1 because the first argument is the aggregate value. argc = step_fn_length - 1; let inverse_fn_length = if let Some(inverse_fn) = &options.inverse { inverse_fn .get(scope, length_key.into()) .ok_or(validators::Error::V8Exception)? .int32_value(scope) .ok_or(validators::Error::V8Exception)? } else { 0 }; argc = cmp::max(argc, cmp::max(inverse_fn_length - 1, 0)); } let mut text_rep = libsqlite3_sys::SQLITE_UTF8; if options.deterministic { text_rep |= libsqlite3_sys::SQLITE_DETERMINISTIC; } if options.direct_only { text_rep |= libsqlite3_sys::SQLITE_DIRECTONLY; } let db = self.conn.borrow(); let db = db.as_ref().ok_or(SqliteError::InUse)?; let context = v8::Global::new(scope, scope.get_current_context()).into_raw(); let start = v8::Global::new(scope, options.start).into_raw(); let step_fn = v8::Global::new(scope, options.step).into_raw(); let inverse_fn = options .inverse .map(|inv| v8::Global::new(scope, inv).into_raw()); let final_fn = options .result .map(|res| v8::Global::new(scope, res).into_raw()); let custom_aggregate = Box::new(CustomAggregate { context, use_big_int_arguments: options.use_big_int_arguments, start, step_fn, inverse_fn, final_fn, ignore_next_sqlite_error: Rc::clone(&self.ignore_next_sqlite_error), callback_depth: Rc::clone(&self.callback_depth), }); let custom_aggregate_ptr = Box::into_raw(custom_aggregate); // SAFETY: lifetime of the connection is guaranteed by reference counting. let raw_handle = unsafe { db.handle() }; let name_cstring = CString::new(name)?; let (value_callback, inverse_callback) = if options.inverse.is_some() { ( Some( custom_aggregate_xvalue as unsafe extern "C" fn(*mut libsqlite3_sys::sqlite3_context), ), Some( custom_aggregate_xinverse as unsafe extern "C" fn( *mut libsqlite3_sys::sqlite3_context, i32, *mut *mut libsqlite3_sys::sqlite3_value, ), ), ) } else { (None, None) }; // SAFETY: `raw_handle` is a valid database handle. // The v8 handles stored in `CustomAggregate` are released in `custom_aggregate_xdestroy`. let r = unsafe { sqlite3_create_window_function( raw_handle, name_cstring.as_ptr(), argc, text_rep, custom_aggregate_ptr as *mut c_void, Some(custom_aggregate_xstep), Some(custom_aggregate_xfinal), value_callback, inverse_callback, Some(custom_aggregate_xdestroy), ) }; check_error_code(r, raw_handle)?; Ok(()) } #[validate(is_open)] #[cppgc] fn create_tag_store( &self, scope: &mut v8::PinScope<'_, '_>, #[varargs] args: Option<&v8::FunctionCallbackArguments>, ) -> Result<SQLTagStore, SqliteError> { let capacity = if let Some(args) = args && args.length() > 0 { let val = args.get(0); if val.is_number() { val.uint32_value(scope).unwrap_or(1000) } else { 1000 } } else { 1000 }; let db_object = args.map(|a| a.this()).ok_or(SqliteError::AlreadyClosed)?; Ok(SQLTagStore::create( self.conn.clone(), self.statements.clone(), capacity, self.options.return_arrays, self.options.use_big_int_arguments, v8::Global::new(scope, db_object), )) } // Serializes the contents of the database into a Uint8Array. The database // can be either an in-memory database or a database opened from a file. // // This method is a wrapper around `sqlite3_serialize()`. #[validate(is_open)] fn serialize<'a>( &self, scope: &mut v8::PinScope<'a, '_>, db_name_value: v8::Local<'a, v8::Value>, ) -> Result<v8::Local<'a, v8::Value>, SqliteError> { let db = self.conn.borrow(); let conn = db.as_ref().ok_or(SqliteError::AlreadyClosed)?; let db_name = if db_name_value.is_undefined() { "main".to_string() } else { let Ok(s) = v8::Local::<v8::String>::try_from(db_name_value) else { return Err(SqliteError::Validation( validators::Error::InvalidArgType( "The \"dbName\" argument must be a string.".into(), ), )); }; s.to_rust_string_lossy(scope) }; let name_cstring = CString::new(db_name)?; // SAFETY: lifetime of the connection is guaranteed by reference counting. let raw_handle = unsafe { conn.handle() }; let mut size: libsqlite3_sys::sqlite3_int64 = 0; // SAFETY: `raw_handle` is a valid sqlite3 pointer; `name_cstring` is a // valid C string for the lifetime of this call; `size` is a valid out // pointer. let data = unsafe { libsqlite3_sys::sqlite3_serialize( raw_handle, name_cstring.as_ptr(), &mut size, 0, ) }; if data.is_null() { return Err(SqliteError::SqliteSysError { message: "unable to serialize database".to_string(), errstr: "unable to serialize database".to_string(), errcode: libsqlite3_sys::SQLITE_ERROR as _, }); } let len = size as usize; // SAFETY: `data` points to `len` bytes owned by SQLite and is valid for // reads of that length. let bytes = unsafe { std::slice::from_raw_parts(data, len) }.to_vec(); // SAFETY: `data` was allocated by `sqlite3_serialize` and must be freed // with `sqlite3_free`. unsafe { libsqlite3_sys::sqlite3_free(data as *mut c_void); } let backing = v8::ArrayBuffer::new_backing_store_from_vec(bytes).make_shared(); let ab = v8::ArrayBuffer::with_backing_store(scope, &backing); let view = v8::Uint8Array::new(scope, ab, 0, len).unwrap(); Ok(view.into()) } // Deserializes the given buffer into the database. Replaces the contents // of the named database (default `"main"`) with the contents of the // serialized buffer. // // This method is a wrapper around `sqlite3_deserialize()`. #[fast] #[validate(is_open)] #[undefined] fn deserialize<'a>( &self, scope: &mut v8::PinScope<'a, '_>, buffer_value: v8::Local<'a, v8::Value>, options_value: v8::Local<'a, v8::Value>, ) -> Result<(), SqliteError> { // Refuse to replace the database while SQLite is executing a // user-defined callback. deserialize() finalizes existing statements, // which would invalidate the currently executing statement. if self.callback_depth.get() > 0 { return Err(SqliteError::ActiveCallback); } if !buffer_value.is_uint8_array() { return Err(SqliteError::Validation(validators::Error::InvalidArgType( "The \"buffer\" argument must be a Uint8Array.".into(), ))); } let view: v8::Local<v8::ArrayBufferView> = buffer_value.try_into().unwrap(); let byte_length = view.byte_length(); if byte_length == 0 { return Err(SqliteError::Validation(validators::Error::InvalidArgValue( "The \"buffer\" argument must not be empty.".into(), ))); } let mut db_name = "main".to_string(); let mut read_only = false; if !options_value.is_undefined() { let Ok(options_obj) = v8::Local::<v8::Object>::try_from(options_value) else { return Err(SqliteError::Validation( validators::Error::InvalidArgType( "The \"options\" argument must be an object.".into(), ), )); }; v8_static_strings! { DB_NAME_STRING = "dbName", READ_ONLY_STRING = "readOnly", } let db_name_string = DB_NAME_STRING.v8_string(scope).unwrap(); if let Some(name_val) = options_obj.get(scope, db_name_string.into()) && !name_val.is_undefined() { let Ok(name_str) = v8::Local::<v8::String>::try_from(name_val) else { return Err(SqliteError::Validation( validators::Error::InvalidArgType( "The \"options.dbName\" argument must be a string.".into(), ), )); }; db_name = name_str.to_rust_string_lossy(scope); } let read_only_string = READ_ONLY_STRING.v8_string(scope).unwrap(); if let Some(read_only_val) = options_obj.get(scope, read_only_string.into()) && !read_only_val.is_undefined() { let Ok(b) = v8::Local::<v8::Boolean>::try_from(read_only_val) else { return Err(SqliteError::Validation( validators::Error::InvalidArgType( "The \"options.readOnly\" argument must be a boolean.".into(), ), )); }; read_only = b.is_true(); } } let name_cstring = CString::new(db_name)?; // Per Node's contract, existing prepared statements are finalized before // deserialization is attempted, even if the operation subsequently fails. for stmt in self.statements.borrow_mut().drain(..) { if let Some(ptr) = stmt.get() { // SAFETY: `ptr` is a valid statement handle. unsafe { libsqlite3_sys::sqlite3_finalize(ptr); } stmt.set(None); } } let db = self.conn.borrow(); let conn = db.as_ref().ok_or(SqliteError::AlreadyClosed)?; // SAFETY: lifetime of the connection is guaranteed by reference counting. let raw_handle = unsafe { conn.handle() }; // Allocate memory owned by SQLite, copy the input bytes in, and hand // ownership over via the FREEONCLOSE flag so SQLite frees it when the // database is closed or another deserialize replaces it. // sqlite3_malloc64(0) is allowed to return null; only treat null as an // error when we actually need a non-empty allocation. // SAFETY: ffi call with no preconditions other than a valid size. let buf = unsafe { libsqlite3_sys::sqlite3_malloc64(byte_length.max(1) as u64) as *mut u8 }; if buf.is_null() { return Err(SqliteError::SqliteSysError { message: "out of memory".to_string(), errstr: "out of memory".to_string(), errcode: libsqlite3_sys::SQLITE_NOMEM as _, }); } if byte_length > 0 { let data = view.data() as *const u8; // SAFETY: `data` points to `byte_length` bytes; `buf` is a freshly // allocated buffer with the same capacity; the two regions do not // overlap. unsafe { std::ptr::copy_nonoverlapping(data, buf, byte_length); } } let mut flags: u32 = libsqlite3_sys::SQLITE_DESERIALIZE_FREEONCLOSE; if read_only { flags |= libsqlite3_sys::SQLITE_DESERIALIZE_READONLY; } else { flags |= libsqlite3_sys::SQLITE_DESERIALIZE_RESIZEABLE; } // SAFETY: `raw_handle` is a valid sqlite3 pointer; `name_cstring` is a // valid C string for the lifetime of this call; `buf` was allocated by // SQLite and ownership transfers via FREEONCLOSE. let r = unsafe { libsqlite3_sys::sqlite3_deserialize( raw_handle, name_cstring.as_ptr(), buf, byte_length as i64, byte_length as i64, flags, ) }; if r != libsqlite3_sys::SQLITE_OK { // sqlite3_deserialize frees `buf` itself on failure when FREEONCLOSE // is set. check_error_code(r, raw_handle)?; check_error_code2(r)?; } Ok(()) } } #[repr(C)] struct AggregateData { value: Option<NonNull<v8::Value>>, is_window: bool, initialized: bool, } struct CustomAggregate { context: NonNull<v8::Context>, use_big_int_arguments: bool, start: NonNull<v8::Value>, step_fn: NonNull<v8::Function>, inverse_fn: Option<NonNull<v8::Function>>, final_fn: Option<NonNull<v8::Function>>, ignore_next_sqlite_error: Rc<Cell<bool>>, callback_depth: Rc<Cell<usize>>, } enum AggregateStepKind { Step, Inverse, } unsafe fn get_aggregate_data( ctx: *mut libsqlite3_sys::sqlite3_context, custom_aggregate: &CustomAggregate, scope: &mut v8::PinScope<'_, '_>, ) -> Option<*mut AggregateData> { // SAFETY: `ctx` is a valid sqlite3_context pointer. unsafe { let agg_ptr = libsqlite3_sys::sqlite3_aggregate_context( ctx, std::mem::size_of::<AggregateData>() as i32, ) as *mut AggregateData; if agg_ptr.is_null() { sqlite_result_error(ctx, "Failed to allocate aggregate context"); return None; } let agg = &mut *agg_ptr; if !agg.initialized { let start_local: v8::Local<v8::Value> = std::mem::transmute(custom_aggregate.start.as_ptr()); let start_value = if start_local.is_function() { let start_fn: v8::Local<v8::Function> = start_local.try_into().unwrap(); let recv = v8::null(scope).into(); match start_fn.call(scope, recv, &[]) { Some(val) => val, None => { custom_aggregate.ignore_next_sqlite_error.set(true); sqlite_result_error(ctx, ""); return None; } } } else { start_local }; let global = v8::Global::new(scope, start_value); agg.value = Some(global.into_raw()); agg.initialized = true; agg.is_window = false; } Some(agg_ptr) } } unsafe fn custom_aggregate_step_base( ctx: *mut libsqlite3_sys::sqlite3_context, argc: i32, argv: *mut *mut libsqlite3_sys::sqlite3_value, kind: AggregateStepKind, ) { // SAFETY: `ctx` is a valid sqlite3_context pointer. unsafe { let data_ptr = libsqlite3_sys::sqlite3_user_data(ctx) as *mut CustomAggregate; if data_ptr.is_null() { sqlite_result_error( ctx, "Internal error: missing custom aggregate context", ); return; } let data = &*data_ptr; // Block close() from freeing the in-flight statement under the VDBE. let _depth_guard = CallbackDepthGuard::new(&data.callback_depth); let context_local: v8::Local<v8::Context> = std::mem::transmute(data.context.as_ptr()); v8::callback_scope!(unsafe cb_scope, context_local); v8::scope!(scope, cb_scope); v8::tc_scope!(tc_scope, scope); let Some(agg_ptr) = get_aggregate_data(ctx, data, tc_scope) else { if tc_scope.has_caught() { data.ignore_next_sqlite_error.set(true); sqlite_result_error(ctx, ""); tc_scope.rethrow(); } return; }; let agg = &mut *agg_ptr; let step_fn = match kind { AggregateStepKind::Step => { let step_fn: v8::Local<v8::Function> = std::mem::transmute(data.step_fn.as_ptr()); step_fn } AggregateStepKind::Inverse => { if let Some(inverse_ptr) = data.inverse_fn { let inverse_fn: v8::Local<v8::Function> = std::mem::transmute(inverse_ptr.as_ptr()); inverse_fn } else { sqlite_result_error(ctx, "Internal error: inverse function not set"); return; } } }; let argc_len = usize::try_from(argc).unwrap_or(0); let args_slice = if argc_len == 0 { &[] } else { std::slice::from_raw_parts(argv, argc_len) }; let current_value = if let Some(value_ptr) = agg.value { let global = v8::Global::from_raw(tc_scope, value_ptr); let local = v8::Local::new(tc_scope, &global); std::mem::forget(global); local } else { v8::undefined(tc_scope).into() }; let mut js_args = Vec::with_capacity(args_slice.len() + 1); js_args.push(current_value); for &value_ptr in args_slice { if let Some(arg) = sqlite_value_to_v8(tc_scope, value_ptr, data.use_big_int_arguments) { js_args.push(arg); } else { data.ignore_next_sqlite_error.set(true); sqlite_result_error(ctx, ""); tc_scope.rethrow(); return; } } let recv = v8::undefined(tc_scope).into(); let result = step_fn .call(tc_scope, recv, &js_args) .unwrap_or_else(|| v8::undefined(tc_scope).into()); if tc_scope.has_caught() { data.ignore_next_sqlite_error.set(true); sqlite_result_error(ctx, ""); tc_scope.rethrow(); return; } if let Some(old_ptr) = agg.value { let _ = v8::Global::from_raw(tc_scope, old_ptr); } let global = v8::Global::new(tc_scope, result); agg.value = Some(global.into_raw()); } } unsafe fn custom_aggregate_value_base( ctx: *mut libsqlite3_sys::sqlite3_context, is_final: bool, ) { // SAFETY: `ctx` is a valid sqlite3_context pointer. unsafe { let data_ptr = libsqlite3_sys::sqlite3_user_data(ctx) as *mut CustomAggregate; if data_ptr.is_null() { sqlite_result_error( ctx, "Internal error: missing custom aggregate context", ); return; } let data = &*data_ptr; // Block close() from freeing the in-flight statement under the VDBE. let _depth_guard = CallbackDepthGuard::new(&data.callback_depth); let context_local: v8::Local<v8::Context> = std::mem::transmute(data.context.as_ptr()); v8::callback_scope!(unsafe cb_scope, context_local); v8::scope!(scope, cb_scope); v8::tc_scope!(tc_scope, scope); let Some(agg_ptr) = get_aggregate_data(ctx, data, tc_scope) else { if tc_scope.has_caught() { data.ignore_next_sqlite_error.set(true); sqlite_result_error(ctx, ""); tc_scope.rethrow(); } return; }; let agg = &mut *agg_ptr; if !is_final { agg.is_window = true; } else if agg.is_window { if let Some(value_ptr) = agg.value.take() { let _ = v8::Global::from_raw(tc_scope, value_ptr); } return; } let current_value = if let Some(value_ptr) = agg.value { let global = v8::Global::from_raw(tc_scope, value_ptr); let local = v8::Local::new(tc_scope, &global); if is_final { agg.value = None; } else { std::mem::forget(global); } local } else { v8::undefined(tc_scope).into() }; let result = if let Some(result_fn_ptr) = data.final_fn { let result_fn: v8::Local<v8::Function> = std::mem::transmute(result_fn_ptr.as_ptr()); let ret = result_fn .call(tc_scope, v8::null(tc_scope).into(), &[current_value]) .unwrap_or_else(|| v8::undefined(tc_scope).into()); if tc_scope.has_caught() { data.ignore_next_sqlite_error.set(true); sqlite_result_error(ctx, ""); tc_scope.rethrow(); return; } ret } else { current_value }; js_value_to_sqlite(tc_scope, ctx, result); if tc_scope.has_caught() { data.ignore_next_sqlite_error.set(true); sqlite_result_error(ctx, ""); tc_scope.rethrow(); } } } unsafe extern "C" fn custom_aggregate_xstep( ctx: *mut libsqlite3_sys::sqlite3_context, argc: i32, argv: *mut *mut libsqlite3_sys::sqlite3_value, ) { // SAFETY: `ctx` is a valid sqlite3_context pointer. unsafe { custom_aggregate_step_base(ctx, argc, argv, AggregateStepKind::Step); } } unsafe extern "C" fn custom_aggregate_xinverse( ctx: *mut libsqlite3_sys::sqlite3_context, argc: i32, argv: *mut *mut libsqlite3_sys::sqlite3_value, ) { // SAFETY: `ctx` is a valid sqlite3_context pointer. unsafe { custom_aggregate_step_base(ctx, argc, argv, AggregateStepKind::Inverse); } } unsafe extern "C" fn custom_aggregate_xfinal( ctx: *mut libsqlite3_sys::sqlite3_context, ) { // SAFETY: `ctx` is a valid sqlite3_context pointer. unsafe { custom_aggregate_value_base(ctx, true); } } unsafe extern "C" fn custom_aggregate_xvalue( ctx: *mut libsqlite3_sys::sqlite3_context, ) { // SAFETY: `ctx` is a valid sqlite3_context pointer. unsafe { custom_aggregate_value_base(ctx, false); } } unsafe extern "C" fn custom_aggregate_xdestroy(data: *mut c_void) { // SAFETY: `data` is a valid pointer to CustomAggregate. // We intentionally do not re-enter V8 here. SQLite may invoke this // destructor while the isolate is already tearing down, so creating a // callback scope can panic. The raw V8 handles are allowed to leak in that // case, matching the custom function destroy path. unsafe { let _ = Box::from_raw(data as *mut CustomAggregate); } } impl DatabaseSync { fn consume_ignore_next_sqlite_error(&self) -> bool { self.ignore_next_sqlite_error.replace(false) } } struct CustomFunctionData { callback: NonNull<v8::Function>, context: NonNull<v8::Context>, use_big_int_arguments: bool, ignore_next_sqlite_error: Rc<Cell<bool>>, callback_depth: Rc<Cell<usize>>, } struct AuthorizerData { callback: NonNull<v8::Function>, context: NonNull<v8::Context>, ignore_next_sqlite_error: Rc<Cell<bool>>, callback_depth: Rc<Cell<usize>>, // authorizer_callback holds shared references across reentrant JS calls. // Mutable callback-lifetime state must use interior mutability so reentrant // release/drop paths never create an aliasing &mut AuthorizerData. active_callbacks: Cell<usize>, free_after_callback: Cell<bool>, } struct AuthorizerCallbackGuard { data_ptr: *mut AuthorizerData, callback_depth: Rc<Cell<usize>>, } impl AuthorizerCallbackGuard { unsafe fn new(data_ptr: *mut AuthorizerData) -> Self { // SAFETY: caller guarantees `data_ptr` points to valid AuthorizerData. let data = unsafe { &*data_ptr }; data .active_callbacks .set(data.active_callbacks.get().saturating_add(1)); data .callback_depth .set(data.callback_depth.get().saturating_add(1)); Self { data_ptr, callback_depth: Rc::clone(&data.callback_depth), } } } impl Drop for AuthorizerCallbackGuard { fn drop(&mut self) { // SAFETY: AuthorizerData cannot be freed while active_callbacks is non-zero. unsafe { let data = &*self.data_ptr; let active_callbacks = data.active_callbacks.get().saturating_sub(1); data.active_callbacks.set(active_callbacks); self .callback_depth .set(self.callback_depth.get().saturating_sub(1)); if active_callbacks == 0 && data.free_after_callback.get() { free_authorizer_data(self.data_ptr); } } } } unsafe extern "C" fn custom_function_handler( ctx: *mut libsqlite3_sys::sqlite3_context, argc: i32, argv: *mut *mut libsqlite3_sys::sqlite3_value, ) { // SAFETY: `ctx` is a valid sqlite3_context pointer. unsafe { let data_ptr = libsqlite3_sys::sqlite3_user_data(ctx) as *mut CustomFunctionData; if data_ptr.is_null() { sqlite_result_error( ctx, "Internal error: missing custom function context", ); return; } let data = &*data_ptr; // Block close() from freeing the in-flight statement under the VDBE. let _depth_guard = CallbackDepthGuard::new(&data.callback_depth); let context_local: v8::Local<v8::Context> = std::mem::transmute(data.context.as_ptr()); v8::callback_scope!(unsafe cb_scope, context_local); v8::scope!(scope, cb_scope); v8::tc_scope!(tc_scope, scope); let function_local: v8::Local<v8::Function> = std::mem::transmute(data.callback.as_ptr()); let argc_len = usize::try_from(argc).unwrap_or(0); let args_slice = if argc_len == 0 { &[] } else { std::slice::from_raw_parts(argv, argc_len) }; let mut js_args = Vec::with_capacity(args_slice.len()); for &value_ptr in args_slice { if let Some(arg) = sqlite_value_to_v8(tc_scope, value_ptr, data.use_big_int_arguments) { js_args.push(arg); } else { data.ignore_next_sqlite_error.set(true); sqlite_result_error(ctx, ""); tc_scope.rethrow(); return; } } let recv = v8::undefined(tc_scope).into(); let result = function_local.call(tc_scope, recv, &js_args); if tc_scope.has_caught() { data.ignore_next_sqlite_error.set(true); sqlite_result_error(ctx, ""); tc_scope.rethrow(); return; } if let Some(value) = result { js_value_to_sqlite(tc_scope, ctx, value); if tc_scope.has_caught() { data.ignore_next_sqlite_error.set(true); sqlite_result_error(ctx, ""); tc_scope.rethrow(); } } } } unsafe extern "C" fn custom_function_destroy(data: *mut c_void) { // SAFETY: `data` is a valid pointer to CustomFunctionData. // Drop the Box to free the raw pointers stored in it. // We intentionally do NOT convert the raw v8::Global handles back // via `Global::from_raw` because this callback can fire during // isolate teardown (e.g. sqlite3_close during GC) when the // isolate annex is already disposed, making scope creation unsafe. // The leaked Global pointers are harmless: the isolate is being // destroyed and will reclaim all V8 heap memory anyway. unsafe { let _ = Box::from_raw(data as *mut CustomFunctionData); } } fn sqlite_value_to_v8<'a>( scope: &mut v8::PinScope<'a, '_>, value: *mut libsqlite3_sys::sqlite3_value, use_big_int_arguments: bool, ) -> Option<v8::Local<'a, v8::Value>> { // SAFETY: `value` is a valid sqlite3_value pointer. unsafe { match libsqlite3_sys::sqlite3_value_type(value) { libsqlite3_sys::SQLITE_INTEGER => { let val = libsqlite3_sys::sqlite3_value_int64(value); if use_big_int_arguments { Some(v8::BigInt::new_from_i64(scope, val).into()) } else if (-MAX_SAFE_JS_INTEGER..=MAX_SAFE_JS_INTEGER).contains(&val) { Some(v8::Number::new(scope, val as f64).into()) } else { let msg = format!( "Value is too large to be represented as a JavaScript number: {}", val ); throw_range_error(scope, &msg); None } } libsqlite3_sys::SQLITE_FLOAT => { let val = libsqlite3_sys::sqlite3_value_double(value); Some(v8::Number::new(scope, val).into()) } libsqlite3_sys::SQLITE_TEXT => { let len = libsqlite3_sys::sqlite3_value_bytes(value) as usize; if len == 0 { let text = v8::String::new_from_utf8(scope, b"", v8::NewStringType::Normal) .unwrap(); Some(text.into()) } else { let ptr = libsqlite3_sys::sqlite3_value_text(value); let slice = std::slice::from_raw_parts(ptr, len); let text = v8::String::new_from_utf8(scope, slice, v8::NewStringType::Normal) .unwrap(); Some(text.into()) } } libsqlite3_sys::SQLITE_BLOB => { let len = libsqlite3_sys::sqlite3_value_bytes(value); if len == 0 { let ab = v8::ArrayBuffer::new(scope, 0); let view = v8::Uint8Array::new(scope, ab, 0, 0).unwrap(); Some(view.into()) } else { let ptr = libsqlite3_sys::sqlite3_value_blob(value) as *const u8; let slice = std::slice::from_raw_parts(ptr, len as usize); let backing = v8::ArrayBuffer::new_backing_store_from_vec(slice.to_vec()) .make_shared(); let ab = v8::ArrayBuffer::with_backing_store(scope, &backing); let view = v8::Uint8Array::new(scope, ab, 0, len as usize).unwrap(); Some(view.into()) } } libsqlite3_sys::SQLITE_NULL => Some(v8::null(scope).into()), _ => Some(v8::undefined(scope).into()), } } } fn js_value_to_sqlite( scope: &mut v8::PinScope<'_, '_>, ctx: *mut libsqlite3_sys::sqlite3_context, value: v8::Local<v8::Value>, ) { // SAFETY: `ctx` is a valid sqlite3_context pointer. unsafe { if value.is_null_or_undefined() { libsqlite3_sys::sqlite3_result_null(ctx); return; } if value.is_number() { let number = value.number_value(scope).unwrap_or(0f64); libsqlite3_sys::sqlite3_result_double(ctx, number); return; } if value.is_string() { let text = value.to_rust_string_lossy(scope); libsqlite3_sys::sqlite3_result_text( ctx, text.as_ptr() as *const _, text.len() as i32, libsqlite3_sys::SQLITE_TRANSIENT(), ); return; } if value.is_array_buffer_view() { let view: v8::Local<v8::ArrayBufferView> = value.try_into().unwrap(); let mut data = view.data(); let mut size = view.byte_length(); if data.is_null() { static EMPTY: [u8; 0] = []; data = EMPTY.as_ptr() as *mut _; size = 0; } libsqlite3_sys::sqlite3_result_blob( ctx, data, size as i32, libsqlite3_sys::SQLITE_TRANSIENT(), ); return; } if value.is_big_int() { let bigint: v8::Local<v8::BigInt> = value.try_into().unwrap(); let (int_value, lossless) = bigint.i64_value(); if !lossless { throw_range_error(scope, "BigInt value is too large for SQLite"); sqlite_result_error(ctx, ""); return; } libsqlite3_sys::sqlite3_result_int64(ctx, int_value); return; } if value.is_promise() { sqlite_result_error( ctx, "Asynchronous user-defined functions are not supported", ); return; } sqlite_result_error( ctx, "Returned JavaScript value cannot be converted to a SQLite value", ); } } fn sqlite_result_error( ctx: *mut libsqlite3_sys::sqlite3_context, message: &str, ) { let msg = CString::new(message).unwrap(); // SAFETY: `ctx` is a valid sqlite3_context pointer. unsafe { libsqlite3_sys::sqlite3_result_error( ctx, msg.as_ptr(), msg.as_bytes().len() as i32, ); } } macro_rules! nullable_cstring_to_v8_value { ($scope:expr, $ptr:expr) => {{ if ($ptr).is_null() { v8::null($scope).into() } else { let cstr = CStr::from_ptr($ptr); v8::String::new_from_utf8( $scope, cstr.to_bytes(), v8::NewStringType::Normal, ) .map(|s| s.into()) .unwrap_or_else(|| v8::null($scope).into()) } }}; } unsafe extern "C" fn authorizer_callback( user_data: *mut c_void, action_code: i32, param1: *const c_char, param2: *const c_char, param3: *const c_char, param4: *const c_char, ) -> i32 { // SAFETY: `user_data` is a valid pointer to AuthorizerData. unsafe { let data_ptr = user_data as *mut AuthorizerData; if data_ptr.is_null() { return libsqlite3_sys::SQLITE_DENY; } let _callback_guard = AuthorizerCallbackGuard::new(data_ptr); let data = &*data_ptr; let context_local: v8::Local<v8::Context> = std::mem::transmute(data.context.as_ptr()); v8::callback_scope!(unsafe cb_scope, context_local); v8::scope!(scope, cb_scope); let result: Option<v8::Local<'_, v8::Value>>; { v8::tc_scope!(tc_scope, scope); let function_local: v8::Local<v8::Function> = std::mem::transmute(data.callback.as_ptr()); let action_code_js = v8::Integer::new(tc_scope, action_code).into(); let param1_js: v8::Local<v8::Value> = nullable_cstring_to_v8_value!(tc_scope, param1); let param2_js: v8::Local<v8::Value> = nullable_cstring_to_v8_value!(tc_scope, param2); let param3_js: v8::Local<v8::Value> = nullable_cstring_to_v8_value!(tc_scope, param3); let param4_js: v8::Local<v8::Value> = nullable_cstring_to_v8_value!(tc_scope, param4); let js_args = [action_code_js, param1_js, param2_js, param3_js, param4_js]; let recv = v8::undefined(tc_scope).into(); result = function_local.call(tc_scope, recv, &js_args); if tc_scope.has_caught() { data.ignore_next_sqlite_error.set(true); tc_scope.rethrow(); return libsqlite3_sys::SQLITE_DENY; } } let Some(value) = result else { data.ignore_next_sqlite_error.set(true); return libsqlite3_sys::SQLITE_DENY; }; if !value.is_int32() { let message = v8::String::new( scope, "Authorizer callback must return an integer authorization code", ) .unwrap(); let err = v8::Exception::type_error(scope, message); data.ignore_next_sqlite_error.set(true); scope.throw_exception(err); return libsqlite3_sys::SQLITE_DENY; } let int_result = value .int32_value(scope) .unwrap_or(libsqlite3_sys::SQLITE_DENY); if int_result != libsqlite3_sys::SQLITE_OK && int_result != libsqlite3_sys::SQLITE_DENY && int_result != libsqlite3_sys::SQLITE_IGNORE { let message = v8::String::new( scope, "Authorizer callback returned a invalid authorization code", ) .unwrap(); let err = v8::Exception::range_error(scope, message); data.ignore_next_sqlite_error.set(true); scope.throw_exception(err); return libsqlite3_sys::SQLITE_DENY; } int_result } } unsafe fn free_authorizer_data(data_ptr: *mut AuthorizerData) { if data_ptr.is_null() { return; } // SAFETY: `data_ptr` is a valid pointer to AuthorizerData. // The v8 handles are properly dropped here. unsafe { let data = Box::from_raw(data_ptr); let context_local: v8::Local<v8::Context> = std::mem::transmute(data.context.as_ptr()); v8::callback_scope!(unsafe cb_scope, context_local); v8::scope!(scope, cb_scope); let _ = v8::Global::from_raw(scope, data.callback); let _ = v8::Global::from_raw(scope, data.context); } } unsafe fn release_authorizer_data(data_ptr: *mut AuthorizerData) { if data_ptr.is_null() { return; } // SAFETY: `data_ptr` is a valid pointer to AuthorizerData. unsafe { let data = &*data_ptr; if data.active_callbacks.get() > 0 { data.free_after_callback.set(true); } else { free_authorizer_data(data_ptr); } } } fn throw_range_error(scope: &mut v8::PinScope<'_, '_>, message: &str) { let msg = v8::String::new(scope, message).unwrap(); let error = v8::Exception::range_error(scope, msg); v8_static_strings!(CODE = "code", ERR_OUT_OF_RANGE = "ERR_OUT_OF_RANGE"); let code_key = CODE.v8_string(scope).unwrap(); let code_value = ERR_OUT_OF_RANGE.v8_string(scope).unwrap(); let error_obj: v8::Local<v8::Object> = error.try_into().unwrap(); error_obj .create_data_property(scope, code_key.into(), code_value.into()) .unwrap(); scope.throw_exception(error); } fn throw_type_error_with_code( scope: &mut v8::PinScope<'_, '_>, message: &str, code: &str, ) { let msg = v8::String::new(scope, message).unwrap(); let error = v8::Exception::type_error(scope, msg); v8_static_strings!(CODE = "code"); let code_key = CODE.v8_string(scope).unwrap(); let code_value = v8::String::new(scope, code).unwrap(); let error_obj: v8::Local<v8::Object> = error.try_into().unwrap(); error_obj .create_data_property(scope, code_key.into(), code_value.into()) .unwrap(); scope.throw_exception(error); } // Throws a plain `Error` carrying `code`. Used for conditions that are not // `TypeError`s or `RangeError`s, such as the ATTACH_DATABASE access boundary. fn throw_error_with_code( scope: &mut v8::PinScope<'_, '_>, message: &str, code: &str, ) { let msg = v8::String::new(scope, message).unwrap(); let error = v8::Exception::error(scope, msg); v8_static_strings!(CODE = "code"); let code_key = CODE.v8_string(scope).unwrap(); let code_value = v8::String::new(scope, code).unwrap(); let error_obj: v8::Local<v8::Object> = error.try_into().unwrap(); error_obj .create_data_property(scope, code_key.into(), code_value.into()) .unwrap(); scope.throw_exception(error); } /// Object representing SQLite database limits. /// This is returned by DatabaseSync.limits getter. pub struct DatabaseSyncLimits { conn: Rc<RefCell<Option<rusqlite::Connection>>>, // When true, ATTACH DATABASE is held disabled for this connection because the // process lacks full permissions for the database path. The `attach` setter // refuses to raise the cap above 0 in that state. disable_attach: bool, } // SAFETY: we're sure this can be GCed unsafe impl GarbageCollected for DatabaseSyncLimits { fn trace(&self, _visitor: &mut deno_core::v8::cppgc::Visitor) {} fn get_name(&self) -> &'static std::ffi::CStr { c"DatabaseSyncLimits" } } impl DatabaseSyncLimits { fn create( conn: Rc<RefCell<Option<rusqlite::Connection>>>, disable_attach: bool, ) -> Self { Self { conn, disable_attach, } } fn get_limit(&self, limit: Limit) -> Result<i32, SqliteError> { let conn = self.conn.borrow(); let conn = conn.as_ref().ok_or(SqliteError::AlreadyClosed)?; conn.limit(limit).map_err(SqliteError::from) } // Uses manual v8 exception throwing instead of returning Err because // returning Err from a cppgc setter would produce a double-throw. fn set_limit_value( &self, scope: &mut v8::PinScope<'_, '_>, limit: Limit, value: v8::Local<v8::Value>, ) -> Result<(), SqliteError> { let conn = self.conn.borrow(); let conn = conn.as_ref().ok_or(SqliteError::AlreadyClosed)?; let new_value = match coerce_limit_value(scope, value) { Ok(v) => v, Err( LimitCoercionError::NotANumber | LimitCoercionError::NotAnInteger, ) => { throw_type_error_with_code( scope, "Limit value must be a non-negative integer or Infinity.", "ERR_INVALID_ARG_TYPE", ); return Ok(()); } Err(LimitCoercionError::Negative) => { throw_range_error(scope, "Limit value must be non-negative."); return Ok(()); } }; conn.set_limit(limit, new_value)?; Ok(()) } } #[op2] impl DatabaseSyncLimits { #[constructor] #[cppgc] fn new(_: bool) -> Result<DatabaseSyncLimits, SqliteError> { Err(SqliteError::InvalidConstructor) } #[getter] fn length(&self) -> Result<i32, SqliteError> { self.get_limit(Limit::SQLITE_LIMIT_LENGTH) } #[setter] fn length( &self, scope: &mut v8::PinScope<'_, '_>, value: v8::Local<v8::Value>, ) -> Result<(), SqliteError> { self.set_limit_value(scope, Limit::SQLITE_LIMIT_LENGTH, value) } #[getter] #[rename("sqlLength")] fn sql_length(&self) -> Result<i32, SqliteError> { self.get_limit(Limit::SQLITE_LIMIT_SQL_LENGTH) } #[rename("sqlLength")] #[setter] fn sql_length( &self, scope: &mut v8::PinScope<'_, '_>, value: v8::Local<v8::Value>, ) -> Result<(), SqliteError> { self.set_limit_value(scope, Limit::SQLITE_LIMIT_SQL_LENGTH, value) } #[getter] fn column(&self) -> Result<i32, SqliteError> { self.get_limit(Limit::SQLITE_LIMIT_COLUMN) } #[setter] fn column( &self, scope: &mut v8::PinScope<'_, '_>, value: v8::Local<v8::Value>, ) -> Result<(), SqliteError> { self.set_limit_value(scope, Limit::SQLITE_LIMIT_COLUMN, value) } #[getter] #[rename("exprDepth")] fn expr_depth(&self) -> Result<i32, SqliteError> { self.get_limit(Limit::SQLITE_LIMIT_EXPR_DEPTH) } #[rename("exprDepth")] #[setter] fn expr_depth( &self, scope: &mut v8::PinScope<'_, '_>, value: v8::Local<v8::Value>, ) -> Result<(), SqliteError> { self.set_limit_value(scope, Limit::SQLITE_LIMIT_EXPR_DEPTH, value) } #[getter] #[rename("compoundSelect")] fn compound_select(&self) -> Result<i32, SqliteError> { self.get_limit(Limit::SQLITE_LIMIT_COMPOUND_SELECT) } #[rename("compoundSelect")] #[setter] fn compound_select( &self, scope: &mut v8::PinScope<'_, '_>, value: v8::Local<v8::Value>, ) -> Result<(), SqliteError> { self.set_limit_value(scope, Limit::SQLITE_LIMIT_COMPOUND_SELECT, value) } #[getter] #[rename("vdbeOp")] fn vdbe_op(&self) -> Result<i32, SqliteError> { self.get_limit(Limit::SQLITE_LIMIT_VDBE_OP) } #[rename("vdbeOp")] #[setter] fn vdbe_op( &self, scope: &mut v8::PinScope<'_, '_>, value: v8::Local<v8::Value>, ) -> Result<(), SqliteError> { self.set_limit_value(scope, Limit::SQLITE_LIMIT_VDBE_OP, value) } #[getter] #[rename("functionArg")] fn function_arg(&self) -> Result<i32, SqliteError> { self.get_limit(Limit::SQLITE_LIMIT_FUNCTION_ARG) } #[rename("functionArg")] #[setter] fn function_arg( &self, scope: &mut v8::PinScope<'_, '_>, value: v8::Local<v8::Value>, ) -> Result<(), SqliteError> { self.set_limit_value(scope, Limit::SQLITE_LIMIT_FUNCTION_ARG, value) } #[getter] fn attach(&self) -> Result<i32, SqliteError> { self.get_limit(Limit::SQLITE_LIMIT_ATTACHED) } #[setter] fn attach( &self, scope: &mut v8::PinScope<'_, '_>, value: v8::Local<v8::Value>, ) -> Result<(), SqliteError> { // ATTACH DATABASE is held disabled for processes without full permissions // for the database path. Refuse to raise the cap above 0 so the limit // setter cannot re-enable attach and bypass that boundary. A value of 0 // (keeping it disabled) is allowed and falls through to the normal setter. // Malformed values also fall through so set_limit_value reports the right // ERR_INVALID_ARG_* error. if self.disable_attach && matches!(coerce_limit_value(scope, value), Ok(v) if v > 0) { throw_error_with_code( scope, "Cannot raise the \"attach\" limit: ATTACH DATABASE is disabled \ without full permissions for the database path.", "ERR_ACCESS_DENIED", ); return Ok(()); } self.set_limit_value(scope, Limit::SQLITE_LIMIT_ATTACHED, value) } #[getter] #[rename("likePatternLength")] fn like_pattern_length(&self) -> Result<i32, SqliteError> { self.get_limit(Limit::SQLITE_LIMIT_LIKE_PATTERN_LENGTH) } #[rename("likePatternLength")] #[setter] fn like_pattern_length( &self, scope: &mut v8::PinScope<'_, '_>, value: v8::Local<v8::Value>, ) -> Result<(), SqliteError> { self.set_limit_value(scope, Limit::SQLITE_LIMIT_LIKE_PATTERN_LENGTH, value) } #[getter] #[rename("variableNumber")] fn variable_number(&self) -> Result<i32, SqliteError> { self.get_limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER) } #[rename("variableNumber")] #[setter] fn variable_number( &self, scope: &mut v8::PinScope<'_, '_>, value: v8::Local<v8::Value>, ) -> Result<(), SqliteError> { self.set_limit_value(scope, Limit::SQLITE_LIMIT_VARIABLE_NUMBER, value) } #[getter] #[rename("triggerDepth")] fn trigger_depth(&self) -> Result<i32, SqliteError> { self.get_limit(Limit::SQLITE_LIMIT_TRIGGER_DEPTH) } #[rename("triggerDepth")] #[setter] fn trigger_depth( &self, scope: &mut v8::PinScope<'_, '_>, value: v8::Local<v8::Value>, ) -> Result<(), SqliteError> { self.set_limit_value(scope, Limit::SQLITE_LIMIT_TRIGGER_DEPTH, value) } }