diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /storage/rust | |
parent | Initial commit. (diff) | |
download | firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'storage/rust')
-rw-r--r-- | storage/rust/Cargo.toml | 14 | ||||
-rw-r--r-- | storage/rust/src/lib.rs | 533 |
2 files changed, 547 insertions, 0 deletions
diff --git a/storage/rust/Cargo.toml b/storage/rust/Cargo.toml new file mode 100644 index 0000000000..bf65f94146 --- /dev/null +++ b/storage/rust/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "storage" +description = "Rust bindings for mozStorage." +version = "0.1.0" +authors = ["Lina Cambridge <lina@yakshaving.ninja>"] +edition = "2018" +license = "MPL-2.0" + +[dependencies] +libc = "0.2" +nserror = { path = "../../xpcom/rust/nserror" } +nsstring = { path = "../../xpcom/rust/nsstring" } +storage_variant = { path = "../variant" } +xpcom = { path = "../../xpcom/rust/xpcom" } diff --git a/storage/rust/src/lib.rs b/storage/rust/src/lib.rs new file mode 100644 index 0000000000..91984bcafc --- /dev/null +++ b/storage/rust/src/lib.rs @@ -0,0 +1,533 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! A Rust wrapper for mozStorage. +//! +//! mozStorage wraps the SQLite C API with support for XPCOM data structures, +//! asynchronous statement execution, cleanup on shutdown, and connection +//! cloning that propagates attached databases, pragmas, functions, and +//! temporary entities. It also collects timing and memory usage stats for +//! telemetry, and supports detailed statement logging. Additionally, mozStorage +//! makes it possible to use the same connection handle from JS and native +//! (C++ and Rust) code. +//! +//! Most mozStorage objects, like connections, statements, result rows, +//! and variants, are thread-safe. Each connection manages a background +//! thread that can be used to execute statements asynchronously, without +//! blocking the main thread. +//! +//! This crate provides a thin wrapper to make mozStorage easier to use +//! from Rust. It only wraps the synchronous API, so you can either manage +//! the entire connection from a background thread, or use the `moz_task` +//! crate to dispatch tasks to the connection's async thread. Executing +//! synchronous statements on the main thread is not supported, and will +//! assert in debug builds. + +#![allow(non_snake_case)] + +use std::{borrow::Cow, convert::TryFrom, error, fmt, ops::Deref, result}; + +use nserror::{nsresult, NS_ERROR_NO_INTERFACE, NS_ERROR_UNEXPECTED}; +use nsstring::nsCString; +use storage_variant::VariantType; +use xpcom::{ + getter_addrefs, + interfaces::{ + mozIStorageAsyncConnection, mozIStorageConnection, mozIStorageStatement, nsIEventTarget, + nsIThread, + }, + RefPtr, XpCom, +}; + +const SQLITE_OK: i32 = 0; + +pub type Result<T> = result::Result<T, Error>; + +/// `Conn` wraps a `mozIStorageConnection`. +#[derive(Clone)] +pub struct Conn { + handle: RefPtr<mozIStorageConnection>, +} + +// This is safe as long as our `mozIStorageConnection` is an instance of +// `mozilla::storage::Connection`, which is atomically reference counted. +unsafe impl Send for Conn {} +unsafe impl Sync for Conn {} + +impl Conn { + /// Wraps a `mozIStorageConnection` in a `Conn`. + #[inline] + pub fn wrap(connection: RefPtr<mozIStorageConnection>) -> Conn { + Conn { handle: connection } + } + + /// Returns the wrapped `mozIStorageConnection`. + #[inline] + pub fn connection(&self) -> &mozIStorageConnection { + &self.handle + } + + /// Returns the maximum number of bound parameters for statements executed + /// on this connection. + pub fn variable_limit(&self) -> Result<usize> { + let mut limit = 0i32; + let rv = unsafe { self.handle.GetVariableLimit(&mut limit) }; + if rv.failed() { + return Err(Error::Limit); + } + usize::try_from(limit).map_err(|_| Error::Limit) + } + + /// Returns the async thread for this connection. This can be used + /// with `moz_task` to run synchronous statements on the storage + /// thread, without blocking the main thread. + pub fn thread(&self) -> Result<RefPtr<nsIThread>> { + let target = self.handle.get_interface::<nsIEventTarget>(); + target + .and_then(|t| t.query_interface::<nsIThread>()) + .ok_or(Error::NoThread) + } + + /// Prepares a SQL statement. `query` should only contain one SQL statement. + /// If `query` contains multiple statements, only the first will be prepared, + /// and the rest will be ignored. + pub fn prepare<Q: AsRef<str>>(&self, query: Q) -> Result<Statement> { + let statement = self.call_and_wrap_error(DatabaseOp::Prepare, || { + getter_addrefs(|p| unsafe { + self.handle + .CreateStatement(&*nsCString::from(query.as_ref()), p) + }) + })?; + Ok(Statement { + conn: self, + handle: statement, + }) + } + + /// Executes a SQL statement. `query` may contain one or more + /// semicolon-separated SQL statements. + pub fn exec<Q: AsRef<str>>(&self, query: Q) -> Result<()> { + self.call_and_wrap_error(DatabaseOp::Exec, || { + unsafe { + self.handle + .ExecuteSimpleSQL(&*nsCString::from(query.as_ref())) + } + .to_result() + }) + } + + /// Opens a transaction with the default transaction behavior for this + /// connection. The transaction should be committed when done. Uncommitted + /// `Transaction`s will automatically roll back when they go out of scope. + pub fn transaction(&mut self) -> Result<Transaction> { + let behavior = self.get_default_transaction_behavior(); + Transaction::new(self, behavior) + } + + /// Indicates if a transaction is currently open on this connection. + /// Attempting to open a new transaction when one is already in progress + /// will fail with a "cannot start a transaction within a transaction" + /// error. + /// + /// Note that this is `true` even if the transaction was started by another + /// caller, like `Sqlite.sys.mjs` or `mozStorageTransaction` from C++. See the + /// explanation above `mozIStorageConnection.transactionInProgress` for why + /// this matters. + pub fn transaction_in_progress(&self) -> Result<bool> { + let mut in_progress = false; + unsafe { self.handle.GetTransactionInProgress(&mut in_progress) }.to_result()?; + Ok(in_progress) + } + + /// Opens a transaction with the requested behavior. + pub fn transaction_with_behavior( + &mut self, + behavior: TransactionBehavior, + ) -> Result<Transaction> { + Transaction::new(self, behavior) + } + + fn get_default_transaction_behavior(&self) -> TransactionBehavior { + let mut typ = 0i32; + let rv = unsafe { self.handle.GetDefaultTransactionType(&mut typ) }; + if rv.failed() { + return TransactionBehavior::Deferred; + } + match typ { + mozIStorageAsyncConnection::TRANSACTION_IMMEDIATE => TransactionBehavior::Immediate, + mozIStorageAsyncConnection::TRANSACTION_EXCLUSIVE => TransactionBehavior::Exclusive, + _ => TransactionBehavior::Deferred, + } + } + + /// Invokes a storage operation and returns the last SQLite error if the + /// operation fails. This lets `Conn::{prepare, exec}` and + /// `Statement::{step, execute}` return more detailed errors, as the + /// `nsresult` codes that mozStorage uses are often too generic. For + /// example, `NS_ERROR_FAILURE` might be anything from a SQL syntax error + /// to an invalid column name in a trigger. + /// + /// Note that the last error may not be accurate if the underlying + /// `mozIStorageConnection` is used concurrently from multiple threads. + /// Multithreaded callers that share a connection should serialize their + /// uses. + fn call_and_wrap_error<T>( + &self, + op: DatabaseOp, + func: impl FnOnce() -> result::Result<T, nsresult>, + ) -> Result<T> { + func().or_else(|rv| -> Result<T> { + let mut code = 0i32; + unsafe { self.handle.GetLastError(&mut code) }.to_result()?; + Err(if code != SQLITE_OK { + let mut message = nsCString::new(); + unsafe { self.handle.GetLastErrorString(&mut *message) }.to_result()?; + Error::Database { + rv, + op, + code, + message, + } + } else { + rv.into() + }) + }) + } +} + +pub enum TransactionBehavior { + Deferred, + Immediate, + Exclusive, +} + +pub struct Transaction<'c> { + conn: &'c mut Conn, + active: bool, +} + +impl<'c> Transaction<'c> { + /// Opens a transaction on `conn` with the given `behavior`. + fn new(conn: &'c mut Conn, behavior: TransactionBehavior) -> Result<Transaction<'c>> { + conn.exec(match behavior { + TransactionBehavior::Deferred => "BEGIN DEFERRED", + TransactionBehavior::Immediate => "BEGIN IMMEDIATE", + TransactionBehavior::Exclusive => "BEGIN EXCLUSIVE", + })?; + Ok(Transaction { conn, active: true }) + } + + /// Commits the transaction. + pub fn commit(mut self) -> Result<()> { + if self.active { + self.conn.exec("COMMIT")?; + self.active = false; + } + Ok(()) + } + + /// Rolls the transaction back. + pub fn rollback(mut self) -> Result<()> { + self.abort() + } + + fn abort(&mut self) -> Result<()> { + if self.active { + self.conn.exec("ROLLBACK")?; + self.active = false; + } + Ok(()) + } +} + +impl<'c> Deref for Transaction<'c> { + type Target = Conn; + + fn deref(&self) -> &Conn { + self.conn + } +} + +impl<'c> Drop for Transaction<'c> { + fn drop(&mut self) { + let _ = self.abort(); + } +} + +pub struct Statement<'c> { + conn: &'c Conn, + handle: RefPtr<mozIStorageStatement>, +} + +impl<'c> Statement<'c> { + /// Binds a parameter at the given `index` to the prepared statement. + /// `value` is any type that can be converted into a `Variant`. + pub fn bind_by_index<V: VariantType>(&mut self, index: u32, value: V) -> Result<()> { + let variant = value.into_variant(); + unsafe { self.handle.BindByIndex(index as u32, variant.coerce()) } + .to_result() + .map_err(|rv| Error::BindByIndex { + rv, + data_type: V::type_name(), + index, + }) + } + + /// Binds a parameter with the given `name` to the prepared statement. + pub fn bind_by_name<N: AsRef<str>, V: VariantType>(&mut self, name: N, value: V) -> Result<()> { + let name = name.as_ref(); + let variant = value.into_variant(); + unsafe { + self.handle + .BindByName(&*nsCString::from(name), variant.coerce()) + } + .to_result() + .map_err(|rv| Error::BindByName { + rv, + data_type: V::type_name(), + name: name.into(), + }) + } + + /// Executes the statement and returns the next row of data. + pub fn step<'s>(&'s mut self) -> Result<Option<Step<'c, 's>>> { + let has_more = self.conn.call_and_wrap_error(DatabaseOp::Step, || { + let mut has_more = false; + unsafe { self.handle.ExecuteStep(&mut has_more) }.to_result()?; + Ok(has_more) + })?; + Ok(if has_more { Some(Step(self)) } else { None }) + } + + /// Executes the statement once, discards any data, and resets the + /// statement. + pub fn execute(&mut self) -> Result<()> { + self.conn.call_and_wrap_error(DatabaseOp::Execute, || { + unsafe { self.handle.Execute() }.to_result() + }) + } + + /// Resets the prepared statement so that it's ready to be executed + /// again, and clears any bound parameters. + pub fn reset(&mut self) -> Result<()> { + unsafe { self.handle.Reset() }.to_result()?; + Ok(()) + } + + fn get_column_index(&self, name: &str) -> Result<u32> { + let mut index = 0u32; + let rv = unsafe { + self.handle + .GetColumnIndex(&*nsCString::from(name), &mut index) + }; + if rv.succeeded() { + Ok(index) + } else { + Err(Error::InvalidColumn { + rv, + name: name.into(), + }) + } + } + + fn get_column_value<T: VariantType>(&self, index: u32) -> result::Result<T, nsresult> { + let variant = getter_addrefs(|p| unsafe { self.handle.GetVariant(index, p) })?; + let value = T::from_variant(variant.coerce())?; + Ok(value) + } +} + +impl<'c> Drop for Statement<'c> { + fn drop(&mut self) { + unsafe { self.handle.Finalize() }; + } +} + +/// A step is the next row in the result set for a statement. +pub struct Step<'c, 's>(&'s mut Statement<'c>); + +impl<'c, 's> Step<'c, 's> { + /// Returns the value of the column at `index` for the current row. + pub fn get_by_index<T: VariantType>(&self, index: u32) -> Result<T> { + self.0 + .get_column_value(index) + .map_err(|rv| Error::GetByIndex { + rv, + data_type: T::type_name(), + index, + }) + } + + /// A convenience wrapper that returns the default value for the column + /// at `index` if `NULL`. + pub fn get_by_index_or_default<T: VariantType + Default>(&self, index: u32) -> T { + self.get_by_index(index).unwrap_or_default() + } + + /// Returns the value of the column specified by `name` for the current row. + pub fn get_by_name<N: AsRef<str>, T: VariantType>(&self, name: N) -> Result<T> { + let name = name.as_ref(); + let index = self.0.get_column_index(name)?; + self.0 + .get_column_value(index) + .map_err(|rv| Error::GetByName { + rv, + data_type: T::type_name(), + name: name.into(), + }) + } + + /// Returns the default value for the column with the given `name`, or the + /// default if the column is `NULL`. + pub fn get_by_name_or_default<N: AsRef<str>, T: VariantType + Default>(&self, name: N) -> T { + self.get_by_name(name).unwrap_or_default() + } +} + +/// A database operation, included for better context in error messages. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DatabaseOp { + Exec, + Prepare, + Step, + Execute, +} + +impl DatabaseOp { + /// Returns a description of the operation to include in an error message. + pub fn what(&self) -> &'static str { + match self { + DatabaseOp::Exec => "execute SQL string", + DatabaseOp::Prepare => "prepare statement", + DatabaseOp::Step => "step statement", + DatabaseOp::Execute => "execute statement", + } + } +} + +/// Storage errors. +#[derive(Debug)] +pub enum Error { + /// A connection doesn't have a usable async thread. The connection might be + /// closed, or the thread manager may have shut down. + NoThread, + + /// Failed to get a limit for a database connection. + Limit, + + /// A database operation failed. The error includes a SQLite result code, + /// and an explanation string. + Database { + rv: nsresult, + op: DatabaseOp, + code: i32, + message: nsCString, + }, + + /// A parameter with the given data type couldn't be bound at this index, + /// likely because the index is out of range. + BindByIndex { + rv: nsresult, + data_type: Cow<'static, str>, + index: u32, + }, + + /// A parameter with the given type couldn't be bound to this name, likely + /// because the statement doesn't have a matching `:`-prefixed parameter + /// with the name. + BindByName { + rv: nsresult, + data_type: Cow<'static, str>, + name: String, + }, + + /// A column with this name doesn't exist. + InvalidColumn { rv: nsresult, name: String }, + + /// A value of the given type couldn't be accessed at this index. This is + /// the error returned when a type conversion fails; for example, requesting + /// an `nsString` instead of an `Option<nsString>` when the column is `NULL`. + GetByIndex { + rv: nsresult, + data_type: Cow<'static, str>, + index: u32, + }, + + /// A value of the given type couldn't be accessed for the column with + /// this name. + GetByName { + rv: nsresult, + data_type: Cow<'static, str>, + name: String, + }, + + /// A storage operation failed for other reasons. + Other(nsresult), +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + None + } +} + +impl From<nsresult> for Error { + fn from(rv: nsresult) -> Error { + Error::Other(rv) + } +} + +impl From<Error> for nsresult { + fn from(err: Error) -> nsresult { + match err { + Error::NoThread => NS_ERROR_NO_INTERFACE, + Error::Limit => NS_ERROR_UNEXPECTED, + Error::Database { rv, .. } + | Error::BindByIndex { rv, .. } + | Error::BindByName { rv, .. } + | Error::InvalidColumn { rv, .. } + | Error::GetByIndex { rv, .. } + | Error::GetByName { rv, .. } + | Error::Other(rv) => rv, + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::NoThread => f.write_str("Async thread unavailable for storage connection"), + Error::Limit => f.write_str("Failed to get limit for storage connection"), + Error::Database { + op, code, message, .. + } => { + if message.is_empty() { + write!(f, "Failed to {} with code {}", op.what(), code) + } else { + write!( + f, + "Failed to {} with code {} ({})", + op.what(), + code, + message + ) + } + } + Error::BindByIndex { + data_type, index, .. + } => write!(f, "Can't bind {} at {}", data_type, index), + Error::BindByName { + data_type, name, .. + } => write!(f, "Can't bind {} to named parameter {}", data_type, name), + Error::InvalidColumn { name, .. } => write!(f, "Column {} doesn't exist", name), + Error::GetByIndex { + data_type, index, .. + } => write!(f, "Can't get {} at {}", data_type, index), + Error::GetByName { + data_type, name, .. + } => write!(f, "Can't get {} for column {}", data_type, name), + Error::Other(rv) => write!(f, "Storage operation failed with {}", rv.error_name()), + } + } +} |