diff --git a/Cargo.lock b/Cargo.lock index c0ef3b0ca0..79906bb607 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3460,7 +3460,6 @@ dependencies = [ "memchr", "native-tls", "percent-encoding", - "regex", "rust_decimal", "rustls", "rustls-native-certs", diff --git a/Cargo.toml b/Cargo.toml index ed4cae93ca..6d08df23d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,8 +54,9 @@ authors.workspace = true repository.workspace = true rust-version.workspace = true +# Note: written so that it may be copy-pasted to other crates [package.metadata.docs.rs] -features = ["all-databases", "_unstable-all-types", "_unstable-doc", "sqlite-preupdate-hook"] +features = ["_unstable-docs"] rustdoc-args = ["--cfg", "docsrs"] [features] @@ -83,8 +84,13 @@ _unstable-all-types = [ "bit-vec", "bstr" ] + # Render documentation that wouldn't otherwise be shown (e.g. `sqlx_core::config`). -_unstable-doc = [] +_unstable-docs = [ + "all-databases", + "_unstable-all-types", + "sqlx-sqlite/_unstable-docs" +] # Base runtime features without TLS runtime-async-std = ["_rt-async-std", "sqlx-core/_rt-async-std", "sqlx-macros?/_rt-async-std"] @@ -110,10 +116,32 @@ _sqlite = [] any = ["sqlx-core/any", "sqlx-mysql?/any", "sqlx-postgres?/any", "sqlx-sqlite?/any"] postgres = ["sqlx-postgres", "sqlx-macros?/postgres"] mysql = ["sqlx-mysql", "sqlx-macros?/mysql"] -sqlite = ["_sqlite", "sqlx-sqlite/bundled", "sqlx-macros?/sqlite"] +sqlite = ["sqlite-bundled", "sqlite-deserialize", "sqlite-load-extension", "sqlite-unlock-notify"] + +# SQLite base features +sqlite-bundled = ["_sqlite", "sqlx-sqlite/bundled", "sqlx-macros?/sqlite"] sqlite-unbundled = ["_sqlite", "sqlx-sqlite/unbundled", "sqlx-macros?/sqlite-unbundled"] + +# SQLite features using conditionally compiled APIs +# Note: these assume `sqlite-bundled` or `sqlite-unbundled` is also enabled +# +# Enable `SqliteConnection::deserialize()` and `::serialize()` +# Cannot be used with `-DSQLITE_OMIT_DESERIALIZE`; requires `-DSQLITE_ENABLE_DESERIALIZE` on SQLite < 3.36.0 +sqlite-deserialize = ["sqlx-sqlite/deserialize"] + +# Enable `SqliteConnectOptions::extension()` and `::extension_with_entrypoint()`. +# Also required to use `drivers.sqlite.unsafe-load-extensions` from `sqlx.toml`. +# Cannot be used with `-DSQLITE_OMIT_LOAD_EXTENSION` +sqlite-load-extension = ["sqlx-sqlite/load-extension", "sqlx-macros?/sqlite-load-extension"] + +# Enables `sqlite3_preupdate_hook` +# Requires `-DSQLITE_ENABLE_PREUPDATE_HOOK` (set automatically with `sqlite-bundled`) sqlite-preupdate-hook = ["sqlx-sqlite/preupdate-hook"] +# Enable internal handling of `SQLITE_LOCKED_SHAREDCACHE` +# Requires `-DSQLITE_ENABLE_UNLOCK_NOTIFY` (set automatically with `sqlite-bundled`) +sqlite-unlock-notify = ["sqlx-sqlite/unlock-notify"] + # types json = ["sqlx-core/json", "sqlx-macros?/json", "sqlx-mysql?/json", "sqlx-postgres?/json", "sqlx-sqlite?/json"] @@ -211,8 +239,14 @@ cast_sign_loss = 'deny' # See `clippy.toml` disallowed_methods = 'deny' -[lints.rust] -unexpected_cfgs = { level = 'warn', check-cfg = ['cfg(mariadb, values(any()))'] } + +[lints.rust.unexpected_cfgs] +level = 'warn' +check-cfg = [ + 'cfg(mariadb, values(any()))', + 'cfg(sqlite_ipaddr)', + 'cfg(sqlite_test_sqlcipher)', +] # # Any diff --git a/examples/sqlite/extension/sqlx.toml b/examples/sqlite/extension/sqlx.toml index 77f844642f..7c67dd160e 100644 --- a/examples/sqlite/extension/sqlx.toml +++ b/examples/sqlite/extension/sqlx.toml @@ -2,11 +2,11 @@ # Including the full path to the extension is somewhat unusual, # because normally an extension will be installed in a standard # directory which is part of the library search path. If that were the -# case here, the load-extensions value could just be `["ipaddr"]` +# case here, the unsafe-load-extensions value could just be `["ipaddr"]` # # When the extension file is installed in a non-standard location, as # in this example, there are two options: # * Provide the full path the the extension, as seen below. # * Add the non-standard location to the library search path, which on # Linux means adding it to the LD_LIBRARY_PATH environment variable. -load-extensions = ["/tmp/sqlite3-lib/ipaddr"] \ No newline at end of file +unsafe-load-extensions = ["/tmp/sqlite3-lib/ipaddr"] diff --git a/sqlx-cli/src/lib.rs b/sqlx-cli/src/lib.rs index deddddb9d3..e3f21c9863 100644 --- a/sqlx-cli/src/lib.rs +++ b/sqlx-cli/src/lib.rs @@ -177,7 +177,7 @@ async fn do_run(opt: Opt) -> anyhow::Result<()> { } => { let config = config.load_config().await?; connect_opts.populate_db_url(&config)?; - prepare::run(check, all, workspace, connect_opts, args).await? + prepare::run(&config, check, all, workspace, connect_opts, args).await? } #[cfg(feature = "completions")] @@ -188,27 +188,9 @@ async fn do_run(opt: Opt) -> anyhow::Result<()> { } /// Attempt to connect to the database server, retrying up to `ops.connect_timeout`. -async fn connect(opts: &ConnectOpts) -> anyhow::Result { +async fn connect(config: &Config, opts: &ConnectOpts) -> anyhow::Result { retry_connect_errors(opts, move |url| { - // This only handles the default case. For good support of - // the new command line options, we need to work out some - // way to make the appropriate ConfigOpt available here. I - // suspect that that infrastructure would be useful for - // other things in the future, as well, but it also seems - // like an extensive and intrusive change. - // - // On the other hand, the compile-time checking macros - // can't be configured to use a different config file at - // all, so I believe this is okay for the time being. - let config = Some(std::path::PathBuf::from("sqlx.toml")).and_then(|p| { - if p.exists() { - Some(p) - } else { - None - } - }); - - async move { AnyConnection::connect_with_config(url, config.clone()).await } + AnyConnection::connect_with_driver_config(url, &config.drivers) }) .await } diff --git a/sqlx-cli/src/migrate.rs b/sqlx-cli/src/migrate.rs index 6d32c9e846..926e264032 100644 --- a/sqlx-cli/src/migrate.rs +++ b/sqlx-cli/src/migrate.rs @@ -130,7 +130,7 @@ pub async fn info( ) -> anyhow::Result<()> { let migrator = migration_source.resolve(config).await?; - let mut conn = crate::connect(connect_opts).await?; + let mut conn = crate::connect(config, connect_opts).await?; // FIXME: we shouldn't actually be creating anything here for schema_name in &config.migrate.create_schemas { @@ -229,7 +229,7 @@ pub async fn run( } } - let mut conn = crate::connect(connect_opts).await?; + let mut conn = crate::connect(config, connect_opts).await?; for schema_name in &config.migrate.create_schemas { conn.create_schema_if_not_exists(schema_name).await?; @@ -331,7 +331,7 @@ pub async fn revert( } } - let mut conn = crate::connect(connect_opts).await?; + let mut conn = crate::connect(config, connect_opts).await?; // FIXME: we should not be creating anything here if it doesn't exist for schema_name in &config.migrate.create_schemas { diff --git a/sqlx-cli/src/prepare.rs b/sqlx-cli/src/prepare.rs index b57246c326..9f3fc67da4 100644 --- a/sqlx-cli/src/prepare.rs +++ b/sqlx-cli/src/prepare.rs @@ -5,14 +5,15 @@ use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; +use crate::metadata::{manifest_dir, Metadata}; +use crate::opt::ConnectOpts; +use crate::Config; use anyhow::{bail, Context}; use console::style; use sqlx::Connection; -use crate::metadata::{manifest_dir, Metadata}; -use crate::opt::ConnectOpts; - -pub struct PrepareCtx { +pub struct PrepareCtx<'a> { + pub config: &'a Config, pub workspace: bool, pub all: bool, pub cargo: OsString, @@ -21,7 +22,7 @@ pub struct PrepareCtx { pub connect_opts: ConnectOpts, } -impl PrepareCtx { +impl PrepareCtx<'_> { /// Path to the directory where cached queries should be placed. fn prepare_dir(&self) -> anyhow::Result { if self.workspace { @@ -33,6 +34,7 @@ impl PrepareCtx { } pub async fn run( + config: &Config, check: bool, all: bool, workspace: bool, @@ -50,6 +52,7 @@ hint: This command only works in the manifest directory of a Cargo package or wo let metadata: Metadata = Metadata::from_current_directory(&cargo)?; let ctx = PrepareCtx { + config, workspace, all, cargo, @@ -65,9 +68,9 @@ hint: This command only works in the manifest directory of a Cargo package or wo } } -async fn prepare(ctx: &PrepareCtx) -> anyhow::Result<()> { +async fn prepare(ctx: &PrepareCtx<'_>) -> anyhow::Result<()> { if ctx.connect_opts.database_url.is_some() { - check_backend(&ctx.connect_opts).await?; + check_backend(ctx.config, &ctx.connect_opts).await?; } let prepare_dir = ctx.prepare_dir()?; @@ -93,9 +96,9 @@ async fn prepare(ctx: &PrepareCtx) -> anyhow::Result<()> { Ok(()) } -async fn prepare_check(ctx: &PrepareCtx) -> anyhow::Result<()> { +async fn prepare_check(ctx: &PrepareCtx<'_>) -> anyhow::Result<()> { if ctx.connect_opts.database_url.is_some() { - check_backend(&ctx.connect_opts).await?; + check_backend(ctx.config, &ctx.connect_opts).await?; } // Re-generate and store the queries in a separate directory from both the prepared @@ -359,8 +362,8 @@ fn load_json_file(path: impl AsRef) -> anyhow::Result { Ok(serde_json::from_slice(&file_bytes)?) } -async fn check_backend(opts: &ConnectOpts) -> anyhow::Result<()> { - crate::connect(opts).await?.close().await?; +async fn check_backend(config: &Config, opts: &ConnectOpts) -> anyhow::Result<()> { + crate::connect(config, opts).await?.close().await?; Ok(()) } diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml index 10d4caffb5..37cf9d3b91 100644 --- a/sqlx-core/Cargo.toml +++ b/sqlx-core/Cargo.toml @@ -76,7 +76,6 @@ futures-util = { version = "0.3.19", default-features = false, features = ["allo log = { version = "0.4.18", default-features = false } memchr = { version = "2.4.1", default-features = false } percent-encoding = "2.1.0" -regex = { version = "1.5.5", optional = true } serde = { version = "1.0.132", features = ["derive", "rc"], optional = true } serde_json = { version = "1.0.73", features = ["raw_value"], optional = true } toml = { version = "0.8.16", optional = true } diff --git a/sqlx-core/src/any/connection/mod.rs b/sqlx-core/src/any/connection/mod.rs index 246877225c..894b109ccd 100644 --- a/sqlx-core/src/any/connection/mod.rs +++ b/sqlx-core/src/any/connection/mod.rs @@ -5,11 +5,11 @@ use crate::any::{Any, AnyConnectOptions}; use crate::connection::{ConnectOptions, Connection}; use crate::error::Error; +use crate::config; use crate::database::Database; use crate::sql_str::SqlSafeStr; -pub use backend::AnyConnectionBackend; - use crate::transaction::Transaction; +pub use backend::AnyConnectionBackend; mod backend; mod executor; @@ -37,7 +37,7 @@ impl AnyConnection { pub(crate) fn connect(options: &AnyConnectOptions) -> BoxFuture<'_, crate::Result> { Box::pin(async { let driver = crate::any::driver::from_url(&options.database_url)?; - (driver.connect)(options).await + (*driver.connect)(options, None).await }) } @@ -45,32 +45,37 @@ impl AnyConnection { /// /// Connect to the database, and instruct the nested driver to /// read options from the sqlx.toml file as appropriate. - #[cfg(feature = "sqlx-toml")] #[doc(hidden)] - pub fn connect_with_config( + pub async fn connect_with_driver_config( url: &str, - path: Option, - ) -> BoxFuture<'static, Result> + driver_config: &config::drivers::Config, + ) -> Result where Self: Sized, { - let options: Result = url.parse(); + let options: AnyConnectOptions = url.parse()?; - Box::pin(async move { Self::connect_with(&options?.with_config_file(path)).await }) + let driver = crate::any::driver::from_url(&options.database_url)?; + (*driver.connect)(&options, Some(driver_config)).await } - pub(crate) fn connect_with_db( - options: &AnyConnectOptions, - ) -> BoxFuture<'_, crate::Result> + pub(crate) fn connect_with_db<'a, DB: Database>( + options: &'a AnyConnectOptions, + driver_config: Option<&'a config::drivers::Config>, + ) -> BoxFuture<'a, crate::Result> where DB::Connection: AnyConnectionBackend, ::Options: - for<'a> TryFrom<&'a AnyConnectOptions, Error = Error>, + for<'b> TryFrom<&'b AnyConnectOptions, Error = Error>, { let res = TryFrom::try_from(options); - Box::pin(async { - let options: ::Options = res?; + Box::pin(async move { + let mut options: ::Options = res?; + + if let Some(config) = driver_config { + options = options.__unstable_apply_driver_config(config)?; + } Ok(AnyConnection { backend: Box::new(options.connect().await?), diff --git a/sqlx-core/src/any/driver.rs b/sqlx-core/src/any/driver.rs index 9985286a2a..58294faecc 100644 --- a/sqlx-core/src/any/driver.rs +++ b/sqlx-core/src/any/driver.rs @@ -3,9 +3,8 @@ use crate::any::{AnyConnectOptions, AnyConnection}; use crate::common::DebugFn; use crate::connection::Connection; use crate::database::Database; -use crate::Error; +use crate::{config, Error}; use futures_core::future::BoxFuture; -use futures_util::FutureExt; use std::fmt::{Debug, Formatter}; use std::sync::OnceLock; use url::Url; @@ -29,8 +28,12 @@ macro_rules! declare_driver_with_optional_migrate { pub struct AnyDriver { pub(crate) name: &'static str, pub(crate) url_schemes: &'static [&'static str], - pub(crate) connect: - DebugFn BoxFuture<'_, crate::Result>>, + pub(crate) connect: DebugFn< + for<'a> fn( + &'a AnyConnectOptions, + Option<&'a config::drivers::Config>, + ) -> BoxFuture<'a, crate::Result>, + >, pub(crate) migrate_database: Option, } @@ -68,10 +71,10 @@ impl AnyDriver { { Self { migrate_database: Some(AnyMigrateDatabase { - create_database: DebugFn(|url| DB::create_database(url).boxed()), - database_exists: DebugFn(|url| DB::database_exists(url).boxed()), - drop_database: DebugFn(|url| DB::drop_database(url).boxed()), - force_drop_database: DebugFn(|url| DB::force_drop_database(url).boxed()), + create_database: DebugFn(|url| Box::pin(DB::create_database(url))), + database_exists: DebugFn(|url| Box::pin(DB::database_exists(url))), + drop_database: DebugFn(|url| Box::pin(DB::drop_database(url))), + force_drop_database: DebugFn(|url| Box::pin(DB::force_drop_database(url))), }), ..Self::without_migrate::() } @@ -131,6 +134,7 @@ pub fn install_drivers( .map_err(|_| "drivers already installed".into()) } +#[cfg(feature = "migrate")] pub(crate) fn from_url_str(url: &str) -> crate::Result<&'static AnyDriver> { from_url(&url.parse().map_err(Error::config)?) } diff --git a/sqlx-core/src/any/options.rs b/sqlx-core/src/any/options.rs index 2d65515943..724060febd 100644 --- a/sqlx-core/src/any/options.rs +++ b/sqlx-core/src/any/options.rs @@ -19,7 +19,6 @@ use url::Url; pub struct AnyConnectOptions { pub database_url: Url, pub log_settings: LogSettings, - pub enable_config: Option, } impl FromStr for AnyConnectOptions { type Err = Error; @@ -30,7 +29,6 @@ impl FromStr for AnyConnectOptions { .parse::() .map_err(|e| Error::Configuration(e.into()))?, log_settings: LogSettings::default(), - enable_config: None, }) } } @@ -42,7 +40,6 @@ impl ConnectOptions for AnyConnectOptions { Ok(AnyConnectOptions { database_url: url.clone(), log_settings: LogSettings::default(), - enable_config: None, }) } @@ -67,14 +64,4 @@ impl ConnectOptions for AnyConnectOptions { } } -impl AnyConnectOptions { - /// UNSTABLE: for use with `sqlx-cli` - /// - /// Allow nested drivers to extract configuration information from - /// the sqlx.toml file. - #[doc(hidden)] - pub fn with_config_file(mut self, path: Option>) -> Self { - self.enable_config = path.map(|p| p.into()); - self - } -} +impl AnyConnectOptions {} diff --git a/sqlx-core/src/config/common.rs b/sqlx-core/src/config/common.rs index e1809d6d2b..2d5342d5b8 100644 --- a/sqlx-core/src/config/common.rs +++ b/sqlx-core/src/config/common.rs @@ -40,14 +40,6 @@ pub struct Config { /// The query macros used in `foo` will use `FOO_DATABASE_URL`, /// and the ones used in `bar` will use `BAR_DATABASE_URL`. pub database_url_var: Option, - - /// Settings for specific database drivers. - /// - /// These settings apply when checking queries, or when applying - /// migrations via `sqlx-cli`. These settings *do not* apply when - /// applying migrations via the macro, as that uses the run-time - /// database connection configured by the application. - pub drivers: Drivers, } impl Config { @@ -55,34 +47,3 @@ impl Config { self.database_url_var.as_deref().unwrap_or("DATABASE_URL") } } - -/// Configuration for specific database drivers. -#[derive(Debug, Default)] -#[cfg_attr( - feature = "sqlx-toml", - derive(serde::Deserialize), - serde(default, rename_all = "kebab-case", deny_unknown_fields) -)] -pub struct Drivers { - /// Specify options for the SQLite driver. - pub sqlite: SQLite, -} - -/// Configuration for the SQLite database driver. -#[derive(Debug, Default)] -#[cfg_attr( - feature = "sqlx-toml", - derive(serde::Deserialize), - serde(default, rename_all = "kebab-case", deny_unknown_fields) -)] -pub struct SQLite { - /// Specify extensions to load. - /// - /// # Example: Load the "uuid" and "vsv" extensions - /// `sqlx.toml`: - /// ```toml - /// [common.drivers.sqlite] - /// load-extensions = ["uuid", "vsv"] - /// ``` - pub load_extensions: Vec, -} diff --git a/sqlx-core/src/config/drivers.rs b/sqlx-core/src/config/drivers.rs new file mode 100644 index 0000000000..5019f1f9f6 --- /dev/null +++ b/sqlx-core/src/config/drivers.rs @@ -0,0 +1,140 @@ +use std::error::Error; + +/// Configuration for specific database drivers (**applies to macros and `sqlx-cli` only**). +/// +/// # Note: Does Not Apply at Application Run-Time +/// As of writing, these configuration parameters do *not* have any bearing on +/// the runtime configuration of SQLx database drivers. +/// +/// Any parameters which overlap with runtime configuration +/// (e.g. [`drivers.sqlite.unsafe-load-extensions`][SqliteConfig::unsafe_load_extensions]) +/// _must_ be configured their normal ways at runtime (e.g. `SqliteConnectOptions::extension()`). +/// +/// See the documentation of individual fields for details. +#[derive(Debug, Default)] +#[cfg_attr( + feature = "sqlx-toml", + derive(serde::Deserialize), + serde(default, rename_all = "kebab-case", deny_unknown_fields) +)] +pub struct Config { + /// Configuration for the MySQL database driver. + /// + /// See [`MySqlConfig`] for details. + pub mysql: MySqlConfig, + + /// Configuration for the Postgres database driver. + /// + /// See [`PgConfig`] for details. + pub postgres: PgConfig, + + /// Configuration for the SQLite database driver. + /// + /// See [`SqliteConfig`] for details. + pub sqlite: SqliteConfig, + + /// Configuration for external database drivers. + /// + /// See [`ExternalDriverConfig`] for details. + pub external: ExternalDriverConfig, +} + +/// Configuration for the MySQL database driver. +#[derive(Debug, Default)] +#[cfg_attr( + feature = "sqlx-toml", + derive(serde::Deserialize), + serde(default, rename_all = "kebab-case", deny_unknown_fields) +)] +pub struct MySqlConfig { + // No fields implemented yet. This key is only used to validate parsing. +} + +/// Configuration for the Postgres database driver. +#[derive(Debug, Default)] +#[cfg_attr( + feature = "sqlx-toml", + derive(serde::Deserialize), + serde(default, rename_all = "kebab-case", deny_unknown_fields) +)] +pub struct PgConfig { + // No fields implemented yet. This key is only used to validate parsing. +} + +/// Configuration for the SQLite database driver. +#[derive(Debug, Default)] +#[cfg_attr( + feature = "sqlx-toml", + derive(serde::Deserialize), + serde(default, rename_all = "kebab-case", deny_unknown_fields) +)] +pub struct SqliteConfig { + /// Specify extensions to load, either by name or by path. + /// + /// Paths should be relative to the workspace root. + /// + /// See [Loading an Extension](https://www.sqlite.org/loadext.html#loading_an_extension) + /// in the SQLite manual for details. + /// + /// The `sqlite-load-extension` feature must be enabled and SQLite must be built + /// _without_ [`SQLITE_OMIT_LOAD_EXTENSION`] enabled. + /// + /// [`SQLITE_OMIT_LOAD_EXTENSION`]: https://www.sqlite.org/compile.html#omit_load_extension + /// + /// # Note: Does Not Configure Runtime Extension Loading + /// Extensions to be loaded at runtime *must* be separately configured with + /// `SqliteConnectOptions::extension()` or `SqliteConnectOptions::extension_with_entrypoint()`. + /// + /// # Safety + /// This causes arbitrary DLLs on the filesystem to be loaded at execution time, + /// which can easily result in undefined behavior, memory corruption, + /// or exploitable vulnerabilities if misused. + /// + /// It is not possible to provide a truly safe version of this API. + /// + /// Use this field with care, and only load extensions that you trust. + /// + /// # Example + /// Load the `uuid` and `vsv` extensions from [`sqlean`](https://github.com/nalgeon/sqlean). + /// + /// `sqlx.toml`: + /// ```toml + /// [common.drivers.sqlite] + /// unsafe-load-extensions = ["uuid", "vsv"] + /// ``` + pub unsafe_load_extensions: Vec, +} + +/// Configuration for external database drivers. +#[derive(Debug, Default)] +#[cfg_attr(feature = "sqlx-toml", derive(serde::Deserialize), serde(transparent))] +pub struct ExternalDriverConfig { + #[cfg(feature = "sqlx-toml")] + by_name: std::collections::BTreeMap, +} + +/// Type-erased [`toml::de::Error`]. +pub type TryParseError = Box; + +impl ExternalDriverConfig { + /// Try to parse the config for a given driver name, returning `Ok(None)` if it does not exist. + #[cfg(feature = "sqlx-toml")] + pub fn try_parse( + &self, + name: &str, + ) -> Result, TryParseError> { + let Some(config) = self.by_name.get(name) else { + return Ok(None); + }; + + // What's really baffling is that `toml` doesn't provide any way to deserialize + // from a `&Table` or `&Value`, only owned variants, so cloning is unavoidable here. + Ok(Some(config.clone().try_into()?)) + } + + /// Try to parse the config for a given driver name, returning `Ok(None)` if it does not exist. + #[cfg(not(feature = "sqlx-toml"))] + pub fn try_parse(&self, _name: &str) -> Result, TryParseError> { + Ok(None) + } +} diff --git a/sqlx-core/src/config/mod.rs b/sqlx-core/src/config/mod.rs index 40feb007fd..267a2f1ed1 100644 --- a/sqlx-core/src/config/mod.rs +++ b/sqlx-core/src/config/mod.rs @@ -21,6 +21,8 @@ use std::path::{Path, PathBuf}; /// See [`common::Config`] for details. pub mod common; +pub mod drivers; + /// Configuration for the `query!()` family of macros. /// /// See [`macros::Config`] for details. @@ -56,6 +58,11 @@ pub struct Config { /// See [`common::Config`] for details. pub common: common::Config, + /// Configuration for database drivers. + /// + /// See [`drivers::Config`] for details. + pub drivers: drivers::Config, + /// Configuration for the `query!()` family of macros. /// /// See [`macros::Config`] for details. @@ -144,13 +151,12 @@ impl ConfigError { /// Internal methods for loading a `Config`. #[allow(clippy::result_large_err)] impl Config { - /// Get the cached config, or read `$CARGO_MANIFEST_DIR/sqlx.toml`. + /// Read `$CARGO_MANIFEST_DIR/sqlx.toml` or return `Config::default()` if it does not exist. /// - /// On success, the config is cached in a `static` and returned by future calls. - /// - /// Errors if `CARGO_MANIFEST_DIR` is not set, or if the config file could not be read. - /// - /// If the file does not exist, the cache is populated with `Config::default()`. + /// # Errors + /// * If `CARGO_MANIFEST_DIR` is not set. + /// * If the file exists but could not be read or parsed. + /// * If the file exists but the `sqlx-toml` feature is disabled. pub fn try_from_crate_or_default() -> Result { Self::read_from(get_crate_path()?).or_else(|e| { if let ConfigError::NotFound { .. } = e { @@ -161,11 +167,12 @@ impl Config { }) } - /// Get the cached config, or attempt to read it from the path given. - /// - /// On success, the config is cached in a `static` and returned by future calls. + /// Attempt to read `Config` from the path given. /// - /// Errors if the config file does not exist, or could not be read. + /// # Errors + /// * If the file does not exist. + /// * If the file exists but could not be read or parsed. + /// * If the file exists but the `sqlx-toml` feature is disabled. pub fn try_from_path(path: PathBuf) -> Result { Self::read_from(path) } diff --git a/sqlx-core/src/config/reference.toml b/sqlx-core/src/config/reference.toml index 787c3456db..f0acbdbe6a 100644 --- a/sqlx-core/src/config/reference.toml +++ b/sqlx-core/src/config/reference.toml @@ -15,11 +15,44 @@ # If not specified, defaults to `DATABASE_URL` database-url-var = "FOO_DATABASE_URL" -[common.drivers.sqlite] +############################################################################################### + +# Configuration of SQLx database drivers (**applies to macros and sqlx-cli only**) +[drivers] + +# Configure MySQL databases in macros and sqlx-cli. +[drivers.mysql] +# No fields implemented yet. This key is only used to validate parsing. + +# Configure Postgres databases in macros and sqlx-cli. +[drivers.postgres] +# No fields implemented yet. This key is only used to validate parsing. + +# Configure Postgres databases in macros and sqlx-cli. +[drivers.sqlite] # Load extensions into SQLite when running macros or migrations # # Defaults to an empty list, which has no effect. -load-extensions = ["uuid", "vsv"] +# +# Must be specified separately at run-time using `SqliteConnectOptions::extension()` or `::extension_with_entrypoint()`. +# +# Safety +# This causes arbitrary DLLs on the filesystem to be loaded at runtime, +# which can easily result in undefined behavior, memory corruption, +# or exploitable vulnerabilities if misused. +# +# It is not possible to provide a truly safe version of this API. +# +# Use this field with care, and only load extensions that you trust. +unsafe-load-extensions = ["uuid", "vsv"] + +# Configure external drivers in macros and sqlx-cli. +# +# These keys are only validated when the external driver tries to parse them, +# unlike config for built-in drivers which is validated directly. +[drivers.external.""] +foo = 'foo' +bar = true ############################################################################################### diff --git a/sqlx-core/src/config/tests.rs b/sqlx-core/src/config/tests.rs index 3d0f4fc871..91e877afb6 100644 --- a/sqlx-core/src/config/tests.rs +++ b/sqlx-core/src/config/tests.rs @@ -8,13 +8,32 @@ fn reference_parses_as_config() { .unwrap_or_else(|e| panic!("expected reference.toml to parse as Config: {e}")); assert_common_config(&config.common); + assert_drivers_config(&config.drivers); assert_macros_config(&config.macros); assert_migrate_config(&config.migrate); } fn assert_common_config(config: &config::common::Config) { assert_eq!(config.database_url_var.as_deref(), Some("FOO_DATABASE_URL")); - assert_eq!(config.drivers.sqlite.load_extensions[1].as_str(), "vsv"); +} + +fn assert_drivers_config(config: &config::drivers::Config) { + assert_eq!(config.sqlite.unsafe_load_extensions, ["uuid", "vsv"]); + + #[derive(Debug, Eq, PartialEq, serde::Deserialize)] + #[serde(rename_all = "kebab-case")] + struct TestExternalDriverConfig { + foo: String, + bar: bool, + } + + assert_eq!( + config.external.try_parse("").unwrap(), + Some(TestExternalDriverConfig { + foo: "foo".to_string(), + bar: true + }) + ); } fn assert_macros_config(config: &config::macros::Config) { diff --git a/sqlx-core/src/connection.rs b/sqlx-core/src/connection.rs index fb698e91aa..b5f8138b2b 100644 --- a/sqlx-core/src/connection.rs +++ b/sqlx-core/src/connection.rs @@ -1,6 +1,7 @@ use crate::database::{Database, HasStatementCache}; use crate::error::Error; +use crate::config; use crate::sql_str::SqlSafeStr; use crate::transaction::{Transaction, TransactionManager}; use futures_core::future::BoxFuture; @@ -269,4 +270,12 @@ pub trait ConnectOptions: 'static + Send + Sync + FromStr + Debug + self.log_statements(LevelFilter::Off) .log_slow_statements(LevelFilter::Off, Duration::default()) } + + #[doc(hidden)] + fn __unstable_apply_driver_config( + self, + _config: &config::drivers::Config, + ) -> crate::Result { + Ok(self) + } } diff --git a/sqlx-core/src/error.rs b/sqlx-core/src/error.rs index 4ec3a0ee2c..c6652aef75 100644 --- a/sqlx-core/src/error.rs +++ b/sqlx-core/src/error.rs @@ -30,10 +30,6 @@ pub struct UnexpectedNullError; #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum Error { - /// Error occurred while reading configuration file - #[error("error reading configuration file: {0}")] - ConfigFile(#[source] crate::config::ConfigError), - /// Error occurred while parsing a connection string. #[error("error with configuration: {0}")] Configuration(#[source] BoxDynError), @@ -127,6 +123,12 @@ pub enum Error { #[error("got unexpected connection status after attempting to begin transaction")] BeginFailed, + + // Not returned in normal operation. + /// Error occurred while reading configuration file + #[doc(hidden)] + #[error("error reading configuration file: {0}")] + ConfigFile(#[from] crate::config::ConfigError), } impl StdError for Box {} diff --git a/sqlx-macros-core/Cargo.toml b/sqlx-macros-core/Cargo.toml index c8eb5760a4..3c0b409113 100644 --- a/sqlx-macros-core/Cargo.toml +++ b/sqlx-macros-core/Cargo.toml @@ -35,6 +35,9 @@ postgres = ["sqlx-postgres"] sqlite = ["_sqlite", "sqlx-sqlite/bundled"] sqlite-unbundled = ["_sqlite", "sqlx-sqlite/unbundled"] +# Enables `drivers.sqlite.unsafe-load-extensions` in sqlx.toml +sqlite-load-extension = ["sqlx-sqlite/load-extension"] + # type integrations json = ["sqlx-core/json", "sqlx-mysql?/json", "sqlx-postgres?/json", "sqlx-sqlite?/json"] diff --git a/sqlx-macros-core/src/database/impls.rs b/sqlx-macros-core/src/database/impls.rs index 499406907f..523b85cc14 100644 --- a/sqlx-macros-core/src/database/impls.rs +++ b/sqlx-macros-core/src/database/impls.rs @@ -17,21 +17,23 @@ macro_rules! impl_describe_blocking { fn describe_blocking( query: &str, database_url: &str, + driver_config: &sqlx_core::config::drivers::Config, ) -> sqlx_core::Result> { use $crate::database::CachingDescribeBlocking; // This can't be a provided method because the `static` can't reference `Self`. static CACHE: CachingDescribeBlocking<$database> = CachingDescribeBlocking::new(); - CACHE.describe(query, database_url) + CACHE.describe(query, database_url, driver_config) } }; ($database:path, $describe:path) => { fn describe_blocking( query: &str, database_url: &str, + driver_config: &sqlx_core::config::drivers::Config, ) -> sqlx_core::Result> { - $describe(query, database_url) + $describe(query, database_url, driver_config) } }; } diff --git a/sqlx-macros-core/src/database/mod.rs b/sqlx-macros-core/src/database/mod.rs index 311dedf4d3..c247af06e3 100644 --- a/sqlx-macros-core/src/database/mod.rs +++ b/sqlx-macros-core/src/database/mod.rs @@ -1,7 +1,4 @@ -use std::collections::hash_map; -use std::collections::HashMap; -use std::sync::{LazyLock, Mutex}; - +use sqlx_core::config; use sqlx_core::connection::Connection; use sqlx_core::database::Database; use sqlx_core::describe::Describe; @@ -9,6 +6,9 @@ use sqlx_core::executor::Executor; use sqlx_core::sql_str::AssertSqlSafe; use sqlx_core::sql_str::SqlSafeStr; use sqlx_core::type_checking::TypeChecking; +use std::collections::hash_map; +use std::collections::HashMap; +use std::sync::{LazyLock, Mutex}; #[cfg(any(feature = "postgres", feature = "mysql", feature = "_sqlite"))] mod impls; @@ -25,7 +25,11 @@ pub trait DatabaseExt: Database + TypeChecking { syn::parse_str(Self::ROW_PATH).unwrap() } - fn describe_blocking(query: &str, database_url: &str) -> sqlx_core::Result>; + fn describe_blocking( + query: &str, + database_url: &str, + driver_config: &config::drivers::Config, + ) -> sqlx_core::Result>; } #[allow(dead_code)] @@ -42,7 +46,12 @@ impl CachingDescribeBlocking { } } - pub fn describe(&self, query: &str, database_url: &str) -> sqlx_core::Result> + pub fn describe( + &self, + query: &str, + database_url: &str, + _driver_config: &config::drivers::Config, + ) -> sqlx_core::Result> where for<'a> &'a mut DB::Connection: Executor<'a, Database = DB>, { diff --git a/sqlx-macros-core/src/query/mod.rs b/sqlx-macros-core/src/query/mod.rs index f6c0cae6db..ea454737c1 100644 --- a/sqlx-macros-core/src/query/mod.rs +++ b/sqlx-macros-core/src/query/mod.rs @@ -229,7 +229,7 @@ where let (query_data, offline): (QueryData, bool) = match data_source { QueryDataSource::Cached(dyn_data) => (QueryData::from_dyn_data(dyn_data)?, true), QueryDataSource::Live { database_url, .. } => { - let describe = DB::describe_blocking(&input.sql, database_url)?; + let describe = DB::describe_blocking(&input.sql, database_url, &config.drivers)?; (QueryData::from_describe(&input.sql, describe), false) } }; diff --git a/sqlx-macros/Cargo.toml b/sqlx-macros/Cargo.toml index 23079a3810..1c3cd96bff 100644 --- a/sqlx-macros/Cargo.toml +++ b/sqlx-macros/Cargo.toml @@ -36,6 +36,8 @@ postgres = ["sqlx-macros-core/postgres"] sqlite = ["sqlx-macros-core/sqlite"] sqlite-unbundled = ["sqlx-macros-core/sqlite-unbundled"] +sqlite-load-extension = ["sqlx-macros-core/sqlite-load-extension"] + # type bigdecimal = ["sqlx-macros-core/bigdecimal"] bit-vec = ["sqlx-macros-core/bit-vec"] diff --git a/sqlx-sqlite/Cargo.toml b/sqlx-sqlite/Cargo.toml index a84dccc6dc..4508e19ff6 100644 --- a/sqlx-sqlite/Cargo.toml +++ b/sqlx-sqlite/Cargo.toml @@ -22,7 +22,11 @@ uuid = ["dep:uuid", "sqlx-core/uuid"] regexp = ["dep:regex"] +# Conditionally compiled SQLite features +deserialize = [] +load-extension = [] preupdate-hook = ["libsqlite3-sys/preupdate_hook"] +unlock-notify = ["libsqlite3-sys/unlock_notify"] bundled = ["libsqlite3-sys/bundled"] unbundled = ["libsqlite3-sys/buildtime_bindgen"] @@ -33,6 +37,32 @@ sqlx-toml = ["sqlx-core/sqlx-toml"] bigdecimal = [] rust_decimal = [] +_unstable-all-types = [ + "json", "chrono", "time", "uuid", +] + +_unstable-all-sqlite-features = [ + "deserialize", + "load-extension", + "preupdate-hook", + "unlock-notify", +] + +_unstable-docs = [ + "bundled", "any", + "_unstable-all-types", + "_unstable-all-sqlite-features" +] + +[dependencies.libsqlite3-sys] +# See `sqlx-sqlite/src/lib.rs` for details. +version = ">=0.30.0, <0.36.0" +default-features = false +features = [ + "pkg-config", + "vcpkg", +] + [dependencies] futures-core = { version = "0.3.19", default-features = false } futures-channel = { version = "0.3.19", default-features = false, features = ["sink", "alloc", "std"] } @@ -60,23 +90,14 @@ thiserror = "2.0.0" serde = { version = "1.0.145", features = ["derive"], optional = true } regex = { version = "1.5.5", optional = true } -[dependencies.libsqlite3-sys] -version = "0.30.1" -default-features = false -features = [ - "pkg-config", - "vcpkg", - "unlock_notify" -] - [dependencies.sqlx-core] workspace = true [dev-dependencies] -sqlx = { workspace = true, default-features = false, features = ["macros", "runtime-tokio", "tls-none", "sqlite"] } +sqlx = { workspace = true, features = ["macros", "runtime-tokio", "tls-none", "sqlite"] } [lints] workspace = true [package.metadata.docs.rs] -features = ["bundled", "any", "json", "chrono", "time", "uuid"] +features = ["__unstable_docs"] diff --git a/sqlx-sqlite/src/any.rs b/sqlx-sqlite/src/any.rs index 0f556b1698..d33677c64e 100644 --- a/sqlx-sqlite/src/any.rs +++ b/sqlx-sqlite/src/any.rs @@ -199,22 +199,6 @@ impl<'a> TryFrom<&'a AnyConnectOptions> for SqliteConnectOptions { let mut opts_out = SqliteConnectOptions::from_url(&opts.database_url)?; opts_out.log_settings = opts.log_settings.clone(); - if let Some(ref path) = opts.enable_config { - if path.exists() { - let config = match sqlx_core::config::Config::try_from_path(path.to_path_buf()) { - Ok(cfg) => cfg, - Err(sqlx_core::config::ConfigError::NotFound { path: _ }) => { - return Ok(opts_out) - } - Err(err) => return Err(Self::Error::ConfigFile(err)), - }; - - for extension in config.common.drivers.sqlite.load_extensions.iter() { - opts_out = opts_out.extension(extension.to_owned()); - } - } - } - Ok(opts_out) } } diff --git a/sqlx-sqlite/src/connection/serialize.rs b/sqlx-sqlite/src/connection/deserialize.rs similarity index 98% rename from sqlx-sqlite/src/connection/serialize.rs rename to sqlx-sqlite/src/connection/deserialize.rs index c8835093da..5056eb6784 100644 --- a/sqlx-sqlite/src/connection/serialize.rs +++ b/sqlx-sqlite/src/connection/deserialize.rs @@ -26,6 +26,7 @@ impl SqliteConnection { /// * [`Error::Database`] if the schema does not exist or another error occurs. /// /// [`sqlite3_serialize()`]: https://sqlite.org/c3ref/serialize.html + #[cfg_attr(docsrs, doc(cfg(feature = "sqlite-deserialize")))] pub async fn serialize(&mut self, schema: Option<&str>) -> Result { let schema = schema.map(SchemaName::try_from).transpose()?; @@ -59,6 +60,7 @@ impl SqliteConnection { /// /// [`sqlite3_deserialize()`]: https://sqlite.org/c3ref/deserialize.html /// [deserialize-flags]: https://sqlite.org/c3ref/c_deserialize_freeonclose.html + #[cfg_attr(docsrs, doc(cfg(feature = "sqlite-deserialize")))] pub async fn deserialize( &mut self, schema: Option<&str>, diff --git a/sqlx-sqlite/src/connection/establish.rs b/sqlx-sqlite/src/connection/establish.rs index 545bad747c..d811275409 100644 --- a/sqlx-sqlite/src/connection/establish.rs +++ b/sqlx-sqlite/src/connection/establish.rs @@ -2,52 +2,34 @@ use crate::connection::handle::ConnectionHandle; use crate::connection::LogSettings; use crate::connection::{ConnectionState, Statements}; use crate::error::Error; -use crate::{SqliteConnectOptions, SqliteError}; +use crate::SqliteConnectOptions; use libsqlite3_sys::{ - sqlite3, sqlite3_busy_timeout, sqlite3_db_config, sqlite3_extended_result_codes, sqlite3_free, - sqlite3_load_extension, sqlite3_open_v2, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, SQLITE_OK, - SQLITE_OPEN_CREATE, SQLITE_OPEN_FULLMUTEX, SQLITE_OPEN_MEMORY, SQLITE_OPEN_NOMUTEX, - SQLITE_OPEN_PRIVATECACHE, SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_SHAREDCACHE, - SQLITE_OPEN_URI, + sqlite3_busy_timeout, SQLITE_OPEN_CREATE, SQLITE_OPEN_FULLMUTEX, SQLITE_OPEN_MEMORY, + SQLITE_OPEN_NOMUTEX, SQLITE_OPEN_PRIVATECACHE, SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, + SQLITE_OPEN_SHAREDCACHE, SQLITE_OPEN_URI, }; use percent_encoding::NON_ALPHANUMERIC; -use sqlx_core::IndexMap; use std::collections::BTreeMap; -use std::ffi::{c_void, CStr, CString}; +use std::ffi::CString; use std::io; -use std::os::raw::c_int; -use std::ptr::{addr_of_mut, null, null_mut}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; +#[cfg(feature = "load-extension")] +use sqlx_core::IndexMap; + // This was originally `AtomicU64` but that's not supported on MIPS (or PowerPC): // https://github.com/launchbadge/sqlx/issues/2859 // https://doc.rust-lang.org/stable/std/sync/atomic/index.html#portability static THREAD_ID: AtomicUsize = AtomicUsize::new(0); -#[derive(Copy, Clone)] -enum SqliteLoadExtensionMode { - /// Enables only the C-API, leaving the SQL function disabled. - Enable, - /// Disables both the C-API and the SQL function. - DisableAll, -} - -impl SqliteLoadExtensionMode { - fn to_int(self) -> c_int { - match self { - SqliteLoadExtensionMode::Enable => 1, - SqliteLoadExtensionMode::DisableAll => 0, - } - } -} - pub struct EstablishParams { filename: CString, open_flags: i32, busy_timeout: Duration, statement_cache_capacity: usize, log_settings: LogSettings, + #[cfg(feature = "load-extension")] extensions: IndexMap>, pub(crate) thread_name: String, pub(crate) command_channel_size: usize, @@ -124,6 +106,7 @@ impl EstablishParams { ) })?; + #[cfg(feature = "load-extension")] let extensions = options .extensions .iter() @@ -159,6 +142,7 @@ impl EstablishParams { busy_timeout: options.busy_timeout, statement_cache_capacity: options.statement_cache_capacity, log_settings: options.log_settings.clone(), + #[cfg(feature = "load-extension")] extensions, thread_name: (options.thread_name)(thread_id as u64), command_channel_size: options.command_channel_size, @@ -167,109 +151,19 @@ impl EstablishParams { }) } - // Enable extension loading via the db_config function, as recommended by the docs rather - // than the more obvious `sqlite3_enable_load_extension` - // https://www.sqlite.org/c3ref/db_config.html - // https://www.sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigenableloadextension - unsafe fn sqlite3_set_load_extension( - db: *mut sqlite3, - mode: SqliteLoadExtensionMode, - ) -> Result<(), Error> { - let status = sqlite3_db_config( - db, - SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, - mode.to_int(), - null::(), - ); - - if status != SQLITE_OK { - return Err(Error::Database(Box::new(SqliteError::new(db)))); - } - - Ok(()) - } - pub(crate) fn establish(&self) -> Result { - let mut handle = null_mut(); - - // - let mut status = unsafe { - sqlite3_open_v2(self.filename.as_ptr(), &mut handle, self.open_flags, null()) - }; - - if handle.is_null() { - // Failed to allocate memory - return Err(Error::Io(io::Error::new( - io::ErrorKind::OutOfMemory, - "SQLite is unable to allocate memory to hold the sqlite3 object", - ))); - } - - // SAFE: tested for NULL just above - // This allows any returns below to close this handle with RAII - let mut handle = unsafe { ConnectionHandle::new(handle) }; - - if status != SQLITE_OK { - return Err(Error::Database(Box::new(handle.expect_error()))); - } + let mut handle = ConnectionHandle::open(&self.filename, self.open_flags)?; - // Enable extended result codes - // https://www.sqlite.org/c3ref/extended_result_codes.html + #[cfg(feature = "load-extension")] unsafe { - // NOTE: ignore the failure here - sqlite3_extended_result_codes(handle.as_ptr(), 1); - } - - if !self.extensions.is_empty() { - // Enable loading extensions - unsafe { - Self::sqlite3_set_load_extension(handle.as_ptr(), SqliteLoadExtensionMode::Enable)?; - } - - for ext in self.extensions.iter() { - // `sqlite3_load_extension` is unusual as it returns its errors via an out-pointer - // rather than by calling `sqlite3_errmsg` - let mut error_msg = null_mut(); - status = unsafe { - sqlite3_load_extension( - handle.as_ptr(), - ext.0.as_ptr(), - ext.1.as_ref().map_or(null(), |e| e.as_ptr()), - addr_of_mut!(error_msg), - ) - }; - - if status != SQLITE_OK { - let mut e = handle.expect_error(); - - // SAFETY: We become responsible for any memory allocation at `&error`, so test - // for null and take an RAII version for returns - if !error_msg.is_null() { - e = e.with_message(unsafe { - let msg = CStr::from_ptr(error_msg).to_string_lossy().into(); - sqlite3_free(error_msg as *mut c_void); - msg - }); - } - return Err(Error::Database(Box::new(e))); - } - } // Preempt any hypothetical security issues arising from leaving ENABLE_LOAD_EXTENSION - // on by disabling the flag again once we've loaded all the requested modules. - // Fail-fast (via `?`) if disabling the extension loader didn't work for some reason, - // avoids an unexpected state going undetected. - unsafe { - Self::sqlite3_set_load_extension( - handle.as_ptr(), - SqliteLoadExtensionMode::DisableAll, - )?; - } + self.apply_extensions(&mut handle)?; } #[cfg(feature = "regexp")] if self.register_regexp_function { // configure a `regexp` function for sqlite, it does not come with one by default let status = crate::regexp::register(handle.as_ptr()); - if status != SQLITE_OK { + if status != libsqlite3_sys::SQLITE_OK { return Err(Error::Database(Box::new(handle.expect_error()))); } } @@ -282,11 +176,7 @@ impl EstablishParams { let ms = i32::try_from(self.busy_timeout.as_millis()) .expect("Given busy timeout value is too big."); - status = unsafe { sqlite3_busy_timeout(handle.as_ptr(), ms) }; - - if status != SQLITE_OK { - return Err(Error::Database(Box::new(handle.expect_error()))); - } + handle.call_with_result(|db| unsafe { sqlite3_busy_timeout(db, ms) })?; Ok(ConnectionState { handle, @@ -300,4 +190,77 @@ impl EstablishParams { rollback_hook_callback: None, }) } + + #[cfg(feature = "load-extension")] + unsafe fn apply_extensions(&self, handle: &mut ConnectionHandle) -> Result<(), Error> { + use libsqlite3_sys::{sqlite3_free, sqlite3_load_extension}; + use std::ffi::{c_int, CStr}; + use std::ptr; + + /// `true` enables *just* `sqlite3_load_extension`, false disables *all* extension loading. + fn enable_load_extension( + handle: &mut ConnectionHandle, + enabled: bool, + ) -> Result<(), Error> { + use libsqlite3_sys::{sqlite3_db_config, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION}; + + // SAFETY: we have exclusive access and this matches the expected signature + // + handle.call_with_result(|db| unsafe { + // https://doc.rust-lang.org/reference/expressions/operator-expr.html#r-expr.as.bool-char-as-int + sqlite3_db_config( + db, + SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, + enabled as c_int, + ptr::null_mut::(), + ) + })?; + + Ok(()) + } + + if self.extensions.is_empty() { + return Ok(()); + } + + // We enable extension loading only so long as *we're* doing it. + enable_load_extension(handle, true)?; + + for (name, entrypoint) in &self.extensions { + let name_ptr = name.as_ptr(); + let entrypoint_ptr = entrypoint.as_ref().map_or_else(ptr::null, |s| s.as_ptr()); + let mut err_msg_ptr = ptr::null_mut(); + + // SAFETY: + // * we have exclusive access + // * all pointers are initialized + // * we warn the user about loading extensions in documentation + handle + .call_with_result(|db| unsafe { + sqlite3_load_extension(db, name_ptr, entrypoint_ptr, &mut err_msg_ptr) + }) + .map_err(|e| { + if !err_msg_ptr.is_null() { + // SAFETY: pointer is not-null, + // and we copy the error message to an allocation we own. + let err_msg = unsafe { CStr::from_ptr(err_msg_ptr) } + // In practice, the string *should* be UTF-8. + .to_string_lossy() + .into_owned(); + + // SAFETY: we're expected to free the error message afterward. + unsafe { + sqlite3_free(err_msg_ptr.cast()); + } + + e.with_message(err_msg) + } else { + e + } + })?; + } + + // We then disable extension loading immediately afterward. + enable_load_extension(handle, false) + } } diff --git a/sqlx-sqlite/src/connection/handle.rs b/sqlx-sqlite/src/connection/handle.rs index 60fbe17dc6..07a5a6da17 100644 --- a/sqlx-sqlite/src/connection/handle.rs +++ b/sqlx-sqlite/src/connection/handle.rs @@ -1,17 +1,17 @@ -use std::ffi::CString; -use std::ptr; +use std::ffi::{c_int, CStr, CString}; use std::ptr::NonNull; +use std::{io, ptr}; use crate::error::Error; use libsqlite3_sys::{ - sqlite3, sqlite3_close, sqlite3_exec, sqlite3_last_insert_rowid, SQLITE_LOCKED_SHAREDCACHE, - SQLITE_OK, + sqlite3, sqlite3_close, sqlite3_exec, sqlite3_extended_result_codes, sqlite3_last_insert_rowid, + sqlite3_open_v2, SQLITE_OK, }; -use crate::{statement::unlock_notify, SqliteError}; +use crate::SqliteError; -/// Managed handle to the raw SQLite3 database handle. -/// The database handle will be closed when this is dropped and no `ConnectionHandleRef`s exist. +/// Managed SQLite3 database handle. +/// The database handle will be closed when this is dropped. #[derive(Debug)] pub(crate) struct ConnectionHandle(NonNull); @@ -19,17 +19,43 @@ pub(crate) struct ConnectionHandle(NonNull); // one is accessing it at the same time. This is upheld as long as [SQLITE_CONFIG_MULTITHREAD] is // enabled and [SQLITE_THREADSAFE] was enabled when sqlite was compiled. We refuse to work // if these conditions are not upheld. - +// // - // unsafe impl Send for ConnectionHandle {} impl ConnectionHandle { - #[inline] - pub(super) unsafe fn new(ptr: *mut sqlite3) -> Self { - Self(NonNull::new_unchecked(ptr)) + pub(crate) fn open(filename: &CStr, flags: c_int) -> Result { + let mut handle = ptr::null_mut(); + + // + let status = unsafe { sqlite3_open_v2(filename.as_ptr(), &mut handle, flags, ptr::null()) }; + + // SAFETY: the database is still initialized as long as the pointer is not `NULL`. + // We need to close it even if there's an error. + let mut handle = Self(NonNull::new(handle).ok_or_else(|| { + Error::Io(io::Error::new( + io::ErrorKind::OutOfMemory, + "SQLite is unable to allocate memory to hold the sqlite3 object", + )) + })?); + + if status != SQLITE_OK { + return Err(Error::Database(Box::new(handle.expect_error()))); + } + + // Enable extended result codes + // https://www.sqlite.org/c3ref/extended_result_codes.html + unsafe { + // This only returns a non-OK code if SQLite is built with `SQLITE_ENABLE_API_ARMOR` + // and the database pointer is `NULL` or already closed. + // + // The invariants of this type guarantee that neither is true. + sqlite3_extended_result_codes(handle.as_ptr(), 1); + } + + Ok(handle) } #[inline] @@ -41,6 +67,17 @@ impl ConnectionHandle { self.0 } + pub(crate) fn call_with_result( + &mut self, + call: impl FnOnce(*mut sqlite3) -> c_int, + ) -> Result<(), SqliteError> { + if call(self.as_ptr()) == SQLITE_OK { + Ok(()) + } else { + Err(self.expect_error()) + } + } + pub(crate) fn last_insert_rowid(&mut self) -> i64 { // SAFETY: we have exclusive access to the database handle unsafe { sqlite3_last_insert_rowid(self.as_ptr()) } @@ -63,6 +100,7 @@ impl ConnectionHandle { // SAFETY: we have exclusive access to the database handle unsafe { + #[cfg_attr(not(feature = "unlock-notify"), expect(clippy::never_loop))] loop { let status = sqlite3_exec( self.as_ptr(), @@ -77,7 +115,10 @@ impl ConnectionHandle { match status { SQLITE_OK => return Ok(()), - SQLITE_LOCKED_SHAREDCACHE => unlock_notify::wait(self.as_ptr())?, + #[cfg(feature = "unlock-notify")] + libsqlite3_sys::SQLITE_LOCKED_SHAREDCACHE => { + crate::statement::unlock_notify::wait(self.as_ptr())? + } _ => return Err(SqliteError::new(self.as_ptr()).into()), } } diff --git a/sqlx-sqlite/src/connection/mod.rs b/sqlx-sqlite/src/connection/mod.rs index 1483f2c07c..c32b81d300 100644 --- a/sqlx-sqlite/src/connection/mod.rs +++ b/sqlx-sqlite/src/connection/mod.rs @@ -40,7 +40,9 @@ mod handle; pub(crate) mod intmap; #[cfg(feature = "preupdate-hook")] mod preupdate_hook; -pub(crate) mod serialize; + +#[cfg(feature = "deserialize")] +pub(crate) mod deserialize; mod worker; diff --git a/sqlx-sqlite/src/connection/preupdate_hook.rs b/sqlx-sqlite/src/connection/preupdate_hook.rs index edcb078124..2759588873 100644 --- a/sqlx-sqlite/src/connection/preupdate_hook.rs +++ b/sqlx-sqlite/src/connection/preupdate_hook.rs @@ -1,10 +1,9 @@ use super::SqliteOperation; -use crate::type_info::DataType; -use crate::{SqliteError, SqliteTypeInfo, SqliteValueRef}; +use crate::{SqliteError, SqliteValueRef}; use libsqlite3_sys::{ sqlite3, sqlite3_preupdate_count, sqlite3_preupdate_depth, sqlite3_preupdate_new, - sqlite3_preupdate_old, sqlite3_value, sqlite3_value_type, SQLITE_OK, + sqlite3_preupdate_old, sqlite3_value, SQLITE_OK, }; use std::ffi::CStr; use std::marker::PhantomData; @@ -122,9 +121,7 @@ impl<'a> PreupdateHookResult<'a> { if ret != SQLITE_OK { return Err(PreupdateError::Database(SqliteError::new(self.db))); } - let data_type = DataType::from_code(sqlite3_value_type(p_value)); - // SAFETY: SQLite will free the sqlite3_value when the callback returns - Ok(SqliteValueRef::borrowed(p_value, SqliteTypeInfo(data_type))) + Ok(SqliteValueRef::borrowed(p_value)) } } diff --git a/sqlx-sqlite/src/connection/worker.rs b/sqlx-sqlite/src/connection/worker.rs index 1be624b7c4..ae50f3e896 100644 --- a/sqlx-sqlite/src/connection/worker.rs +++ b/sqlx-sqlite/src/connection/worker.rs @@ -21,7 +21,8 @@ use crate::connection::execute; use crate::connection::ConnectionState; use crate::{Sqlite, SqliteArguments, SqliteQueryResult, SqliteRow, SqliteStatement}; -use super::serialize::{deserialize, serialize, SchemaName, SqliteOwnedBuf}; +#[cfg(feature = "deserialize")] +use crate::connection::deserialize::{deserialize, serialize, SchemaName, SqliteOwnedBuf}; // Each SQLite connection has a dedicated thread. @@ -67,10 +68,12 @@ enum Command { tx: flume::Sender, Error>>, limit: Option, }, + #[cfg(feature = "deserialize")] Serialize { schema: Option, tx: oneshot::Sender>, }, + #[cfg(feature = "deserialize")] Deserialize { schema: Option, data: SqliteOwnedBuf, @@ -302,9 +305,11 @@ impl ConnectionWorker { } } } + #[cfg(feature = "deserialize")] Command::Serialize { schema, tx } => { tx.send(serialize(&mut conn, schema)).ok(); } + #[cfg(feature = "deserialize")] Command::Deserialize { schema, data, read_only, tx } => { tx.send(deserialize(&mut conn, schema, data, read_only)).ok(); } @@ -397,6 +402,7 @@ impl ConnectionWorker { self.oneshot_cmd(|tx| Command::Ping { tx }).await } + #[cfg(feature = "deserialize")] pub(crate) async fn deserialize( &mut self, schema: Option, @@ -412,6 +418,7 @@ impl ConnectionWorker { .await? } + #[cfg(feature = "deserialize")] pub(crate) async fn serialize( &mut self, schema: Option, diff --git a/sqlx-sqlite/src/error.rs b/sqlx-sqlite/src/error.rs index eee2e8b1a2..b4373d7a07 100644 --- a/sqlx-sqlite/src/error.rs +++ b/sqlx-sqlite/src/error.rs @@ -7,7 +7,7 @@ use std::{borrow::Cow, str}; use libsqlite3_sys::{ sqlite3, sqlite3_errmsg, sqlite3_errstr, sqlite3_extended_errcode, SQLITE_CONSTRAINT_CHECK, SQLITE_CONSTRAINT_FOREIGNKEY, SQLITE_CONSTRAINT_NOTNULL, SQLITE_CONSTRAINT_PRIMARYKEY, - SQLITE_CONSTRAINT_UNIQUE, SQLITE_ERROR, + SQLITE_CONSTRAINT_UNIQUE, SQLITE_ERROR, SQLITE_NOMEM, }; pub(crate) use sqlx_core::error::*; @@ -49,11 +49,13 @@ impl SqliteError { } /// For errors during extension load, the error message is supplied via a separate pointer + #[allow(dead_code)] pub(crate) fn with_message(mut self, error_msg: String) -> Self { self.message = error_msg.into(); self } + #[allow(dead_code)] pub(crate) fn from_code(code: c_int) -> Self { let message = unsafe { let errstr = sqlite3_errstr(code); @@ -72,12 +74,18 @@ impl SqliteError { SqliteError { code, message } } + #[allow(dead_code)] pub(crate) fn generic(message: impl Into>) -> Self { Self { code: SQLITE_ERROR, message: message.into(), } } + + /// Return `SQLITE_NOMEM`. + pub(crate) fn nomem() -> Self { + Self::from_code(SQLITE_NOMEM) + } } impl Display for SqliteError { diff --git a/sqlx-sqlite/src/lib.rs b/sqlx-sqlite/src/lib.rs index 3c4ec524f0..36d3ee7e69 100644 --- a/sqlx-sqlite/src/lib.rs +++ b/sqlx-sqlite/src/lib.rs @@ -1,38 +1,68 @@ //! **SQLite** database driver. //! -//! ### Note: linkage is semver-exempt. +//! ### Note: `libsqlite3-sys` Version //! This driver uses the `libsqlite3-sys` crate which links the native library for SQLite 3. -//! With the "sqlite" feature, we enable the `bundled` feature which builds and links SQLite from -//! source. +//! Only one version of `libsqlite3-sys` may appear in the dependency tree of your project. //! -//! We reserve the right to upgrade the version of `libsqlite3-sys` as necessary to pick up new -//! `3.x.y` versions of SQLite. +//! As of SQLx 0.9.0, the version of `libsqlite3-sys` is now a range instead of any specific version. +//! See the `Cargo.toml` of the `sqlx-sqlite` crate for the current version range. //! -//! Due to Cargo's requirement that only one version of a crate that links a given native library -//! exists in the dependency graph at a time, using SQLx alongside another crate linking -//! `libsqlite3-sys` like `rusqlite` is a semver hazard. +//! If you are using `rusqlite` or any other crate that indirectly depends on `libsqlite3-sys`, +//! this should allow Cargo to select a compatible version. //! -//! If you are doing so, we recommend pinning the version of both SQLx and the other crate you're -//! using to prevent a `cargo update` from breaking things, e.g.: +//! If Cargo **fails to select a compatible version**, this means the other crate is using +//! a `libsqlite3-sys` version outside of this range. +//! +//! We may increase the *maximum* version of the range at our discretion, +//! in patch (SemVer-compatible) releases, to allow users to upgrade to newer versions as desired. +//! +//! The *minimum* version of the range may be increased over time to drop very old or +//! insecure versions of SQLite, but this will only occur in major (SemVer-incompatible) releases. +//! +//! Note that this means a `cargo update` may increase the `libsqlite3-sys` version, +//! which could, in rare cases, break your build. +//! +//! To prevent this, you can pin the `libsqlite3-sys` version in your own dependencies: //! //! ```toml -//! sqlx = { version = "=0.8.1", features = ["sqlite"] } -//! rusqlite = "=0.32.1" +//! [dependencies] +//! # for example, if 0.35.0 breaks the build +//! libsqlite3-sys = "0.34" //! ``` //! -//! and then upgrade these crates in lockstep when necessary. +//! ### Static Linking (Default) +//! The `sqlite` feature enables the `bundled` feature of `libsqlite3-sys`, +//! which builds SQLite 3 from included source code and statically links it into the final binary. +//! +//! This requires some C build tools to be installed on the system; see +//! [the `rusqlite` README][rusqlite-readme-building] for details. +//! +//! This version of SQLite is generally much newer than system-installed versions of SQLite +//! (especially for LTS Linux distributions), and can be updated with a `cargo update`, +//! so this is the recommended option for ease of use and keeping up-to-date. //! //! ### Dynamic linking -//! To dynamically link to a system SQLite library, the "sqlite-unbundled" feature can be used +//! To dynamically link to an existing SQLite library, the `sqlite-unbundled` feature can be used //! instead. //! //! This allows updating SQLite independently of SQLx or using forked versions, but you must have -//! SQLite installed on the system or provide a path to the library at build time (See -//! [the `rusqlite` README](https://github.com/rusqlite/rusqlite?tab=readme-ov-file#notes-on-building-rusqlite-and-libsqlite3-sys) -//! for details). +//! SQLite installed on the system or provide a path to the library at build time (see +//! [the `rusqlite` README][rusqlite-readme-building] for details). +//! +//! Note that this _may_ result in link errors if the SQLite version is too old, +//! or has [certain features disabled at compile-time](https://www.sqlite.org/compile.html). +//! +//! SQLite version `3.20.0` (released August 2018) or newer is recommended. +//! +//! **Please check your SQLite version and the flags it was built with before opening +//! a GitHub issue because of errors in `libsqlite3-sys`.** Thank you. +//! +//! [rusqlite-readme-building]: https://github.com/rusqlite/rusqlite?tab=readme-ov-file#notes-on-building-rusqlite-and-libsqlite3-sys +//! +//! ### Optional Features +//! +//! The following features //! -//! It may result in link errors if the SQLite version is too old. Version `3.20.0` or newer is -//! recommended. It can increase build time due to the use of bindgen. // SQLite is a C library. All interactions require FFI which is unsafe. // All unsafe blocks should have comments pointing to SQLite docs and ensuring that we maintain @@ -46,8 +76,11 @@ use std::sync::atomic::AtomicBool; pub use arguments::{SqliteArgumentValue, SqliteArguments}; pub use column::SqliteColumn; -pub use connection::serialize::SqliteOwnedBuf; +#[cfg(feature = "deserialize")] +#[cfg_attr(docsrs, doc(cfg(feature = "deserialize")))] +pub use connection::deserialize::SqliteOwnedBuf; #[cfg(feature = "preupdate-hook")] +#[cfg_attr(docsrs, doc(cfg(feature = "preupdate-hook")))] pub use connection::PreupdateHookResult; pub use connection::{LockedSqliteHandle, SqliteConnection, SqliteOperation, UpdateHookResult}; pub use database::Sqlite; @@ -57,7 +90,6 @@ pub use options::{ }; pub use query_result::SqliteQueryResult; pub use row::SqliteRow; -use sqlx_core::sql_str::{AssertSqlSafe, SqlSafeStr}; pub use statement::SqliteStatement; pub use transaction::SqliteTransactionManager; pub use type_info::SqliteTypeInfo; @@ -67,9 +99,11 @@ use crate::connection::establish::EstablishParams; pub(crate) use sqlx_core::driver_prelude::*; +use sqlx_core::config; use sqlx_core::describe::Describe; use sqlx_core::error::Error; use sqlx_core::executor::Executor; +use sqlx_core::sql_str::{AssertSqlSafe, SqlSafeStr}; mod arguments; mod column; @@ -127,18 +161,14 @@ pub static CREATE_DB_WAL: AtomicBool = AtomicBool::new(true); /// UNSTABLE: for use by `sqlite-macros-core` only. #[doc(hidden)] -pub fn describe_blocking(query: &str, database_url: &str) -> Result, Error> { +pub fn describe_blocking( + query: &str, + database_url: &str, + driver_config: &config::drivers::Config, +) -> Result, Error> { let mut opts: SqliteConnectOptions = database_url.parse()?; - match sqlx_core::config::Config::try_from_crate_or_default() { - Ok(config) => { - for extension in config.common.drivers.sqlite.load_extensions.iter() { - opts = opts.extension(extension.to_owned()); - } - } - Err(sqlx_core::config::ConfigError::NotFound { path: _ }) => {} - Err(err) => return Err(Error::ConfigFile(err)), - } + opts = opts.apply_driver_config(&driver_config.sqlite)?; let params = EstablishParams::from_options(&opts)?; let mut conn = params.establish()?; diff --git a/sqlx-sqlite/src/options/connect.rs b/sqlx-sqlite/src/options/connect.rs index 3f147981d1..cfa25872b2 100644 --- a/sqlx-sqlite/src/options/connect.rs +++ b/sqlx-sqlite/src/options/connect.rs @@ -1,5 +1,6 @@ use crate::{SqliteConnectOptions, SqliteConnection}; use log::LevelFilter; +use sqlx_core::config; use sqlx_core::connection::ConnectOptions; use sqlx_core::error::Error; use sqlx_core::executor::Executor; @@ -57,6 +58,13 @@ impl ConnectOptions for SqliteConnectOptions { self.log_settings.log_slow_statements(level, duration); self } + + fn __unstable_apply_driver_config( + self, + config: &config::drivers::Config, + ) -> crate::Result { + self.apply_driver_config(&config.sqlite) + } } impl SqliteConnectOptions { diff --git a/sqlx-sqlite/src/options/mod.rs b/sqlx-sqlite/src/options/mod.rs index 5f22edd913..b2849f243c 100644 --- a/sqlx-sqlite/src/options/mod.rs +++ b/sqlx-sqlite/src/options/mod.rs @@ -18,7 +18,7 @@ pub use synchronous::SqliteSynchronous; use crate::common::DebugFn; use crate::connection::collation::Collation; -use sqlx_core::IndexMap; +use sqlx_core::{config, IndexMap}; /// Options and flags which can be used to configure a SQLite connection. /// @@ -72,10 +72,12 @@ pub struct SqliteConnectOptions { pub(crate) vfs: Option>, pub(crate) pragmas: IndexMap, Option>>, + /// Extensions are specified as a pair of \, the majority /// of SQLite extensions will use the default entry points specified in the docs, these should /// be added to the map with a `None` value. /// + #[cfg(feature = "load-extension")] pub(crate) extensions: IndexMap, Option>>, pub(crate) command_channel_size: usize, @@ -203,6 +205,7 @@ impl SqliteConnectOptions { immutable: false, vfs: None, pragmas, + #[cfg(feature = "load-extension")] extensions: Default::default(), collations: Default::default(), serialized: false, @@ -465,35 +468,94 @@ impl SqliteConnectOptions { self } - /// Load an [extension](https://www.sqlite.org/loadext.html) at run-time when the database connection - /// is established, using the default entry point. + /// Add a [SQLite extension](https://www.sqlite.org/loadext.html) to be loaded into the database + /// connection at startup, using the default entrypoint. + /// + /// Most common SQLite extensions can be loaded using this method. + /// For extensions where you need to override the entry point, + /// use [`.extension_with_entrypoint()`]. + /// + /// Multiple extensions can be loaded by calling this method, + /// or [`.extension_with_entrypoint()`] where applicable, + /// once for each extension. + /// + /// Extension loading is only enabled during the initialization of the connection, + /// and disabled before `connect()` returns by setting + /// [`SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION`] to 0. + /// + /// This will not enable the SQL `load_extension()` function. + /// + /// [`SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION`]: https://www.sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigenableloadextension + /// [`.extension_with_entrypoint()`]: Self::extension_with_entrypoint /// - /// Most common SQLite extensions can be loaded using this method, for extensions where you need - /// to specify the entry point, use [`extension_with_entrypoint`][`Self::extension_with_entrypoint`] instead. + /// # Safety + /// This causes arbitrary DLLs on the filesystem to be loaded at runtime, + /// which can easily result in undefined behavior, memory corruption, + /// or exploitable vulnerabilities if misused. /// - /// Multiple extensions can be loaded by calling the method repeatedly on the options struct, they - /// will be loaded in the order they are added. + /// It is not possible to provide a truly safe version of this API. + /// + /// Use this method with care, and only load extensions that you trust. + /// + /// # Example /// ```rust,no_run /// # use sqlx_core::error::Error; /// # use std::str::FromStr; /// # use sqlx_sqlite::SqliteConnectOptions; /// # fn options() -> Result { - /// let options = SqliteConnectOptions::from_str("sqlite://data.db")? - /// .extension("vsv") - /// .extension("mod_spatialite"); + /// let mut options = SqliteConnectOptions::from_str("sqlite://data.db")?; + /// + /// // SAFETY: these are trusted extensions. + /// unsafe { + /// options = options + /// .extension("vsv") + /// .extension("mod_spatialite"); + /// } + /// /// # Ok(options) /// # } /// ``` - pub fn extension(mut self, extension_name: impl Into>) -> Self { + #[cfg(feature = "load-extension")] + #[cfg_attr(docsrs, doc(cfg(feature = "sqlite-load-extension")))] + pub unsafe fn extension(mut self, extension_name: impl Into>) -> Self { self.extensions.insert(extension_name.into(), None); self } - /// Load an extension with a specified entry point. + /// Add a [SQLite extension](https://www.sqlite.org/loadext.html) to be loaded into the database + /// connection at startup, overriding the entrypoint. + /// + /// See also [`.extension()`] for extensions using the standard entrypoint name + /// `sqlite3_extension_init` or `sqlite3__init`. + /// + /// Multiple extensions can be loaded by calling this method, + /// or [`.extension()`] where applicable, + /// once for each extension. + /// + /// Extension loading is only enabled during the initialization of the connection, + /// and disabled before `connect()` returns by setting + /// [`SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION`] to 0. + /// + /// This will not enable the SQL `load_extension()` function. /// - /// Useful when using non-standard extensions, or when developing your own, the second argument - /// specifies where SQLite should expect to find the extension init routine. - pub fn extension_with_entrypoint( + /// [`SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION`]: https://www.sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigenableloadextension + /// [`.extension_with_entrypoint()`]: Self::extension_with_entrypoint + /// + /// # Safety + /// This causes arbitrary DLLs on the filesystem to be loaded at runtime, + /// which can easily result in undefined behavior, memory corruption, + /// or exploitable vulnerabilities if misused. + /// + /// If you specify the wrong entrypoint name, it _may_ simply result in an error, + /// or it may end up invoking the wrong routine, leading to undefined behavior. + /// + /// It is not possible to provide a truly safe version of this API. + /// + /// Use this method with care, only load extensions that you trust, + /// and double-check the entrypoint name with the extension's documentation or source code. + #[cfg(feature = "load-extension")] + #[cfg_attr(docsrs, doc(cfg(feature = "sqlite-load-extension")))] + pub unsafe fn extension_with_entrypoint( mut self, extension_name: impl Into>, entry_point: impl Into>, @@ -575,4 +637,31 @@ impl SqliteConnectOptions { self.register_regexp_function = true; self } + + #[cfg_attr(not(feature = "load-extension"), expect(unused_mut))] + pub(crate) fn apply_driver_config( + mut self, + config: &config::drivers::SqliteConfig, + ) -> crate::Result { + #[cfg(feature = "load-extension")] + for extension in &config.unsafe_load_extensions { + // SAFETY: the documentation warns the user about loading extensions + self = unsafe { self.extension(extension.clone()) }; + } + + #[cfg(not(feature = "load-extension"))] + if !config.unsafe_load_extensions.is_empty() { + return Err(sqlx_core::Error::Configuration( + format!( + "sqlx.toml specifies `drivers.sqlite.unsafe-load-extensions = {:?}` \ + but extension loading is not enabled; \ + enable the `sqlite-load-extension` feature of SQLx to use SQLite extensions", + config.unsafe_load_extensions, + ) + .into(), + )); + } + + Ok(self) + } } diff --git a/sqlx-sqlite/src/row.rs b/sqlx-sqlite/src/row.rs index 3cb47efec8..ec038b6d40 100644 --- a/sqlx-sqlite/src/row.rs +++ b/sqlx-sqlite/src/row.rs @@ -40,7 +40,7 @@ impl SqliteRow { values.push(unsafe { let raw = statement.column_value(i); - SqliteValue::new(raw, columns[i].type_info.clone()) + SqliteValue::dup(raw, Some(columns[i].type_info.clone())) }); } diff --git a/sqlx-sqlite/src/statement/handle.rs b/sqlx-sqlite/src/statement/handle.rs index e3a757868b..7985ff9d36 100644 --- a/sqlx-sqlite/src/statement/handle.rs +++ b/sqlx-sqlite/src/statement/handle.rs @@ -13,8 +13,8 @@ use libsqlite3_sys::{ sqlite3_column_name, sqlite3_column_origin_name, sqlite3_column_table_name, sqlite3_column_type, sqlite3_column_value, sqlite3_db_handle, sqlite3_finalize, sqlite3_reset, sqlite3_sql, sqlite3_step, sqlite3_stmt, sqlite3_stmt_readonly, sqlite3_table_column_metadata, - sqlite3_value, SQLITE_DONE, SQLITE_LOCKED_SHAREDCACHE, SQLITE_MISUSE, SQLITE_OK, SQLITE_ROW, - SQLITE_TRANSIENT, SQLITE_UTF8, + sqlite3_value, SQLITE_DONE, SQLITE_MISUSE, SQLITE_OK, SQLITE_ROW, SQLITE_TRANSIENT, + SQLITE_UTF8, }; use sqlx_core::column::{ColumnOrigin, TableColumn}; use std::os::raw::{c_char, c_int}; @@ -24,8 +24,6 @@ use std::slice::from_raw_parts; use std::str::{from_utf8, from_utf8_unchecked}; use std::sync::Arc; -use super::unlock_notify; - #[derive(Debug)] pub(crate) struct StatementHandle(NonNull); @@ -393,15 +391,17 @@ impl StatementHandle { pub(crate) fn step(&mut self) -> Result { // SAFETY: we have exclusive access to the handle unsafe { + #[cfg_attr(not(feature = "unlock-notify"), expect(clippy::never_loop))] loop { match sqlite3_step(self.0.as_ptr()) { SQLITE_ROW => return Ok(true), SQLITE_DONE => return Ok(false), SQLITE_MISUSE => panic!("misuse!"), - SQLITE_LOCKED_SHAREDCACHE => { + #[cfg(feature = "unlock-notify")] + libsqlite3_sys::SQLITE_LOCKED_SHAREDCACHE => { // The shared cache is locked by another connection. Wait for unlock // notification and try again. - unlock_notify::wait(self.db_handle())?; + super::unlock_notify::wait(self.db_handle())?; // Need to reset the handle after the unlock // (https://www.sqlite.org/unlock_notify.html) sqlite3_reset(self.0.as_ptr()); diff --git a/sqlx-sqlite/src/statement/mod.rs b/sqlx-sqlite/src/statement/mod.rs index ff7d841ab1..407c567045 100644 --- a/sqlx-sqlite/src/statement/mod.rs +++ b/sqlx-sqlite/src/statement/mod.rs @@ -9,6 +9,8 @@ use std::sync::Arc; pub(crate) use sqlx_core::statement::*; mod handle; + +#[cfg(feature = "unlock-notify")] pub(super) mod unlock_notify; mod r#virtual; diff --git a/sqlx-sqlite/src/types/bool.rs b/sqlx-sqlite/src/types/bool.rs index e3533573f3..a229298ff9 100644 --- a/sqlx-sqlite/src/types/bool.rs +++ b/sqlx-sqlite/src/types/bool.rs @@ -28,6 +28,6 @@ impl<'q> Encode<'q, Sqlite> for bool { impl<'r> Decode<'r, Sqlite> for bool { fn decode(value: SqliteValueRef<'r>) -> Result { - Ok(value.int64() != 0) + Ok(value.int64()? != 0) } } diff --git a/sqlx-sqlite/src/types/bytes.rs b/sqlx-sqlite/src/types/bytes.rs index af625579e0..2d67335a52 100644 --- a/sqlx-sqlite/src/types/bytes.rs +++ b/sqlx-sqlite/src/types/bytes.rs @@ -32,7 +32,7 @@ impl<'q> Encode<'q, Sqlite> for &'q [u8] { impl<'r> Decode<'r, Sqlite> for &'r [u8] { fn decode(value: SqliteValueRef<'r>) -> Result { - Ok(value.blob()) + Ok(value.blob_borrowed()) } } @@ -84,7 +84,7 @@ impl<'q> Encode<'q, Sqlite> for Vec { impl<'r> Decode<'r, Sqlite> for Vec { fn decode(value: SqliteValueRef<'r>) -> Result { - Ok(value.blob().to_owned()) + Ok(value.blob_owned()) } } diff --git a/sqlx-sqlite/src/types/chrono.rs b/sqlx-sqlite/src/types/chrono.rs index 7424720444..5c4a41caff 100644 --- a/sqlx-sqlite/src/types/chrono.rs +++ b/sqlx-sqlite/src/types/chrono.rs @@ -108,9 +108,9 @@ impl<'r> Decode<'r, Sqlite> for DateTime { fn decode_datetime(value: SqliteValueRef<'_>) -> Result, BoxDynError> { let dt = match value.type_info().0 { - DataType::Text => decode_datetime_from_text(value.text()?), - DataType::Int4 | DataType::Integer => decode_datetime_from_int(value.int64()), - DataType::Float => decode_datetime_from_float(value.double()), + DataType::Text => decode_datetime_from_text(value.text_borrowed()?), + DataType::Int4 | DataType::Integer => decode_datetime_from_int(value.int64()?), + DataType::Float => decode_datetime_from_float(value.double()?), _ => None, }; @@ -118,7 +118,7 @@ fn decode_datetime(value: SqliteValueRef<'_>) -> Result, B if let Some(dt) = dt { Ok(dt) } else { - Err(format!("invalid datetime: {}", value.text()?).into()) + Err(format!("invalid datetime: {}", value.text_borrowed()?).into()) } } @@ -191,13 +191,13 @@ impl<'r> Decode<'r, Sqlite> for NaiveDateTime { impl<'r> Decode<'r, Sqlite> for NaiveDate { fn decode(value: SqliteValueRef<'r>) -> Result { - Ok(NaiveDate::parse_from_str(value.text()?, "%F")?) + Ok(NaiveDate::parse_from_str(value.text_borrowed()?, "%F")?) } } impl<'r> Decode<'r, Sqlite> for NaiveTime { fn decode(value: SqliteValueRef<'r>) -> Result { - let value = value.text()?; + let value = value.text_borrowed()?; // Loop over common time patterns, inspired by Diesel // https://github.com/diesel-rs/diesel/blob/93ab183bcb06c69c0aee4a7557b6798fd52dd0d8/diesel/src/sqlite/types/date_and_time/chrono.rs#L29-L47 diff --git a/sqlx-sqlite/src/types/float.rs b/sqlx-sqlite/src/types/float.rs index 79224f5451..c6e105d783 100644 --- a/sqlx-sqlite/src/types/float.rs +++ b/sqlx-sqlite/src/types/float.rs @@ -26,7 +26,7 @@ impl<'r> Decode<'r, Sqlite> for f32 { fn decode(value: SqliteValueRef<'r>) -> Result { // Truncation is intentional #[allow(clippy::cast_possible_truncation)] - Ok(value.double() as f32) + Ok(value.double()? as f32) } } @@ -49,6 +49,6 @@ impl<'q> Encode<'q, Sqlite> for f64 { impl<'r> Decode<'r, Sqlite> for f64 { fn decode(value: SqliteValueRef<'r>) -> Result { - Ok(value.double()) + Ok(value.double()?) } } diff --git a/sqlx-sqlite/src/types/int.rs b/sqlx-sqlite/src/types/int.rs index 2da9dddd42..e87025e2fb 100644 --- a/sqlx-sqlite/src/types/int.rs +++ b/sqlx-sqlite/src/types/int.rs @@ -32,7 +32,7 @@ impl<'r> Decode<'r, Sqlite> for i8 { // which leads to bugs, e.g.: // https://github.com/launchbadge/sqlx/issues/3179 // Similar bug in Postgres: https://github.com/launchbadge/sqlx/issues/3161 - Ok(value.int64().try_into()?) + Ok(value.int64()?.try_into()?) } } @@ -59,7 +59,7 @@ impl<'q> Encode<'q, Sqlite> for i16 { impl<'r> Decode<'r, Sqlite> for i16 { fn decode(value: SqliteValueRef<'r>) -> Result { - Ok(value.int64().try_into()?) + Ok(value.int64()?.try_into()?) } } @@ -86,7 +86,7 @@ impl<'q> Encode<'q, Sqlite> for i32 { impl<'r> Decode<'r, Sqlite> for i32 { fn decode(value: SqliteValueRef<'r>) -> Result { - Ok(value.int64().try_into()?) + Ok(value.int64()?.try_into()?) } } @@ -113,6 +113,6 @@ impl<'q> Encode<'q, Sqlite> for i64 { impl<'r> Decode<'r, Sqlite> for i64 { fn decode(value: SqliteValueRef<'r>) -> Result { - Ok(value.int64()) + Ok(value.int64()?) } } diff --git a/sqlx-sqlite/src/types/json.rs b/sqlx-sqlite/src/types/json.rs index cbb1542460..b8b665c4d3 100644 --- a/sqlx-sqlite/src/types/json.rs +++ b/sqlx-sqlite/src/types/json.rs @@ -30,6 +30,7 @@ where T: 'r + Deserialize<'r>, { fn decode(value: SqliteValueRef<'r>) -> Result { - Self::decode_from_string(Decode::::decode(value)?) + // Saves a pass over the data by making `serde_json` check UTF-8. + Self::decode_from_bytes(Decode::::decode(value)?) } } diff --git a/sqlx-sqlite/src/types/str.rs b/sqlx-sqlite/src/types/str.rs index 8acd0d8798..5392f6401a 100644 --- a/sqlx-sqlite/src/types/str.rs +++ b/sqlx-sqlite/src/types/str.rs @@ -28,7 +28,7 @@ impl<'q> Encode<'q, Sqlite> for &'q str { impl<'r> Decode<'r, Sqlite> for &'r str { fn decode(value: SqliteValueRef<'r>) -> Result { - value.text() + Ok(value.text_borrowed()?) } } @@ -76,7 +76,7 @@ impl<'q> Encode<'q, Sqlite> for String { impl<'r> Decode<'r, Sqlite> for String { fn decode(value: SqliteValueRef<'r>) -> Result { - value.text().map(ToOwned::to_owned) + Ok(value.text_owned()?) } } diff --git a/sqlx-sqlite/src/types/text.rs b/sqlx-sqlite/src/types/text.rs index 5c8f71eb2a..80aab0d4ba 100644 --- a/sqlx-sqlite/src/types/text.rs +++ b/sqlx-sqlite/src/types/text.rs @@ -31,7 +31,6 @@ where BoxDynError: From<::Err>, { fn decode(value: SqliteValueRef<'r>) -> Result { - let s: &str = Decode::::decode(value)?; - Ok(Self(s.parse()?)) + Ok(Self(value.with_temp_text(|text| text.parse::())??)) } } diff --git a/sqlx-sqlite/src/types/time.rs b/sqlx-sqlite/src/types/time.rs index fb01c5d0d5..c7ad3b3d05 100644 --- a/sqlx-sqlite/src/types/time.rs +++ b/sqlx-sqlite/src/types/time.rs @@ -95,13 +95,16 @@ impl<'r> Decode<'r, Sqlite> for PrimitiveDateTime { impl<'r> Decode<'r, Sqlite> for Date { fn decode(value: SqliteValueRef<'r>) -> Result { - Ok(Date::parse(value.text()?, &fd!("[year]-[month]-[day]"))?) + Ok(Date::parse( + value.text_borrowed()?, + &fd!("[year]-[month]-[day]"), + )?) } } impl<'r> Decode<'r, Sqlite> for Time { fn decode(value: SqliteValueRef<'r>) -> Result { - let value = value.text()?; + let value = value.text_borrowed()?; let sqlite_time_formats = &[ fd!("[hour]:[minute]:[second].[subsecond]"), @@ -121,9 +124,9 @@ impl<'r> Decode<'r, Sqlite> for Time { fn decode_offset_datetime(value: SqliteValueRef<'_>) -> Result { let dt = match value.type_info().0 { - DataType::Text => decode_offset_datetime_from_text(value.text()?), + DataType::Text => decode_offset_datetime_from_text(value.text_borrowed()?), DataType::Int4 | DataType::Integer => { - Some(OffsetDateTime::from_unix_timestamp(value.int64())?) + Some(OffsetDateTime::from_unix_timestamp(value.int64()?)?) } _ => None, @@ -132,7 +135,7 @@ fn decode_offset_datetime(value: SqliteValueRef<'_>) -> Result Option { fn decode_datetime(value: SqliteValueRef<'_>) -> Result { let dt = match value.type_info().0 { - DataType::Text => decode_datetime_from_text(value.text()?), + DataType::Text => decode_datetime_from_text(value.text_borrowed()?), DataType::Int4 | DataType::Integer => { - let parsed = OffsetDateTime::from_unix_timestamp(value.int64()).unwrap(); + let parsed = OffsetDateTime::from_unix_timestamp(value.int64()?).unwrap(); Some(PrimitiveDateTime::new(parsed.date(), parsed.time())) } @@ -166,7 +169,7 @@ fn decode_datetime(value: SqliteValueRef<'_>) -> Result Decode<'r, Sqlite> for u8 { // which leads to bugs, e.g.: // https://github.com/launchbadge/sqlx/issues/3179 // Similar bug in Postgres: https://github.com/launchbadge/sqlx/issues/3161 - Ok(value.int64().try_into()?) + Ok(value.int64()?.try_into()?) } } @@ -59,7 +59,7 @@ impl<'q> Encode<'q, Sqlite> for u16 { impl<'r> Decode<'r, Sqlite> for u16 { fn decode(value: SqliteValueRef<'r>) -> Result { - Ok(value.int64().try_into()?) + Ok(value.int64()?.try_into()?) } } @@ -86,7 +86,7 @@ impl<'q> Encode<'q, Sqlite> for u32 { impl<'r> Decode<'r, Sqlite> for u32 { fn decode(value: SqliteValueRef<'r>) -> Result { - Ok(value.int64().try_into()?) + Ok(value.int64()?.try_into()?) } } @@ -102,6 +102,6 @@ impl Type for u64 { impl<'r> Decode<'r, Sqlite> for u64 { fn decode(value: SqliteValueRef<'r>) -> Result { - Ok(value.int64().try_into()?) + Ok(value.int64()?.try_into()?) } } diff --git a/sqlx-sqlite/src/types/uuid.rs b/sqlx-sqlite/src/types/uuid.rs index faaa69f9aa..99291be86e 100644 --- a/sqlx-sqlite/src/types/uuid.rs +++ b/sqlx-sqlite/src/types/uuid.rs @@ -36,7 +36,7 @@ impl<'q> Encode<'q, Sqlite> for Uuid { impl Decode<'_, Sqlite> for Uuid { fn decode(value: SqliteValueRef<'_>) -> Result { // construct a Uuid from the returned bytes - Uuid::from_slice(value.blob()).map_err(Into::into) + Uuid::from_slice(value.blob_borrowed()).map_err(Into::into) } } @@ -60,7 +60,7 @@ impl<'q> Encode<'q, Sqlite> for Hyphenated { impl Decode<'_, Sqlite> for Hyphenated { fn decode(value: SqliteValueRef<'_>) -> Result { let uuid: Result = - Uuid::parse_str(&value.text().map(ToOwned::to_owned)?).map_err(Into::into); + Uuid::parse_str(&value.text_borrowed().map(ToOwned::to_owned)?).map_err(Into::into); Ok(uuid?.hyphenated()) } @@ -86,7 +86,7 @@ impl<'q> Encode<'q, Sqlite> for Simple { impl Decode<'_, Sqlite> for Simple { fn decode(value: SqliteValueRef<'_>) -> Result { let uuid: Result = - Uuid::parse_str(&value.text().map(ToOwned::to_owned)?).map_err(Into::into); + Uuid::parse_str(&value.text_borrowed().map(ToOwned::to_owned)?).map_err(Into::into); Ok(uuid?.simple()) } diff --git a/sqlx-sqlite/src/value.rs b/sqlx-sqlite/src/value.rs index dc40f29ccb..c66a2501ae 100644 --- a/sqlx-sqlite/src/value.rs +++ b/sqlx-sqlite/src/value.rs @@ -1,156 +1,349 @@ use std::borrow::Cow; -use std::marker::PhantomData; +use std::cell::OnceCell; use std::ptr::NonNull; -use std::slice::from_raw_parts; -use std::str::from_utf8; -use std::sync::Arc; +use std::slice; +use std::str; use libsqlite3_sys::{ sqlite3_value, sqlite3_value_blob, sqlite3_value_bytes, sqlite3_value_double, - sqlite3_value_dup, sqlite3_value_free, sqlite3_value_int64, sqlite3_value_type, SQLITE_NULL, + sqlite3_value_dup, sqlite3_value_free, sqlite3_value_int64, sqlite3_value_type, }; - +use sqlx_core::type_info::TypeInfo; pub(crate) use sqlx_core::value::{Value, ValueRef}; -use crate::error::BoxDynError; use crate::type_info::DataType; -use crate::{Sqlite, SqliteTypeInfo}; +use crate::{Sqlite, SqliteError, SqliteTypeInfo}; + +/// An owned handle to a [`sqlite3_value`]. +/// +/// # Note: Decoding is Stateful +/// The [`sqlite3_value` interface][value-methods] reserves the right to be stateful: +/// +/// > Other interfaces might change the datatype for an sqlite3_value object. +/// > For example, if the datatype is initially SQLITE_INTEGER and sqlite3_value_text(V) is called +/// > to extract a text value for that integer, then subsequent calls to sqlite3_value_type(V) +/// > might return SQLITE_TEXT. Whether or not a persistent internal datatype conversion occurs is +/// > undefined and may change from one release of SQLite to the next. +/// +/// Thus, this type is `!Sync` and [`SqliteValueRef`] is `!Send` and `!Sync` to prevent data races. +/// +/// Additionally, this statefulness means that the return values of `sqlite3_value_bytes()` and +/// `sqlite3_value_blob()` could be invalidated by later calls to other `sqlite3_value*` methods. +/// +/// To prevent undefined behavior from accessing dangling pointers, this type (and any +/// [`SqliteValueRef`] instances created from it) remembers when it was used to decode a +/// borrowed `&[u8]` or `&str` and returns an error if it is used to decode any other type. +/// +/// To bypass this error, you must prove that no outstanding borrows exist. +/// +/// This may be done in one of a few ways: +/// * If you hold mutable access, call [`Self::reset_borrow()`] which resets the borrowed state. +/// * If you have an immutable reference, call [`Self::clone()`] to get a new instance +/// with no outstanding borrows. +/// * If you hold a [`SqliteValueRef`], call [`SqliteValueRef::to_owned()`] +/// to get a new `SqliteValue` with no outstanding borrows. +/// +/// This is *only* necessary if using the same `SqliteValue` or [`SqliteValueRef`] to decode +/// multiple different types. The vast majority of use-cases employing once-through decoding +/// should not have to worry about this. +/// +/// [`sqlite3_value`]: https://www.sqlite.org/c3ref/value.html +/// [value-methods]: https://www.sqlite.org/c3ref/value_blob.html +pub struct SqliteValue(ValueHandle); + +/// A borrowed reference to a [`sqlite3_value`]. +/// +/// Semantically, this behaves as a reference to [`SqliteValue`]. +/// +/// # Note: Decoding is Stateful +/// See [`SqliteValue`] for details. +pub struct SqliteValueRef<'r>(Cow<'r, ValueHandle>); + +impl SqliteValue { + // SAFETY: The sqlite3_value must be non-null and SQLite must not free it. It will be freed on drop. + pub(crate) unsafe fn dup( + value: *mut sqlite3_value, + column_type: Option, + ) -> Self { + debug_assert!(!value.is_null()); + let handle = ValueHandle::try_dup_of(value, column_type) + .expect("SQLite failed to allocate memory for duplicated value"); + Self(handle) + } + + /// Prove that there are no outstanding borrows of this instance. + /// + /// Call this after decoding a borrowed `&[u8]` or `&str` + /// to reset the internal borrowed state and allow decoding of other types. + pub fn reset_borrow(&mut self) { + self.0.reset_blob_borrow(); + } -enum SqliteValueData<'r> { - Value(&'r SqliteValue), - BorrowedHandle(ValueHandle<'r>), + /// Call [`sqlite3_value_dup()`] to create a new instance of this type. + /// + /// Returns an error if the call returns a null pointer, indicating that + /// SQLite was unable to allocate the additional memory required. + /// + /// Non-panicking version of [`Self::clone()`]. + /// + /// [`sqlite3_value_dup()`]: https://www.sqlite.org/c3ref/value_dup.html + pub fn try_clone(&self) -> Result { + self.0.try_dup().map(Self) + } } -pub struct SqliteValueRef<'r>(SqliteValueData<'r>); +impl Clone for SqliteValue { + /// Call [`sqlite3_value_dup()`] to create a new instance of this type. + /// + /// # Panics + /// If [`sqlite3_value_dup()`] returns a null pointer, indicating an out-of-memory condition. + /// + /// See [`Self::try_clone()`] for a non-panicking version. + /// + /// [`sqlite3_value_dup()`]: https://www.sqlite.org/c3ref/value_dup.html + fn clone(&self) -> Self { + self.try_clone().expect("failed to clone `SqliteValue`") + } +} + +impl Value for SqliteValue { + type Database = Sqlite; + + fn as_ref(&self) -> SqliteValueRef<'_> { + SqliteValueRef::value(self) + } + + fn type_info(&self) -> Cow<'_, SqliteTypeInfo> { + Cow::Owned(self.0.type_info()) + } + + fn is_null(&self) -> bool { + self.0.is_null() + } +} impl<'r> SqliteValueRef<'r> { + /// Attempt to duplicate the internal `sqlite3_value` with [`sqlite3_value_dup()`]. + /// + /// Returns an error if the call returns a null pointer, indicating that + /// SQLite was unable to allocate the additional memory required. + /// + /// Non-panicking version of [`Self::try_to_owned()`]. + /// + /// [`sqlite3_value_dup()`]: https://www.sqlite.org/c3ref/value_dup.html + pub fn try_to_owned(&self) -> Result { + self.0.try_dup().map(SqliteValue) + } + pub(crate) fn value(value: &'r SqliteValue) -> Self { - Self(SqliteValueData::Value(value)) + Self(Cow::Borrowed(&value.0)) } - // SAFETY: The supplied sqlite3_value must not be null and SQLite must free it. It will not be freed on drop. - // The lifetime on this struct should tie it to whatever scope it's valid for before SQLite frees it. + /// # Safety + /// The supplied sqlite3_value must not be null and SQLite must free it. + /// It will not be freed on drop. + /// The lifetime on this struct should tie it to whatever scope it's valid for before SQLite frees it. #[allow(unused)] - pub(crate) unsafe fn borrowed(value: *mut sqlite3_value, type_info: SqliteTypeInfo) -> Self { + pub(crate) unsafe fn borrowed(value: *mut sqlite3_value) -> Self { debug_assert!(!value.is_null()); - let handle = ValueHandle::new_borrowed(NonNull::new_unchecked(value), type_info); - Self(SqliteValueData::BorrowedHandle(handle)) + let handle = ValueHandle::temporary(NonNull::new_unchecked(value)); + Self(Cow::Owned(handle)) } // NOTE: `int()` is deliberately omitted because it will silently truncate a wider value, // which is likely to cause bugs: // https://github.com/launchbadge/sqlx/issues/3179 // (Similar bug in Postgres): https://github.com/launchbadge/sqlx/issues/3161 - pub(super) fn int64(&self) -> i64 { - match &self.0 { - SqliteValueData::Value(v) => v.0.int64(), - SqliteValueData::BorrowedHandle(v) => v.int64(), - } + pub(super) fn int64(&self) -> Result { + self.0.int64() } - pub(super) fn double(&self) -> f64 { - match &self.0 { - SqliteValueData::Value(v) => v.0.double(), - SqliteValueData::BorrowedHandle(v) => v.double(), - } + pub(super) fn double(&self) -> Result { + self.0.double() } - pub(super) fn blob(&self) -> &'r [u8] { - match &self.0 { - SqliteValueData::Value(v) => v.0.blob(), - SqliteValueData::BorrowedHandle(v) => v.blob(), - } + pub(super) fn blob_borrowed(&self) -> &'r [u8] { + // SAFETY: lifetime is matched to `'r` + unsafe { self.0.blob_borrowed() } } - pub(super) fn text(&self) -> Result<&'r str, BoxDynError> { - match &self.0 { - SqliteValueData::Value(v) => v.0.text(), - SqliteValueData::BorrowedHandle(v) => v.text(), - } + pub(super) fn with_temp_blob(&self, op: impl FnOnce(&[u8]) -> R) -> R { + self.0.with_blob(op) + } + + pub(super) fn blob_owned(&self) -> Vec { + self.with_temp_blob(|blob| blob.to_vec()) + } + + pub(super) fn text_borrowed(&self) -> Result<&'r str, str::Utf8Error> { + // SAFETY: lifetime is matched to `'r` + unsafe { self.0.text_borrowed() } + } + + pub(super) fn with_temp_text( + &self, + op: impl FnOnce(&str) -> R, + ) -> Result { + self.0.with_blob(|blob| str::from_utf8(blob).map(op)) + } + + pub(super) fn text_owned(&self) -> Result { + self.with_temp_text(|text| text.to_string()) } } impl<'r> ValueRef<'r> for SqliteValueRef<'r> { type Database = Sqlite; + /// Attempt to duplicate the internal `sqlite3_value` with [`sqlite3_value_dup()`]. + /// + /// # Panics + /// If [`sqlite3_value_dup()`] returns a null pointer, indicating an out-of-memory condition. + /// + /// See [`Self::try_to_owned()`] for a non-panicking version. + /// + /// [`sqlite3_value_dup()`]: https://www.sqlite.org/c3ref/value_dup.html fn to_owned(&self) -> SqliteValue { - match &self.0 { - SqliteValueData::Value(v) => (*v).clone(), - SqliteValueData::BorrowedHandle(v) => unsafe { - SqliteValue::new(v.value.as_ptr(), v.type_info.clone()) - }, - } + SqliteValue( + self.0 + .try_dup() + .expect("failed to convert SqliteValueRef to owned SqliteValue"), + ) } fn type_info(&self) -> Cow<'_, SqliteTypeInfo> { - match &self.0 { - SqliteValueData::Value(v) => v.type_info(), - SqliteValueData::BorrowedHandle(v) => v.type_info(), - } + Cow::Owned(self.0.type_info()) } fn is_null(&self) -> bool { - match &self.0 { - SqliteValueData::Value(v) => v.is_null(), - SqliteValueData::BorrowedHandle(v) => v.is_null(), - } + self.0.is_null() } } -#[derive(Clone)] -pub struct SqliteValue(Arc>); - -pub(crate) struct ValueHandle<'a> { +pub(crate) struct ValueHandle { value: NonNull, - type_info: SqliteTypeInfo, + column_type: Option, + // Note: `std::cell` version + borrowed_blob: OnceCell, free_on_drop: bool, - _sqlite_value_lifetime: PhantomData<&'a ()>, } +struct Blob { + ptr: *const u8, + len: usize, +} + +#[derive(Debug, thiserror::Error)] +#[error("given `SqliteValue` was previously decoded as BLOB or TEXT; `SqliteValue::reset_borrow()` must be called first")] +pub(crate) struct BorrowedBlobError; + // SAFE: only protected value objects are stored in SqliteValue -unsafe impl Send for ValueHandle<'_> {} -unsafe impl Sync for ValueHandle<'_> {} +unsafe impl Send for ValueHandle {} -impl ValueHandle<'static> { - fn new_owned(value: NonNull, type_info: SqliteTypeInfo) -> Self { - Self { +// SAFETY: the `sqlite3_value_*()` methods reserve the right to be stateful, +// which means method calls aren't thread-safe without mutual exclusion. +// +// impl !Sync for ValueHandle {} + +impl ValueHandle { + /// # Safety + /// The `sqlite3_value` must be valid and SQLite must not free it. It will be freed on drop. + unsafe fn try_dup_of( + value: *mut sqlite3_value, + column_type: Option, + ) -> Result { + // SAFETY: caller must ensure `value` is valid. + let value = + unsafe { NonNull::new(sqlite3_value_dup(value)).ok_or_else(SqliteError::nomem)? }; + + Ok(Self { value, - type_info, + column_type, + borrowed_blob: OnceCell::new(), free_on_drop: true, - _sqlite_value_lifetime: PhantomData, - } + }) } -} -impl ValueHandle<'_> { - fn new_borrowed(value: NonNull, type_info: SqliteTypeInfo) -> Self { + fn temporary(value: NonNull) -> Self { Self { value, - type_info, + column_type: None, + borrowed_blob: OnceCell::new(), free_on_drop: false, - _sqlite_value_lifetime: PhantomData, } } - fn type_info_opt(&self) -> Option { - let dt = DataType::from_code(unsafe { sqlite3_value_type(self.value.as_ptr()) }); + fn try_dup(&self) -> Result { + // SAFETY: `value` is initialized + unsafe { Self::try_dup_of(self.value.as_ptr(), self.column_type.clone()) } + } - if let DataType::Null = dt { - None - } else { - Some(SqliteTypeInfo(dt)) + fn type_info(&self) -> SqliteTypeInfo { + let value_type = SqliteTypeInfo(DataType::from_code(unsafe { + sqlite3_value_type(self.value.as_ptr()) + })); + + // Assume the actual value type is more accurate, if it's not NULL. + match &self.column_type { + Some(column_type) if value_type.is_null() => column_type.clone(), + _ => value_type, + } + } + + fn int64(&self) -> Result { + // SAFETY: we have to be certain the caller isn't still holding a borrow from `.blob_borrowed()` + self.assert_blob_not_borrowed()?; + + Ok(unsafe { sqlite3_value_int64(self.value.as_ptr()) }) + } + + fn double(&self) -> Result { + // SAFETY: we have to be certain the caller isn't still holding a borrow from `.blob_borrowed()` + self.assert_blob_not_borrowed()?; + + Ok(unsafe { sqlite3_value_double(self.value.as_ptr()) }) + } + + fn is_null(&self) -> bool { + self.type_info().is_null() + } +} + +impl Clone for ValueHandle { + fn clone(&self) -> Self { + self.try_dup().unwrap() + } +} + +impl Drop for ValueHandle { + fn drop(&mut self) { + if self.free_on_drop { + unsafe { + sqlite3_value_free(self.value.as_ptr()); + } } } +} - fn int64(&self) -> i64 { - unsafe { sqlite3_value_int64(self.value.as_ptr()) } +impl ValueHandle { + fn assert_blob_not_borrowed(&self) -> Result<(), BorrowedBlobError> { + if self.borrowed_blob.get().is_none() { + Ok(()) + } else { + Err(BorrowedBlobError) + } } - fn double(&self) -> f64 { - unsafe { sqlite3_value_double(self.value.as_ptr()) } + fn reset_blob_borrow(&mut self) { + self.borrowed_blob.take(); } - fn blob<'b>(&self) -> &'b [u8] { + fn get_blob(&self) -> Option { + if let Some(blob) = self.borrowed_blob.get() { + return Some(Blob { ..*blob }); + } + + // SAFETY: calling `sqlite3_value_bytes` from multiple threads at once is a data race. let len = unsafe { sqlite3_value_bytes(self.value.as_ptr()) }; // This likely means UB in SQLite itself or our usage of it; @@ -160,85 +353,60 @@ impl ValueHandle<'_> { }); if len == 0 { - // empty blobs are NULL so just return an empty slice - return &[]; + // empty blobs are NULL + return None; } let ptr = unsafe { sqlite3_value_blob(self.value.as_ptr()) } as *const u8; debug_assert!(!ptr.is_null()); - unsafe { from_raw_parts(ptr, len) } + Some(Blob { ptr, len }) } - fn text<'b>(&self) -> Result<&'b str, BoxDynError> { - Ok(from_utf8(self.blob())?) - } + fn with_blob(&self, with_blob: impl FnOnce(&[u8]) -> R) -> R { + let Some(blob) = self.get_blob() else { + return with_blob(&[]); + }; - fn type_info(&self) -> Cow<'_, SqliteTypeInfo> { - self.type_info_opt() - .map(Cow::Owned) - .unwrap_or(Cow::Borrowed(&self.type_info)) + // SAFETY: the slice cannot outlive the call + with_blob(unsafe { blob.as_slice() }) } - fn is_null(&self) -> bool { - unsafe { sqlite3_value_type(self.value.as_ptr()) == SQLITE_NULL } - } -} + /// # Safety + /// Caller must ensure lifetime '`b` cannot outlive `self`. + unsafe fn blob_borrowed<'a>(&self) -> &'a [u8] { + let Some(blob) = self.get_blob() else { + return &[]; + }; -impl Drop for ValueHandle<'_> { - fn drop(&mut self) { - if self.free_on_drop { - unsafe { - sqlite3_value_free(self.value.as_ptr()); - } - } - } -} + // SAFETY: we need to store that the blob was borrowed + // to prevent + let blob = self.borrowed_blob.get_or_init(|| blob); -impl SqliteValue { - // SAFETY: The sqlite3_value must be non-null and SQLite must not free it. It will be freed on drop. - pub(crate) unsafe fn new(value: *mut sqlite3_value, type_info: SqliteTypeInfo) -> Self { - debug_assert!(!value.is_null()); - let handle = - ValueHandle::new_owned(NonNull::new_unchecked(sqlite3_value_dup(value)), type_info); - Self(Arc::new(handle)) + unsafe { blob.as_slice() } } -} -impl Value for SqliteValue { - type Database = Sqlite; + /// # Safety + /// Caller must ensure lifetime '`b` cannot outlive `self`. + unsafe fn text_borrowed<'b>(&self) -> Result<&'b str, str::Utf8Error> { + let Some(blob) = self.get_blob() else { + return Ok(""); + }; - fn as_ref(&self) -> SqliteValueRef<'_> { - SqliteValueRef::value(self) - } + // SAFETY: lifetime of `blob` will be tied to `'b`. + let s = str::from_utf8(unsafe { blob.as_slice() })?; - fn type_info(&self) -> Cow<'_, SqliteTypeInfo> { - self.0.type_info() - } + // We only store the borrow after we ensure the string is valid. + self.borrowed_blob.set(blob).ok(); - fn is_null(&self) -> bool { - self.0.is_null() + Ok(s) } } -// #[cfg(feature = "any")] -// impl<'r> From> for crate::any::AnyValueRef<'r> { -// #[inline] -// fn from(value: SqliteValueRef<'r>) -> Self { -// crate::any::AnyValueRef { -// type_info: value.type_info().clone().into_owned().into(), -// kind: crate::any::value::AnyValueRefKind::Sqlite(value), -// } -// } -// } -// -// #[cfg(feature = "any")] -// impl From for crate::any::AnyValue { -// #[inline] -// fn from(value: SqliteValue) -> Self { -// crate::any::AnyValue { -// type_info: value.type_info().clone().into_owned().into(), -// kind: crate::any::value::AnyValueKind::Sqlite(value), -// } -// } -// } +impl Blob { + /// # Safety + /// `'a` must not outlive the `sqlite3_value` this blob came from. + unsafe fn as_slice<'a>(&self) -> &'a [u8] { + slice::from_raw_parts(self.ptr, self.len) + } +} diff --git a/src/lib.md b/src/lib.md index 7fc5b899a7..ea8fae4ed0 100644 --- a/src/lib.md +++ b/src/lib.md @@ -20,7 +20,7 @@ which is useful for libraries building on top of it, **the use of nearly any async function in the API will panic without at least one runtime feature enabled**. The chief exception is the SQLite driver, which is runtime-agnostic, including its integration with the query macros. -However, [`SqlitePool`][crate::sqlite::SqlitePool] _does_ require runtime support for timeouts and spawning +However, [`SqlitePool`] _does_ require runtime support for timeouts and spawning internal management tasks. ### TLS Support diff --git a/src/lib.rs b/src/lib.rs index 5a21c3c98a..438463210d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -175,8 +175,7 @@ pub mod prelude { pub use super::Type; } -#[cfg(feature = "_unstable-doc")] -#[cfg_attr(docsrs, doc(cfg(feature = "_unstable-doc")))] +#[cfg(feature = "_unstable-docs")] pub use sqlx_core::config as _config; // NOTE: APIs exported in this module are SemVer-exempt. diff --git a/tests/sqlite/sqlite.rs b/tests/sqlite/sqlite.rs index b9bc6320f8..99626c28a1 100644 --- a/tests/sqlite/sqlite.rs +++ b/tests/sqlite/sqlite.rs @@ -214,7 +214,12 @@ async fn it_executes_with_pool() -> anyhow::Result<()> { async fn it_opens_with_extension() -> anyhow::Result<()> { use std::str::FromStr; - let opts = SqliteConnectOptions::from_str(&dotenvy::var("DATABASE_URL")?)?.extension("ipaddr"); + let mut opts = SqliteConnectOptions::from_str(&dotenvy::var("DATABASE_URL")?)?; + + // SAFETY: the `sqlite_ipaddr` cfg is only enabled when we want to test this. + unsafe { + opts = opts.extension("ipaddr"); + } let mut conn = SqliteConnection::connect_with(&opts).await?; conn.execute("SELECT ipmasklen('192.168.16.12/24');")