summaryrefslogtreecommitdiffstats
path: root/storage/rust
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /storage/rust
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'storage/rust')
-rw-r--r--storage/rust/Cargo.toml14
-rw-r--r--storage/rust/src/lib.rs533
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()),
+ }
+ }
+}