Bug 1626506 - Vendor the webext_storage component. r=markh

Hooray, our first Application Services Rust component! This is a
mechanical run of `mach vendor rust`, split out into its own commit
to make reviewing the Firefox bindings easier.

Differential Revision: https://phabricator.services.mozilla.com/D71895
This commit is contained in:
Lina Cambridge 2020-04-27 05:40:52 +00:00
parent 807ec47bae
commit e6e665546f
46 changed files with 3039 additions and 7 deletions

View File

@ -20,7 +20,7 @@ tag = "v0.2.4"
[source."https://github.com/mozilla/application-services"]
git = "https://github.com/mozilla/application-services"
replace-with = "vendored-sources"
rev = "120e51dd5f2aab4194cf0f7e93b2a8923f4504bb"
rev = "c17198fa5a88295f2cca722586c539280e10201c"
[source."https://github.com/mozilla-spidermonkey/jsparagus"]
git = "https://github.com/mozilla-spidermonkey/jsparagus"

61
Cargo.lock generated
View File

@ -1238,6 +1238,14 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3"
[[package]]
name = "error-support"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=c17198fa5a88295f2cca722586c539280e10201c#c17198fa5a88295f2cca722586c539280e10201c"
dependencies = [
"failure",
]
[[package]]
name = "euclid"
version = "0.20.8"
@ -1843,6 +1851,7 @@ dependencies = [
"sync15-traits",
"unic-langid",
"unic-langid-ffi",
"webext-storage",
"webrender_bindings",
"wgpu_bindings",
"xpcom",
@ -2148,6 +2157,11 @@ dependencies = [
"adler32",
]
[[package]]
name = "interrupt-support"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=c17198fa5a88295f2cca722586c539280e10201c#c17198fa5a88295f2cca722586c539280e10201c"
[[package]]
name = "intl-memoizer"
version = "0.4.0"
@ -2395,6 +2409,7 @@ version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e704a02bcaecd4a08b93a23f6be59d0bd79cd161e0963e9499165a0a35df7bd"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
@ -3077,6 +3092,11 @@ dependencies = [
"nsstring",
]
[[package]]
name = "nss_build_common"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=c17198fa5a88295f2cca722586c539280e10201c#c17198fa5a88295f2cca722586c539280e10201c"
[[package]]
name = "nsstring"
version = "0.1.0"
@ -4202,6 +4222,18 @@ dependencies = [
"spirv-cross-internal",
]
[[package]]
name = "sql-support"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=c17198fa5a88295f2cca722586c539280e10201c#c17198fa5a88295f2cca722586c539280e10201c"
dependencies = [
"ffi-support",
"interrupt-support",
"lazy_static",
"log",
"rusqlite",
]
[[package]]
name = "stable_deref_trait"
version = "1.0.0"
@ -4390,18 +4422,22 @@ dependencies = [
[[package]]
name = "sync-guid"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=120e51dd5f2aab4194cf0f7e93b2a8923f4504bb#120e51dd5f2aab4194cf0f7e93b2a8923f4504bb"
source = "git+https://github.com/mozilla/application-services?rev=c17198fa5a88295f2cca722586c539280e10201c#c17198fa5a88295f2cca722586c539280e10201c"
dependencies = [
"base64 0.12.0",
"rand",
"rusqlite",
"serde",
]
[[package]]
name = "sync15-traits"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=120e51dd5f2aab4194cf0f7e93b2a8923f4504bb#120e51dd5f2aab4194cf0f7e93b2a8923f4504bb"
source = "git+https://github.com/mozilla/application-services?rev=c17198fa5a88295f2cca722586c539280e10201c#c17198fa5a88295f2cca722586c539280e10201c"
dependencies = [
"failure",
"ffi-support",
"interrupt-support",
"log",
"serde",
"serde_json",
@ -4974,6 +5010,7 @@ dependencies = [
"idna",
"matches",
"percent-encoding",
"serde",
]
[[package]]
@ -5108,6 +5145,26 @@ dependencies = [
"warp",
]
[[package]]
name = "webext-storage"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=c17198fa5a88295f2cca722586c539280e10201c#c17198fa5a88295f2cca722586c539280e10201c"
dependencies = [
"error-support",
"failure",
"interrupt-support",
"lazy_static",
"log",
"nss_build_common",
"rusqlite",
"serde",
"serde_derive",
"serde_json",
"sql-support",
"sync-guid",
"url",
]
[[package]]
name = "webrender"
version = "0.61.0"

View File

@ -0,0 +1 @@
{"files":{"Cargo.toml":"9ba6f30454cfbe5cc844824a89f31b65d607df6aec569d093eb6307d902c5159","src/lib.rs":"4581b12eb58f9fb5275c7af74fbc4521b82ef224b6ba81f0e785c372ba95f8c6"},"package":null}

View File

@ -0,0 +1,10 @@
[package]
name = "error-support"
version = "0.1.0"
authors = ["Thom Chiovoloni <tchiovoloni@mozilla.com>"]
edition = "2018"
license = "MPL-2.0"
[dependencies]
failure = "0.1.6"

View File

@ -0,0 +1,99 @@
/* 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/. */
/// Define a wrapper around the the provided ErrorKind type.
/// See also `define_error` which is more likely to be what you want.
#[macro_export]
macro_rules! define_error_wrapper {
($Kind:ty) => {
/// Re-exported, so that using crate::error::* gives you the .context()
/// method, which we don't use much but should *really* use more.
pub use failure::ResultExt;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug)]
pub struct Error(Box<failure::Context<$Kind>>);
impl failure::Fail for Error {
fn cause(&self) -> Option<&dyn failure::Fail> {
self.0.cause()
}
fn backtrace(&self) -> Option<&failure::Backtrace> {
self.0.backtrace()
}
fn name(&self) -> Option<&str> {
self.0.name()
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&*self.0, f)
}
}
impl Error {
pub fn kind(&self) -> &$Kind {
&*self.0.get_context()
}
}
impl From<failure::Context<$Kind>> for Error {
// Cold to optimize in favor of non-error cases.
#[cold]
fn from(ctx: failure::Context<$Kind>) -> Error {
Error(Box::new(ctx))
}
}
impl From<$Kind> for Error {
// Cold to optimize in favor of non-error cases.
#[cold]
fn from(kind: $Kind) -> Self {
Error(Box::new(failure::Context::new(kind)))
}
}
};
}
/// Define a set of conversions from external error types into the provided
/// error kind. Use `define_error` to do this at the same time as
/// `define_error_wrapper`.
#[macro_export]
macro_rules! define_error_conversions {
($Kind:ident { $(($variant:ident, $type:ty)),* $(,)? }) => ($(
impl From<$type> for $Kind {
// Cold to optimize in favor of non-error cases.
#[cold]
fn from(e: $type) -> $Kind {
$Kind::$variant(e)
}
}
impl From<$type> for Error {
// Cold to optimize in favor of non-error cases.
#[cold]
fn from(e: $type) -> Self {
Error::from($Kind::$variant(e))
}
}
)*);
}
/// All the error boilerplate (okay, with a couple exceptions in some cases) in
/// one place.
#[macro_export]
macro_rules! define_error {
($Kind:ident { $(($variant:ident, $type:ty)),* $(,)? }) => {
$crate::define_error_wrapper!($Kind);
$crate::define_error_conversions! {
$Kind {
$(($variant, $type)),*
}
}
};
}

View File

@ -0,0 +1 @@
{"files":{"Cargo.toml":"e4b1f4f6a20cfcbfdfe9e47a875a09d7c37e815953441000c62c191570bfa5de","README.md":"7f1418b4a7c138ba20bcaea077fe6cf0d6ffbaf6df6b90c80efc52aa0d0e2e9f","src/lib.rs":"d7311f1fe25c25e651fae85fcd734cd313331c580a050c31b8bf64d957aede0f"},"package":null}

View File

@ -0,0 +1,6 @@
[package]
name = "interrupt-support"
version = "0.1.0"
authors = ["application-services@mozilla.com"]
license = "MPL-2.0"
edition = "2018"

View File

@ -0,0 +1,4 @@
## Interrupt crate
This create exposes traits and errors to allow for interrupt support across
the various crates in this repository.

View File

@ -0,0 +1,46 @@
/* 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/. */
#![allow(unknown_lints)]
#![warn(rust_2018_idioms)]
// Note that in the future it might make sense to also add a trait for
// an Interruptable, but we don't need this abstraction now and it's unclear
// if we ever will.
/// Represents the state of something that may be interrupted. Decoupled from
/// the interrupt mechanics so that things which want to check if they have been
/// interrupted are simpler.
pub trait Interruptee {
fn was_interrupted(&self) -> bool;
fn err_if_interrupted(&self) -> Result<(), Interrupted> {
if self.was_interrupted() {
return Err(Interrupted);
}
Ok(())
}
}
/// A convenience implementation, should only be used in tests.
pub struct NeverInterrupts;
impl Interruptee for NeverInterrupts {
#[inline]
fn was_interrupted(&self) -> bool {
false
}
}
/// The error returned by err_if_interrupted.
#[derive(Debug, Clone, PartialEq)]
pub struct Interrupted;
impl std::fmt::Display for Interrupted {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("The operation was interrupted")
}
}
impl std::error::Error for Interrupted {}

View File

@ -0,0 +1 @@
{"files":{"Cargo.toml":"4f1d37d926e853eb9f3d8074b45c00a317e2b4aafbc339a471430d28526716e9","src/lib.rs":"bf8f68b313cf179725ecf84960fc0e18dc00cee428ab0d51a038252152427681"},"package":null}

View File

@ -0,0 +1,8 @@
[package]
name = "nss_build_common"
version = "0.1.0"
authors = ["Thom Chiovoloni <tchiovoloni@mozilla.com>"]
edition = "2018"
license = "MPL-2.0"
[dependencies]

View File

@ -0,0 +1,154 @@
/* 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/. */
//! This shouldn't exist, but does because if something isn't going to link
//! against `nss` but has an `nss`-enabled `sqlcipher` turned on (for example,
//! by a `cargo` feature activated by something else in the workspace).
//! it might need to issue link commands for NSS.
use std::{
env,
ffi::OsString,
path::{Path, PathBuf},
};
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum LinkingKind {
Dynamic { folded_libs: bool },
Static,
}
#[derive(Debug, PartialEq, Clone)]
pub struct NoNssDir;
pub fn link_nss() -> Result<(), NoNssDir> {
let is_gecko = env::var_os("MOZ_TOPOBJDIR").is_some();
if !is_gecko {
let (lib_dir, include_dir) = get_nss()?;
println!(
"cargo:rustc-link-search=native={}",
lib_dir.to_string_lossy()
);
println!("cargo:include={}", include_dir.to_string_lossy());
let kind = determine_kind();
link_nss_libs(kind);
} else {
let libs = match env::var("CARGO_CFG_TARGET_OS")
.as_ref()
.map(std::string::String::as_str)
{
Ok("android") | Ok("macos") => vec!["nss3"],
_ => vec!["nssutil3", "nss3", "plds4", "plc4", "nspr4"],
};
for lib in &libs {
println!("cargo:rustc-link-lib=dylib={}", lib);
}
}
Ok(())
}
fn get_nss() -> Result<(PathBuf, PathBuf), NoNssDir> {
let nss_dir = env("NSS_DIR").ok_or(NoNssDir)?;
let nss_dir = Path::new(&nss_dir);
let lib_dir = nss_dir.join("lib");
let include_dir = nss_dir.join("include");
Ok((lib_dir, include_dir))
}
fn determine_kind() -> LinkingKind {
if env_flag("NSS_STATIC") {
LinkingKind::Static
} else {
let folded_libs = env_flag("NSS_USE_FOLDED_LIBS");
LinkingKind::Dynamic { folded_libs }
}
}
fn link_nss_libs(kind: LinkingKind) {
let libs = get_nss_libs(kind);
// Emit -L flags
let kind_str = match kind {
LinkingKind::Dynamic { .. } => "dylib",
LinkingKind::Static => "static",
};
for lib in libs {
println!("cargo:rustc-link-lib={}={}", kind_str, lib);
}
}
fn get_nss_libs(kind: LinkingKind) -> Vec<&'static str> {
match kind {
LinkingKind::Static => {
let mut static_libs = vec![
"certdb",
"certhi",
"cryptohi",
"freebl_static",
"hw-acc-crypto",
"nspr4",
"nss_static",
"nssb",
"nssdev",
"nsspki",
"nssutil",
"pk11wrap_static",
"plc4",
"plds4",
"softokn_static",
];
// Hardware specific libs.
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
// https://searchfox.org/mozilla-central/rev/1eb05019f47069172ba81a6c108a584a409a24ea/security/nss/lib/freebl/freebl.gyp#159-168
if target_arch == "x86_64" || target_arch == "x86" {
static_libs.push("gcm-aes-x86_c_lib");
} else if target_arch == "aarch64" {
static_libs.push("gcm-aes-aarch64_c_lib");
}
// https://searchfox.org/mozilla-central/rev/1eb05019f47069172ba81a6c108a584a409a24ea/security/nss/lib/freebl/freebl.gyp#224-233
if ((target_os == "android" || target_os == "linux") && target_arch == "x86_64")
|| target_os == "windows"
{
static_libs.push("intel-gcm-wrap_c_lib");
// https://searchfox.org/mozilla-central/rev/1eb05019f47069172ba81a6c108a584a409a24ea/security/nss/lib/freebl/freebl.gyp#43-47
if (target_os == "android" || target_os == "linux") && target_arch == "x86_64" {
static_libs.push("intel-gcm-s_lib");
}
}
static_libs
}
LinkingKind::Dynamic { folded_libs } => {
let mut dylibs = vec!["freebl3", "nss3", "nssckbi", "softokn3"];
if !folded_libs {
dylibs.append(&mut vec!["nspr4", "nssutil3", "plc4", "plds4"]);
}
dylibs
}
}
}
pub fn env(name: &str) -> Option<OsString> {
println!("cargo:rerun-if-env-changed={}", name);
env::var_os(name)
}
pub fn env_str(name: &str) -> Option<String> {
println!("cargo:rerun-if-env-changed={}", name);
env::var(name).ok()
}
pub fn env_flag(name: &str) -> bool {
match env_str(name).as_ref().map(String::as_ref) {
Some("1") => true,
Some("0") => false,
Some(s) => {
println!(
"cargo:warning=unknown value for environment var {:?}: {:?}. Ignoring",
name, s
);
false
}
None => false,
}
}

View File

@ -0,0 +1 @@
{"files":{"Cargo.toml":"6ac08b70091eff4fc18499837eef7b330aeeda34da64c707a322a2cdaac0ae31","doc/query-plan.md":"fc877e6cbf1b0e089ec99ee4f34673cd9b3fe1a23c8fcfec20cf286cdc0cd0d0","src/conn_ext.rs":"1126009dd562a333d336c6230814b03de970e2eceaef51b3a3ecd23484a3e23b","src/each_chunk.rs":"8aaba842e43b002fbc0fee95d14ce08faa7187b1979c765b2e270cd4802607a5","src/interrupt.rs":"76c829dce08673e06cf1273030a134cd38f713f9b8a9c80982e753a1fe1437a2","src/lib.rs":"cceb1d597dfc01e1141b89351bc875d7b2a680c272642eee53221c3aab9a70e0","src/maybe_cached.rs":"0b18425595055883a98807fbd62ff27a79c18af34e7cb3439f8c3438463ef2dd","src/query_plan.rs":"c0cc296ddf528a949f683317cea2da67ff5caee8042cf20ff00d9f8f54272ad8","src/repeat.rs":"1885f4dd36cc21fabad1ba28ad2ff213ed17707c57564e1c0d7b0349112118bb"},"package":null}

20
third_party/rust/sql-support/Cargo.toml vendored Normal file
View File

@ -0,0 +1,20 @@
[package]
name = "sql-support"
edition = "2018"
version = "0.1.0"
authors = ["Thom Chiovoloni <tchiovoloni@mozilla.com>"]
license = "MPL-2.0"
[features]
default = []
log_query_plans = []
[dependencies]
log = "0.4"
lazy_static = "1.4.0"
interrupt-support = { path = "../interrupt" }
ffi-support = "0.4"
[dependencies.rusqlite]
version = "0.23.1"
features = ["functions", "limits", "bundled"]

View File

@ -0,0 +1,79 @@
# Getting query plans out of places/logins/other consumers.
If these crates are built with the `log_query_plans` feature enabled (or cargo decides to use a version of `sql-support` that has beeen built with that feature), then queries that go through sql-support will have their [query plans](https://www.sqlite.org/eqp.html) logged. The default place they get logged is stdout, however you can also specify a file by setting the `QUERY_PLAN_LOG` variable in the environment to a file where the plans will be appended.
Worth noting that new logs will be appended to `QUERY_PLAN_LOG`, we don't clear the file. This is so that you can more easily see how the query plan changed during testing.
The queries that go through this are any that are
1. Executed entirely within sql-support (we need both the query and it's parameters)
2. Take named (and not positional) parameters.
At the time of writing this, that includes:
- `try_query_row`
- `query_rows_and_then_named_cached`
- `query_rows_and_then_named`
- `query_row_and_then_named`
- `query_one`
- `execute_named_cached`
- Possibly more, check [ConnExt](https://github.com/mozilla/application-services/blob/master/components/support/sql/src/conn_ext.rs).
In particular, this excludes queries where the statement is prepared separately from execution.
## Usage
As mentioned, this is turned on with the log_query_plans feature. I don't know why, but I've had mediocre luck enabling it explicitly, but 100% success enabling it via `--all-features`. So that's what I recommend.
Note that for tests, if you're logging to stdout, you'll need to end the test command with `-- --no-capture`, or else it will hide stdout output from you. You also may want to pass `--test-threads 1` (also after the `--`) so that the plans are logged near the tests that are executing, but it doesn't matter that much, since we log the SQL before the plan.
Executing tests, having the output logged to stdout:
```
$ cargo test -p logins --all-features -- --no-capture
... <snip>
test engine::test::test_general ...
### QUERY PLAN
#### SQL:
SELECT <bunch of fields here>
FROM loginsL
WHERE is_deleted = 0
AND guid = :guid
UNION ALL
SELECT <same bunch of fields here>
FROM loginsM
WHERE is_overridden IS NOT 1
AND guid = :guid
ORDER BY hostname ASC
LIMIT 1
#### PLAN:
QUERY PLAN
`--MERGE (UNION ALL)
|--LEFT
| `--SEARCH TABLE loginsL USING INDEX sqlite_autoindex_loginsL_1 (guid=?)
`--RIGHT
`--SEARCH TABLE loginsM USING INDEX sqlite_autoindex_loginsM_1 (guid=?)
### END QUERY PLAN
... <snip>
```
Executing an example, with the output logged to a file.
```
$ env QUERY_PLAN_LOG=/path/to/my/logfile.txt cargo run -p places --all-features --example autocomplete -- <args for example go here>
# (many shells can also do this as follows)
$ QUERY_PLAN_LOG=/path/to/my/logfile.txt cargo run -p places --all-features --example autocomplete -- <args for example go here>
```
## Using from code
This is also available as types on `sql_support`.
```rust
println!("This prints the same output as is normally logged, and works \
even when the logging feature is off: {}",
sql_support:QueryPlan::new(conn, sql, params));
```

View File

@ -0,0 +1,380 @@
/* 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/. */
use rusqlite::{
self,
types::{FromSql, ToSql},
Connection, Result as SqlResult, Row, Savepoint, Transaction, TransactionBehavior, NO_PARAMS,
};
use std::iter::FromIterator;
use std::ops::Deref;
use std::time::Instant;
use crate::maybe_cached::MaybeCached;
pub struct Conn(rusqlite::Connection);
/// This trait exists so that we can use these helpers on `rusqlite::{Transaction, Connection}`.
/// Note that you must import ConnExt in order to call these methods on anything.
pub trait ConnExt {
/// The method you need to implement to opt in to all of this.
fn conn(&self) -> &Connection;
/// Set the value of the pragma on the main database. Returns the same object, for chaining.
fn set_pragma<T>(&self, pragma_name: &str, pragma_value: T) -> SqlResult<&Self>
where
T: ToSql,
Self: Sized,
{
// None == Schema name, e.g. `PRAGMA some_attached_db.something = blah`
self.conn()
.pragma_update(None, pragma_name, &pragma_value)?;
Ok(self)
}
/// Get a cached or uncached statement based on a flag.
fn prepare_maybe_cached<'conn>(
&'conn self,
sql: &str,
cache: bool,
) -> SqlResult<MaybeCached<'conn>> {
MaybeCached::prepare(self.conn(), sql, cache)
}
/// Execute all the provided statements.
fn execute_all(&self, stmts: &[&str]) -> SqlResult<()> {
let conn = self.conn();
for sql in stmts {
let r = conn.execute(sql, NO_PARAMS);
match r {
Ok(_) => {}
// Ignore ExecuteReturnedResults error because they're pointless
// and annoying.
Err(rusqlite::Error::ExecuteReturnedResults) => {}
Err(e) => return Err(e),
}
}
Ok(())
}
/// Equivalent to `Connection::execute_named` but caches the statement so that subsequent
/// calls to `execute_cached` will have improved performance.
fn execute_cached<P>(&self, sql: &str, params: P) -> SqlResult<usize>
where
P: IntoIterator,
P::Item: ToSql,
{
let mut stmt = self.conn().prepare_cached(sql)?;
stmt.execute(params)
}
/// Equivalent to `Connection::execute_named` but caches the statement so that subsequent
/// calls to `execute_named_cached` will have imprroved performance.
fn execute_named_cached(&self, sql: &str, params: &[(&str, &dyn ToSql)]) -> SqlResult<usize> {
crate::maybe_log_plan(self.conn(), sql, params);
let mut stmt = self.conn().prepare_cached(sql)?;
stmt.execute_named(params)
}
/// Execute a query that returns a single result column, and return that result.
fn query_one<T: FromSql>(&self, sql: &str) -> SqlResult<T> {
crate::maybe_log_plan(self.conn(), sql, &[]);
let res: T = self
.conn()
.query_row_and_then(sql, NO_PARAMS, |row| row.get(0))?;
Ok(res)
}
/// Execute a query that returns 0 or 1 result columns, returning None
/// if there were no rows, or if the only result was NULL.
fn try_query_one<T: FromSql>(
&self,
sql: &str,
params: &[(&str, &dyn ToSql)],
cache: bool,
) -> SqlResult<Option<T>>
where
Self: Sized,
{
crate::maybe_log_plan(self.conn(), sql, params);
use rusqlite::OptionalExtension;
// The outer option is if we got rows, the inner option is
// if the first row was null.
let res: Option<Option<T>> = self
.conn()
.query_row_and_then_named(sql, params, |row| row.get(0), cache)
.optional()?;
// go from Option<Option<T>> to Option<T>
Ok(res.unwrap_or_default())
}
/// Equivalent to `rusqlite::Connection::query_row_and_then` but allows use
/// of named parameters, and allows passing a flag to indicate that it's cached.
fn query_row_and_then_named<T, E, F>(
&self,
sql: &str,
params: &[(&str, &dyn ToSql)],
mapper: F,
cache: bool,
) -> Result<T, E>
where
Self: Sized,
E: From<rusqlite::Error>,
F: FnOnce(&Row<'_>) -> Result<T, E>,
{
crate::maybe_log_plan(self.conn(), sql, params);
Ok(self
.try_query_row(sql, params, mapper, cache)?
.ok_or(rusqlite::Error::QueryReturnedNoRows)?)
}
/// Helper for when you'd like to get a Vec<T> of all the rows returned by a
/// query that takes named arguments. See also
/// `query_rows_and_then_named_cached`.
fn query_rows_and_then_named<T, E, F>(
&self,
sql: &str,
params: &[(&str, &dyn ToSql)],
mapper: F,
) -> Result<Vec<T>, E>
where
Self: Sized,
E: From<rusqlite::Error>,
F: FnMut(&Row<'_>) -> Result<T, E>,
{
crate::maybe_log_plan(self.conn(), sql, params);
query_rows_and_then_named(self.conn(), sql, params, mapper, false)
}
/// Helper for when you'd like to get a Vec<T> of all the rows returned by a
/// query that takes named arguments.
fn query_rows_and_then_named_cached<T, E, F>(
&self,
sql: &str,
params: &[(&str, &dyn ToSql)],
mapper: F,
) -> Result<Vec<T>, E>
where
Self: Sized,
E: From<rusqlite::Error>,
F: FnMut(&Row<'_>) -> Result<T, E>,
{
crate::maybe_log_plan(self.conn(), sql, params);
query_rows_and_then_named(self.conn(), sql, params, mapper, true)
}
/// Like `query_rows_and_then_named`, but works if you want a non-Vec as a result.
/// # Example:
/// ```rust,no_run
/// # use std::collections::HashSet;
/// # use sql_support::ConnExt;
/// # use rusqlite::Connection;
/// fn get_visit_tombstones(conn: &Connection, id: i64) -> rusqlite::Result<HashSet<i64>> {
/// Ok(conn.query_rows_into(
/// "SELECT visit_date FROM moz_historyvisit_tombstones
/// WHERE place_id = :place_id",
/// &[(":place_id", &id)],
/// |row| row.get::<_, i64>(0))?)
/// }
/// ```
/// Note if the type isn't inferred, you'll have to do something gross like
/// `conn.query_rows_into::<HashSet<_>, _, _, _>(...)`.
fn query_rows_into<Coll, T, E, F>(
&self,
sql: &str,
params: &[(&str, &dyn ToSql)],
mapper: F,
) -> Result<Coll, E>
where
Self: Sized,
E: From<rusqlite::Error>,
F: FnMut(&Row<'_>) -> Result<T, E>,
Coll: FromIterator<T>,
{
crate::maybe_log_plan(self.conn(), sql, params);
query_rows_and_then_named(self.conn(), sql, params, mapper, false)
}
/// Same as `query_rows_into`, but caches the stmt if possible.
fn query_rows_into_cached<Coll, T, E, F>(
&self,
sql: &str,
params: &[(&str, &dyn ToSql)],
mapper: F,
) -> Result<Coll, E>
where
Self: Sized,
E: From<rusqlite::Error>,
F: FnMut(&Row<'_>) -> Result<T, E>,
Coll: FromIterator<T>,
{
crate::maybe_log_plan(self.conn(), sql, params);
query_rows_and_then_named(self.conn(), sql, params, mapper, true)
}
// This should probably have a longer name...
/// Like `query_row_and_then_named` but returns None instead of erroring if no such row exists.
fn try_query_row<T, E, F>(
&self,
sql: &str,
params: &[(&str, &dyn ToSql)],
mapper: F,
cache: bool,
) -> Result<Option<T>, E>
where
Self: Sized,
E: From<rusqlite::Error>,
F: FnOnce(&Row<'_>) -> Result<T, E>,
{
crate::maybe_log_plan(self.conn(), sql, params);
let conn = self.conn();
let mut stmt = MaybeCached::prepare(conn, sql, cache)?;
let mut rows = stmt.query_named(params)?;
rows.next()?.map(mapper).transpose()
}
fn unchecked_transaction(&self) -> SqlResult<UncheckedTransaction<'_>> {
UncheckedTransaction::new(self.conn(), TransactionBehavior::Deferred)
}
/// Begin `unchecked_transaction` with `TransactionBehavior::Immediate`. Use
/// when the first operation will be a read operation, that further writes
/// depend on for correctness.
fn unchecked_transaction_imm(&self) -> SqlResult<UncheckedTransaction<'_>> {
UncheckedTransaction::new(self.conn(), TransactionBehavior::Immediate)
}
}
impl ConnExt for Connection {
#[inline]
fn conn(&self) -> &Connection {
self
}
}
impl<'conn> ConnExt for Transaction<'conn> {
#[inline]
fn conn(&self) -> &Connection {
&*self
}
}
impl<'conn> ConnExt for Savepoint<'conn> {
#[inline]
fn conn(&self) -> &Connection {
&*self
}
}
/// rusqlite, in an attempt to save us from ourselves, needs a mutable ref to
/// a connection to start a transaction. That is a bit of a PITA in some cases,
/// so we offer this as an alternative - but the responsibility of ensuring
/// there are no concurrent transactions is on our head.
///
/// This is very similar to the rusqlite `Transaction` - it doesn't prevent
/// against nested transactions but does allow you to use an immutable
/// `Connection`.
pub struct UncheckedTransaction<'conn> {
pub conn: &'conn Connection,
pub started_at: Instant,
pub finished: bool,
// we could add drop_behavior etc too, but we don't need it yet - we
// always rollback.
}
impl<'conn> UncheckedTransaction<'conn> {
/// Begin a new unchecked transaction. Cannot be nested, but this is not
/// enforced by Rust (hence 'unchecked') - however, it is enforced by
/// SQLite; use a rusqlite `savepoint` for nested transactions.
pub fn new(conn: &'conn Connection, behavior: TransactionBehavior) -> SqlResult<Self> {
let query = match behavior {
TransactionBehavior::Deferred => "BEGIN DEFERRED",
TransactionBehavior::Immediate => "BEGIN IMMEDIATE",
TransactionBehavior::Exclusive => "BEGIN EXCLUSIVE",
_ => unreachable!(),
};
conn.execute_batch(query)
.map(move |_| UncheckedTransaction {
conn,
started_at: Instant::now(),
finished: false,
})
}
/// Consumes and commits an unchecked transaction.
pub fn commit(mut self) -> SqlResult<()> {
if self.finished {
log::warn!("ignoring request to commit an already finished transaction");
return Ok(());
}
self.finished = true;
self.conn.execute_batch("COMMIT")?;
log::debug!("Transaction commited after {:?}", self.started_at.elapsed());
Ok(())
}
/// Consumes and rolls back an unchecked transaction.
pub fn rollback(mut self) -> SqlResult<()> {
if self.finished {
log::warn!("ignoring request to rollback an already finished transaction");
return Ok(());
}
self.rollback_()
}
fn rollback_(&mut self) -> SqlResult<()> {
self.finished = true;
self.conn.execute_batch("ROLLBACK")?;
Ok(())
}
fn finish_(&mut self) -> SqlResult<()> {
if self.finished || self.conn.is_autocommit() {
return Ok(());
}
self.rollback_()?;
Ok(())
}
}
impl<'conn> Deref for UncheckedTransaction<'conn> {
type Target = Connection;
#[inline]
fn deref(&self) -> &Connection {
self.conn
}
}
impl<'conn> Drop for UncheckedTransaction<'conn> {
fn drop(&mut self) {
if let Err(e) = self.finish_() {
log::warn!("Error dropping an unchecked transaction: {}", e);
}
}
}
impl<'conn> ConnExt for UncheckedTransaction<'conn> {
#[inline]
fn conn(&self) -> &Connection {
&*self
}
}
fn query_rows_and_then_named<Coll, T, E, F>(
conn: &Connection,
sql: &str,
params: &[(&str, &dyn ToSql)],
mapper: F,
cache: bool,
) -> Result<Coll, E>
where
E: From<rusqlite::Error>,
F: FnMut(&Row<'_>) -> Result<T, E>,
Coll: FromIterator<T>,
{
let mut stmt = conn.prepare_maybe_cached(sql, cache)?;
let iter = stmt.query_and_then_named(params, mapper)?;
Ok(iter.collect::<Result<Coll, E>>()?)
}

View File

@ -0,0 +1,311 @@
/* 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/. */
use lazy_static::lazy_static;
use rusqlite::{self, limits::Limit, types::ToSql};
use std::iter::Map;
use std::slice::Iter;
/// Returns SQLITE_LIMIT_VARIABLE_NUMBER as read from an in-memory connection and cached.
/// connection and cached. That means this will return the wrong value if it's set to a lower
/// value for a connection using this will return the wrong thing, but doing so is rare enough
/// that we explicitly don't support it (why would you want to lower this at runtime?).
///
/// If you call this and the actual value was set to a negative number or zero (nothing prevents
/// this beyond a warning in the SQLite documentation), we panic. However, it's unlikely you can
/// run useful queries if this happened anyway.
pub fn default_max_variable_number() -> usize {
lazy_static! {
static ref MAX_VARIABLE_NUMBER: usize = {
let conn = rusqlite::Connection::open_in_memory()
.expect("Failed to initialize in-memory connection (out of memory?)");
let limit = conn.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER);
assert!(
limit > 0,
"Illegal value for SQLITE_LIMIT_VARIABLE_NUMBER (must be > 0) {}",
limit
);
limit as usize
};
}
*MAX_VARIABLE_NUMBER
}
/// Helper for the case where you have a `&[impl ToSql]` of arbitrary length, but need one
/// of no more than the connection's `MAX_VARIABLE_NUMBER` (rather,
/// `default_max_variable_number()`). This is useful when performing batched updates.
///
/// The `do_chunk` callback is called with a slice of no more than `default_max_variable_number()`
/// items as it's first argument, and the offset from the start as it's second.
///
/// See `each_chunk_mapped` for the case where `T` doesn't implement `ToSql`, but can be
/// converted to something that does.
pub fn each_chunk<'a, T, E, F>(items: &'a [T], do_chunk: F) -> Result<(), E>
where
T: 'a,
F: FnMut(&'a [T], usize) -> Result<(), E>,
{
each_sized_chunk(items, default_max_variable_number(), do_chunk)
}
/// A version of `each_chunk` for the case when the conversion to `to_sql` requires an custom
/// intermediate step. For example, you might want to grab a property off of an arrray of records
pub fn each_chunk_mapped<'a, T, U, E, Mapper, DoChunk>(
items: &'a [T],
to_sql: Mapper,
do_chunk: DoChunk,
) -> Result<(), E>
where
T: 'a,
U: ToSql + 'a,
Mapper: Fn(&'a T) -> U,
DoChunk: FnMut(Map<Iter<'a, T>, &'_ Mapper>, usize) -> Result<(), E>,
{
each_sized_chunk_mapped(items, default_max_variable_number(), to_sql, do_chunk)
}
// Split out for testing. Separate so that we can pass an actual slice
// to the callback if they don't need mapping. We could probably unify
// this with each_sized_chunk_mapped with a lot of type system trickery,
// but one of the benefits to each_chunk over the mapped versions is
// that the declaration is simpler.
pub fn each_sized_chunk<'a, T, E, F>(
items: &'a [T],
chunk_size: usize,
mut do_chunk: F,
) -> Result<(), E>
where
T: 'a,
F: FnMut(&'a [T], usize) -> Result<(), E>,
{
if items.is_empty() {
return Ok(());
}
let mut offset = 0;
for chunk in items.chunks(chunk_size) {
do_chunk(chunk, offset)?;
offset += chunk.len();
}
Ok(())
}
/// Utility to help perform batched updates, inserts, queries, etc. This is the low-level version
/// of this utility which is wrapped by `each_chunk` and `each_chunk_mapped`, and it allows you to
/// provide both the mapping function, and the chunk size.
///
/// Note: `mapped` basically just refers to the translating of `T` to some `U` where `U: ToSql`
/// using the `to_sql` function. This is useful for e.g. inserting the IDs of a large list
/// of records.
pub fn each_sized_chunk_mapped<'a, T, U, E, Mapper, DoChunk>(
items: &'a [T],
chunk_size: usize,
to_sql: Mapper,
mut do_chunk: DoChunk,
) -> Result<(), E>
where
T: 'a,
U: ToSql + 'a,
Mapper: Fn(&'a T) -> U,
DoChunk: FnMut(Map<Iter<'a, T>, &'_ Mapper>, usize) -> Result<(), E>,
{
if items.is_empty() {
return Ok(());
}
let mut offset = 0;
for chunk in items.chunks(chunk_size) {
let mapped = chunk.iter().map(&to_sql);
do_chunk(mapped, offset)?;
offset += chunk.len();
}
Ok(())
}
#[cfg(test)]
fn check_chunk<T, C>(items: C, expect: &[T], desc: &str)
where
C: IntoIterator,
<C as IntoIterator>::Item: ToSql,
T: ToSql,
{
let items = items.into_iter().collect::<Vec<_>>();
assert_eq!(items.len(), expect.len());
// Can't quite make the borrowing work out here w/o a loop, oh well.
for (idx, (got, want)) in items.iter().zip(expect.iter()).enumerate() {
assert_eq!(
got.to_sql().unwrap(),
want.to_sql().unwrap(),
// ToSqlOutput::Owned(Value::Integer(*num)),
"{}: Bad value at index {}",
desc,
idx
);
}
}
#[cfg(test)]
mod test_mapped {
use super::*;
#[test]
fn test_separate() {
let mut iteration = 0;
each_sized_chunk_mapped(
&[1, 2, 3, 4, 5],
3,
|item| item as &dyn ToSql,
|chunk, offset| {
match offset {
0 => {
assert_eq!(iteration, 0);
check_chunk(chunk, &[1, 2, 3], "first chunk");
}
3 => {
assert_eq!(iteration, 1);
check_chunk(chunk, &[4, 5], "second chunk");
}
n => {
panic!("Unexpected offset {}", n);
}
}
iteration += 1;
Ok::<(), ()>(())
},
)
.unwrap();
}
#[test]
fn test_leq_chunk_size() {
for &check_size in &[5, 6] {
let mut iteration = 0;
each_sized_chunk_mapped(
&[1, 2, 3, 4, 5],
check_size,
|item| item as &dyn ToSql,
|chunk, offset| {
assert_eq!(iteration, 0);
iteration += 1;
assert_eq!(offset, 0);
check_chunk(chunk, &[1, 2, 3, 4, 5], "only iteration");
Ok::<(), ()>(())
},
)
.unwrap();
}
}
#[test]
fn test_empty_chunk() {
let items: &[i64] = &[];
each_sized_chunk_mapped::<_, _, (), _, _>(
items,
100,
|item| item as &dyn ToSql,
|_, _| {
panic!("Should never be called");
},
)
.unwrap();
}
#[test]
fn test_error() {
let mut iteration = 0;
let e = each_sized_chunk_mapped(
&[1, 2, 3, 4, 5, 6, 7],
3,
|item| item as &dyn ToSql,
|_, offset| {
if offset == 0 {
assert_eq!(iteration, 0);
iteration += 1;
Ok(())
} else if offset == 3 {
assert_eq!(iteration, 1);
iteration += 1;
Err("testing".to_string())
} else {
// Make sure we stopped after the error.
panic!("Shouldn't get called with offset of {}", offset);
}
},
)
.expect_err("Should be an error");
assert_eq!(e, "testing");
}
}
#[cfg(test)]
mod test_unmapped {
use super::*;
#[test]
fn test_separate() {
let mut iteration = 0;
each_sized_chunk(&[1, 2, 3, 4, 5], 3, |chunk, offset| {
match offset {
0 => {
assert_eq!(iteration, 0);
check_chunk(chunk, &[1, 2, 3], "first chunk");
}
3 => {
assert_eq!(iteration, 1);
check_chunk(chunk, &[4, 5], "second chunk");
}
n => {
panic!("Unexpected offset {}", n);
}
}
iteration += 1;
Ok::<(), ()>(())
})
.unwrap();
}
#[test]
fn test_leq_chunk_size() {
for &check_size in &[5, 6] {
let mut iteration = 0;
each_sized_chunk(&[1, 2, 3, 4, 5], check_size, |chunk, offset| {
assert_eq!(iteration, 0);
iteration += 1;
assert_eq!(offset, 0);
check_chunk(chunk, &[1, 2, 3, 4, 5], "only iteration");
Ok::<(), ()>(())
})
.unwrap();
}
}
#[test]
fn test_empty_chunk() {
let items: &[i64] = &[];
each_sized_chunk::<_, (), _>(items, 100, |_, _| {
panic!("Should never be called");
})
.unwrap();
}
#[test]
fn test_error() {
let mut iteration = 0;
let e = each_sized_chunk(&[1, 2, 3, 4, 5, 6, 7], 3, |_, offset| {
if offset == 0 {
assert_eq!(iteration, 0);
iteration += 1;
Ok(())
} else if offset == 3 {
assert_eq!(iteration, 1);
iteration += 1;
Err("testing".to_string())
} else {
// Make sure we stopped after the error.
panic!("Shouldn't get called with offset of {}", offset);
}
})
.expect_err("Should be an error");
assert_eq!(e, "testing");
}
}

View File

@ -0,0 +1,89 @@
/* 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/. */
use ffi_support::implement_into_ffi_by_pointer;
use interrupt_support::Interruptee;
use rusqlite::InterruptHandle;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
// SeqCst is overkill for much of this, but whatever.
/// A Sync+Send type which can be used allow someone to interrupt an
/// operation, even if it happens while rust code (and not SQL) is
/// executing.
pub struct SqlInterruptHandle {
db_handle: InterruptHandle,
interrupt_counter: Arc<AtomicUsize>,
}
impl SqlInterruptHandle {
pub fn new(
db_handle: InterruptHandle,
interrupt_counter: Arc<AtomicUsize>,
) -> SqlInterruptHandle {
SqlInterruptHandle {
db_handle,
interrupt_counter,
}
}
pub fn interrupt(&self) {
self.interrupt_counter.fetch_add(1, Ordering::SeqCst);
self.db_handle.interrupt();
}
}
implement_into_ffi_by_pointer!(SqlInterruptHandle);
/// A helper that can be used to determine if an interrupt request has come in while
/// the object lives. This is used to avoid a case where we aren't running any
/// queries when the request to stop comes in, but we're still not done (for example,
/// maybe we've run some of the autocomplete matchers, and are about to start
/// running the others. If we rely solely on sqlite3_interrupt(), we'd miss
/// the message that we should stop).
#[derive(Debug)]
pub struct SqlInterruptScope {
// The value of the interrupt counter when the scope began
start_value: usize,
// This could be &'conn AtomicUsize, but it would prevent the connection
// from being mutably borrowed for no real reason...
ptr: Arc<AtomicUsize>,
}
impl SqlInterruptScope {
#[inline]
pub fn new(ptr: Arc<AtomicUsize>) -> Self {
let start_value = ptr.load(Ordering::SeqCst);
Self { start_value, ptr }
}
/// Add this as an inherent method to reduce the amount of things users have to bring in.
#[inline]
pub fn err_if_interrupted(&self) -> Result<(), interrupt_support::Interrupted> {
<Self as Interruptee>::err_if_interrupted(self)
}
}
impl Interruptee for SqlInterruptScope {
#[inline]
fn was_interrupted(&self) -> bool {
self.ptr.load(Ordering::SeqCst) != self.start_value
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_sync_send() {
fn is_sync<T: Sync>() {}
fn is_send<T: Send>() {}
// Make sure this compiles
is_sync::<SqlInterruptHandle>();
is_send::<SqlInterruptHandle>();
}
}

39
third_party/rust/sql-support/src/lib.rs vendored Normal file
View File

@ -0,0 +1,39 @@
/* 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/. */
#![allow(unknown_lints)]
#![warn(rust_2018_idioms)]
mod conn_ext;
mod each_chunk;
mod interrupt;
mod maybe_cached;
mod query_plan;
mod repeat;
pub use crate::conn_ext::*;
pub use crate::each_chunk::*;
pub use crate::interrupt::*;
pub use crate::maybe_cached::*;
pub use crate::query_plan::*;
pub use crate::repeat::*;
/// In PRAGMA foo='bar', `'bar'` must be a constant string (it cannot be a
/// bound parameter), so we need to escape manually. According to
/// https://www.sqlite.org/faq.html, the only character that must be escaped is
/// the single quote, which is escaped by placing two single quotes in a row.
pub fn escape_string_for_pragma(s: &str) -> String {
s.replace("'", "''")
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_escape_string_for_pragma() {
assert_eq!(escape_string_for_pragma("foobar"), "foobar");
assert_eq!(escape_string_for_pragma("'foo'bar'"), "''foo''bar''");
assert_eq!(escape_string_for_pragma("''"), "''''");
}
}

View File

@ -0,0 +1,64 @@
/* 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/. */
use rusqlite::{self, CachedStatement, Connection, Statement};
use std::ops::{Deref, DerefMut};
/// MaybeCached is a type that can be used to help abstract
/// over cached and uncached rusqlite statements in a transparent manner.
pub enum MaybeCached<'conn> {
Uncached(Statement<'conn>),
Cached(CachedStatement<'conn>),
}
impl<'conn> Deref for MaybeCached<'conn> {
type Target = Statement<'conn>;
#[inline]
fn deref(&self) -> &Statement<'conn> {
match self {
MaybeCached::Cached(cached) => Deref::deref(cached),
MaybeCached::Uncached(uncached) => uncached,
}
}
}
impl<'conn> DerefMut for MaybeCached<'conn> {
#[inline]
fn deref_mut(&mut self) -> &mut Statement<'conn> {
match self {
MaybeCached::Cached(cached) => DerefMut::deref_mut(cached),
MaybeCached::Uncached(uncached) => uncached,
}
}
}
impl<'conn> From<Statement<'conn>> for MaybeCached<'conn> {
#[inline]
fn from(stmt: Statement<'conn>) -> Self {
MaybeCached::Uncached(stmt)
}
}
impl<'conn> From<CachedStatement<'conn>> for MaybeCached<'conn> {
#[inline]
fn from(stmt: CachedStatement<'conn>) -> Self {
MaybeCached::Cached(stmt)
}
}
impl<'conn> MaybeCached<'conn> {
#[inline]
pub fn prepare(
conn: &'conn Connection,
sql: &str,
cached: bool,
) -> rusqlite::Result<MaybeCached<'conn>> {
if cached {
Ok(MaybeCached::Cached(conn.prepare_cached(sql)?))
} else {
Ok(MaybeCached::Uncached(conn.prepare(sql)?))
}
}
}

View File

@ -0,0 +1,182 @@
/* 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/. */
use rusqlite::{types::ToSql, Connection, Result as SqlResult};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct QueryPlanStep {
pub node_id: i32,
pub parent_id: i32,
pub aux: i32,
pub detail: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct QueryPlan {
pub query: String,
pub plan: Vec<QueryPlanStep>,
}
impl QueryPlan {
// TODO: support positional params (it's a pain...)
pub fn new(conn: &Connection, sql: &str, params: &[(&str, &dyn ToSql)]) -> SqlResult<Self> {
let plan_sql = format!("EXPLAIN QUERY PLAN {}", sql);
let mut stmt = conn.prepare(&plan_sql)?;
let plan = stmt
.query_and_then_named(params, |row| -> SqlResult<_> {
Ok(QueryPlanStep {
node_id: row.get(0)?,
parent_id: row.get(1)?,
aux: row.get(2)?,
detail: row.get(3)?,
})
})?
.collect::<Result<Vec<QueryPlanStep>, _>>()?;
Ok(QueryPlan {
query: sql.into(),
plan,
})
}
pub fn print_pretty_tree(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.plan.is_empty() {
return writeln!(f, "<no query plan>");
}
writeln!(f, "QUERY PLAN")?;
let children = self
.plan
.iter()
.filter(|e| e.parent_id == 0)
.collect::<Vec<_>>();
for (i, child) in children.iter().enumerate() {
let last = i == children.len() - 1;
self.print_tree(f, child, "", last)?;
}
Ok(())
}
fn print_tree(
&self,
f: &mut std::fmt::Formatter<'_>,
entry: &QueryPlanStep,
prefix: &str,
last_child: bool,
) -> std::fmt::Result {
let children = self
.plan
.iter()
.filter(|e| e.parent_id == entry.node_id)
.collect::<Vec<_>>();
let next_prefix = if last_child {
writeln!(f, "{}`--{}", prefix, entry.detail)?;
format!("{} ", prefix)
} else {
writeln!(f, "{}|--{}", prefix, entry.detail)?;
format!("{}| ", prefix)
};
for (i, child) in children.iter().enumerate() {
let last = i == children.len() - 1;
self.print_tree(f, child, &next_prefix, last)?;
}
Ok(())
}
}
impl std::fmt::Display for QueryPlan {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "### QUERY PLAN")?;
writeln!(f, "#### SQL:\n{}\n#### PLAN:", self.query)?;
self.print_pretty_tree(f)?;
writeln!(f, "### END QUERY PLAN")
}
}
/// Log a query plan if the `log_query_plans` feature is enabled and it hasn't been logged yet.
#[inline]
pub fn maybe_log_plan(_conn: &Connection, _sql: &str, _params: &[(&str, &dyn ToSql)]) {
// Note: underscores ar needed becasue those go unused if the feature is not turned on.
#[cfg(feature = "log_query_plans")]
{
plan_log::log_plan(_conn, _sql, _params)
}
}
#[cfg(feature = "log_query_plans")]
mod plan_log {
use super::*;
use std::collections::HashMap;
use std::io::Write;
use std::sync::Mutex;
struct PlanLogger {
seen: HashMap<String, QueryPlan>,
out: Box<dyn Write + Send>,
}
impl PlanLogger {
fn new() -> Self {
let out_file = std::env::var("QUERY_PLAN_LOG").unwrap_or_default();
let output: Box<dyn Write + Send> = if out_file != "" {
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(out_file)
.expect("QUERY_PLAN_LOG file does not exist!");
writeln!(
file,
"\n\n# Query Plan Log starting at time: {:?}\n",
std::time::SystemTime::now()
)
.expect("Failed to write to plan log file");
Box::new(file)
} else {
println!("QUERY_PLAN_LOG was not set, logging to stdout");
Box::new(std::io::stdout())
};
Self {
seen: Default::default(),
out: output,
}
}
fn maybe_log(&mut self, plan: QueryPlan) {
use std::collections::hash_map::Entry;
match self.seen.entry(plan.query.clone()) {
Entry::Occupied(mut o) => {
if o.get() == &plan {
return;
}
// Ignore IO failures.
let _ = writeln!(self.out, "### QUERY PLAN CHANGED!\n{}", plan);
o.insert(plan);
}
Entry::Vacant(v) => {
let _ = writeln!(self.out, "{}", plan);
v.insert(plan);
}
}
let _ = self.out.flush();
}
}
lazy_static::lazy_static! {
static ref PLAN_LOGGER: Mutex<PlanLogger> = Mutex::new(PlanLogger::new());
}
pub fn log_plan(conn: &Connection, sql: &str, params: &[(&str, &dyn ToSql)]) {
if sql.starts_with("EXPLAIN") {
return;
}
let plan = match QueryPlan::new(conn, sql, params) {
Ok(plan) => plan,
Err(e) => {
// We're usually doing this during tests where logs often arent available
eprintln!("Failed to get query plan for {}: {}", sql, e);
return;
}
};
let mut logger = PLAN_LOGGER.lock().unwrap();
logger.maybe_log(plan);
}
}

View File

@ -0,0 +1,113 @@
/* 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/. */
use std::fmt;
/// Helper type for printing repeated strings more efficiently. You should use
/// [`repeat_display`](sql_support::repeat_display), or one of the `repeat_sql_*` helpers to
/// construct it.
#[derive(Debug, Clone)]
pub struct RepeatDisplay<'a, F> {
count: usize,
sep: &'a str,
fmt_one: F,
}
impl<'a, F> fmt::Display for RepeatDisplay<'a, F>
where
F: Fn(usize, &mut fmt::Formatter<'_>) -> fmt::Result,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for i in 0..self.count {
if i != 0 {
f.write_str(self.sep)?;
}
(self.fmt_one)(i, f)?;
}
Ok(())
}
}
/// Construct a RepeatDisplay that will repeatedly call `fmt_one` with a formatter `count` times,
/// separated by `sep`.
///
/// # Example
///
/// ```rust
/// # use sql_support::repeat_display;
/// assert_eq!(format!("{}", repeat_display(1, ",", |i, f| write!(f, "({},?)", i))),
/// "(0,?)");
/// assert_eq!(format!("{}", repeat_display(2, ",", |i, f| write!(f, "({},?)", i))),
/// "(0,?),(1,?)");
/// assert_eq!(format!("{}", repeat_display(3, ",", |i, f| write!(f, "({},?)", i))),
/// "(0,?),(1,?),(2,?)");
/// ```
#[inline]
pub fn repeat_display<F>(count: usize, sep: &str, fmt_one: F) -> RepeatDisplay<'_, F>
where
F: Fn(usize, &mut fmt::Formatter<'_>) -> fmt::Result,
{
RepeatDisplay {
count,
sep,
fmt_one,
}
}
/// Returns a value that formats as `count` instances of `?` separated by commas.
///
/// # Example
///
/// ```rust
/// # use sql_support::repeat_sql_vars;
/// assert_eq!(format!("{}", repeat_sql_vars(0)), "");
/// assert_eq!(format!("{}", repeat_sql_vars(1)), "?");
/// assert_eq!(format!("{}", repeat_sql_vars(2)), "?,?");
/// assert_eq!(format!("{}", repeat_sql_vars(3)), "?,?,?");
/// ```
pub fn repeat_sql_vars(count: usize) -> impl fmt::Display {
repeat_display(count, ",", |_, f| write!(f, "?"))
}
/// Returns a value that formats as `count` instances of `(?)` separated by commas.
///
/// # Example
///
/// ```rust
/// # use sql_support::repeat_sql_values;
/// assert_eq!(format!("{}", repeat_sql_values(0)), "");
/// assert_eq!(format!("{}", repeat_sql_values(1)), "(?)");
/// assert_eq!(format!("{}", repeat_sql_values(2)), "(?),(?)");
/// assert_eq!(format!("{}", repeat_sql_values(3)), "(?),(?),(?)");
/// ```
///
pub fn repeat_sql_values(count: usize) -> impl fmt::Display {
// We could also implement this as `repeat_sql_multi_values(count, 1)`,
// but this is faster and no less clear IMO.
repeat_display(count, ",", |_, f| write!(f, "(?)"))
}
/// Returns a value that formats as `num_values` instances of `(?,?,?,...)` (where there are
/// `vars_per_value` question marks separated by commas in between the `?`s).
///
/// Panics if `vars_per_value` is zero (however, `num_values` is allowed to be zero).
///
/// # Example
///
/// ```rust
/// # use sql_support::repeat_multi_values;
/// assert_eq!(format!("{}", repeat_multi_values(0, 2)), "");
/// assert_eq!(format!("{}", repeat_multi_values(1, 5)), "(?,?,?,?,?)");
/// assert_eq!(format!("{}", repeat_multi_values(2, 3)), "(?,?,?),(?,?,?)");
/// assert_eq!(format!("{}", repeat_multi_values(3, 1)), "(?),(?),(?)");
/// ```
pub fn repeat_multi_values(num_values: usize, vars_per_value: usize) -> impl fmt::Display {
assert_ne!(
vars_per_value, 0,
"Illegal value for `vars_per_value`, must not be zero"
);
repeat_display(num_values, ",", move |_, f| {
write!(f, "({})", repeat_sql_vars(vars_per_value))
})
}

View File

@ -1 +1 @@
{"files":{"Cargo.toml":"b5cc525d2aa129f84cb3f729a579217591c7705e2be78dbd348a95fc354831be","src/lib.rs":"729e562be4e63ec7db2adc00753a019ae77c11ce82637a893ea18122580c3c98","src/rusqlite_support.rs":"827d314605d8c741efdf238a0780a891c88bc56026a3e6dcfa534772a4852fb3","src/serde_support.rs":"519b5eb59ca7be555d522f2186909db969069dc9586a5fe4047d4ec176b2368a"},"package":null}
{"files":{"Cargo.toml":"fec1d023581c5e34b5669c1b42efd11819eba4c3c29eca1f6095f6044a1fa5ae","src/lib.rs":"729e562be4e63ec7db2adc00753a019ae77c11ce82637a893ea18122580c3c98","src/rusqlite_support.rs":"827d314605d8c741efdf238a0780a891c88bc56026a3e6dcfa534772a4852fb3","src/serde_support.rs":"519b5eb59ca7be555d522f2186909db969069dc9586a5fe4047d4ec176b2368a"},"package":null}

View File

@ -6,7 +6,7 @@ license = "MPL-2.0"
edition = "2018"
[dependencies]
rusqlite = { version = "0.21.0", optional = true }
rusqlite = { version = "0.23.1", optional = true }
serde = { version = "1.0.104", optional = true }
rand = { version = "0.7", optional = true }
base64 = { version = "0.12.0", optional = true }

View File

@ -1 +1 @@
{"files":{"Cargo.toml":"326b1c017a76b1987e34c6dde0fa57f2c85d5de23a9f0cf1dfb029cc99d34471","README.md":"396105211d8ce7f40b05d8062d7ab55d99674555f3ac81c061874ae26656ed7e","src/changeset.rs":"442aa92b5130ec0f8f2b0054acb399c547380e0060015cbf4ca7a72027440d54","src/client.rs":"6be4f550ade823fafc350c5490e031f90a4af833a9bba9739b05568464255a74","src/lib.rs":"9abce82e0248c8aa7e3d55b7db701b95e8f337f6e5d1319381f995a0b708400d","src/payload.rs":"09db1a444e7893990a4f03cb16263b9c15abc9e48ec4f1343227be1b490865a5","src/request.rs":"9e656ec487e53c7485643687e605d73bb25e138056e920d6f4b7d63fc6a8c460","src/server_timestamp.rs":"43d1b98a90e55e49380a0b66c209c9eb393e2aeaa27d843a4726d93cdd4cea02","src/store.rs":"10e215dd24270b6bec10903ac1d5274ce997eb437134f43be7de44e36fb9d1e4","src/telemetry.rs":"027befb099a6fcded3457f7e566296548a0898ff613267190621856b9ef288f6"},"package":null}
{"files":{"Cargo.toml":"656c4c4af39bcf924098be33996360250f9610ee3a4090b8152b68bdad03c46e","README.md":"396105211d8ce7f40b05d8062d7ab55d99674555f3ac81c061874ae26656ed7e","src/bridged_engine.rs":"b4d45cd43db3e5926df614ae9706b8d1a5bb96860577463d05b56a4213532ec1","src/changeset.rs":"442aa92b5130ec0f8f2b0054acb399c547380e0060015cbf4ca7a72027440d54","src/client.rs":"6be4f550ade823fafc350c5490e031f90a4af833a9bba9739b05568464255a74","src/lib.rs":"c1ca44e7bb6477b8018bd554479021dbf52754e64577185b3f7e208ae45bf754","src/payload.rs":"09db1a444e7893990a4f03cb16263b9c15abc9e48ec4f1343227be1b490865a5","src/request.rs":"9e656ec487e53c7485643687e605d73bb25e138056e920d6f4b7d63fc6a8c460","src/server_timestamp.rs":"43d1b98a90e55e49380a0b66c209c9eb393e2aeaa27d843a4726d93cdd4cea02","src/store.rs":"10e215dd24270b6bec10903ac1d5274ce997eb437134f43be7de44e36fb9d1e4","src/telemetry.rs":"027befb099a6fcded3457f7e566296548a0898ff613267190621856b9ef288f6"},"package":null}

View File

@ -16,3 +16,5 @@ log = "0.4"
ffi-support = "0.4"
url = "2.1"
failure = "0.1.6"
interrupt-support = { path = "../interrupt" }

View File

@ -0,0 +1,197 @@
/* 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/. */
use std::{sync::Mutex, sync::MutexGuard, sync::PoisonError};
use interrupt_support::Interruptee;
/// A bridged Sync engine implements all the methods needed to support
/// Desktop Sync.
pub trait BridgedEngine {
/// The type returned for errors.
type Error;
/// Initializes the engine. This is called once, when the engine is first
/// created, and guaranteed to be called before any of the other methods.
/// The default implementation does nothing.
fn initialize(&self) -> Result<(), Self::Error> {
Ok(())
}
/// Returns the last sync time, in milliseconds, for this engine's
/// collection. This is called before each sync, to determine the lower
/// bound for new records to fetch from the server.
fn last_sync(&self) -> Result<i64, Self::Error>;
/// Sets the last sync time, in milliseconds. This is called throughout
/// the sync, to fast-forward the stored last sync time to match the
/// timestamp on the uploaded records.
fn set_last_sync(&self, last_sync_millis: i64) -> Result<(), Self::Error>;
/// Returns the sync ID for this engine's collection. This is only used in
/// tests.
fn sync_id(&self) -> Result<Option<String>, Self::Error>;
/// Resets the sync ID for this engine's collection, returning the new ID.
/// As a side effect, implementations should reset all local Sync state,
/// as in `reset`.
fn reset_sync_id(&self) -> Result<String, Self::Error>;
/// Ensures that the locally stored sync ID for this engine's collection
/// matches the `new_sync_id` from the server. If the two don't match,
/// implementations should reset all local Sync state, as in `reset`.
/// This method returns the assigned sync ID, which can be either the
/// `new_sync_id`, or a different one if the engine wants to force other
/// devices to reset their Sync state for this collection the next time they
/// sync.
fn ensure_current_sync_id(&self, new_sync_id: &str) -> Result<String, Self::Error>;
/// Stages a batch of incoming Sync records. This is called multiple
/// times per sync, once for each batch. Implementations can use the
/// signal to check if the operation was aborted, and cancel any
/// pending work.
fn store_incoming(
&self,
incoming_cleartexts: &[String],
signal: &dyn Interruptee,
) -> Result<(), Self::Error>;
/// Applies all staged records, reconciling changes on both sides and
/// resolving conflicts. Returns a list of records to upload.
fn apply(&self, signal: &dyn Interruptee) -> Result<ApplyResults, Self::Error>;
/// Indicates that the given record IDs were uploaded successfully to the
/// server. This is called multiple times per sync, once for each batch
/// upload.
fn set_uploaded(
&self,
server_modified_millis: i64,
ids: &[String],
signal: &dyn Interruptee,
) -> Result<(), Self::Error>;
/// Indicates that all records have been uploaded. At this point, any record
/// IDs marked for upload that haven't been passed to `set_uploaded`, can be
/// assumed to have failed: for example, because the server rejected a record
/// with an invalid TTL or sort index.
fn sync_finished(&self, signal: &dyn Interruptee) -> Result<(), Self::Error>;
/// Resets all local Sync state, including any change flags, mirrors, and
/// the last sync time, such that the next sync is treated as a first sync
/// with all new local data. Does not erase any local user data.
fn reset(&self) -> Result<(), Self::Error>;
/// Erases all local user data for this collection, and any Sync metadata.
/// This method is destructive, and unused for most collections.
fn wipe(&self) -> Result<(), Self::Error>;
/// Tears down the engine. The opposite of `initialize`, `finalize` is
/// called when an engine is disabled, or otherwise no longer needed. The
/// default implementation does nothing.
fn finalize(&self) -> Result<(), Self::Error> {
Ok(())
}
}
#[derive(Clone, Debug, Default)]
pub struct ApplyResults {
/// List of records
pub records: Vec<String>,
/// The number of incoming records whose contents were merged because they
/// changed on both sides. None indicates we aren't reporting this
/// information.
pub num_reconciled: Option<usize>,
}
impl ApplyResults {
pub fn new(records: Vec<String>, num_reconciled: impl Into<Option<usize>>) -> Self {
Self {
records,
num_reconciled: num_reconciled.into(),
}
}
}
// Shorthand for engines that don't care.
impl From<Vec<String>> for ApplyResults {
fn from(records: Vec<String>) -> Self {
Self {
records,
num_reconciled: None,
}
}
}
/// A blanket implementation of `BridgedEngine` for any `Mutex<BridgedEngine>`.
/// This is provided for convenience, since we expect most bridges to hold
/// their engines in an `Arc<Mutex<impl BridgedEngine>>`.
impl<E> BridgedEngine for Mutex<E>
where
E: BridgedEngine,
E::Error: for<'a> From<PoisonError<MutexGuard<'a, E>>>,
{
type Error = E::Error;
fn initialize(&self) -> Result<(), Self::Error> {
self.lock()?.initialize()
}
fn last_sync(&self) -> Result<i64, Self::Error> {
self.lock()?.last_sync()
}
fn set_last_sync(&self, millis: i64) -> Result<(), Self::Error> {
self.lock()?.set_last_sync(millis)
}
fn store_incoming(
&self,
incoming_cleartexts: &[String],
signal: &dyn Interruptee,
) -> Result<(), Self::Error> {
self.lock()?.store_incoming(incoming_cleartexts, signal)
}
fn apply(&self, signal: &dyn Interruptee) -> Result<ApplyResults, Self::Error> {
self.lock()?.apply(signal)
}
fn set_uploaded(
&self,
server_modified_millis: i64,
ids: &[String],
signal: &dyn Interruptee,
) -> Result<(), Self::Error> {
self.lock()?
.set_uploaded(server_modified_millis, ids, signal)
}
fn sync_finished(&self, signal: &dyn Interruptee) -> Result<(), Self::Error> {
self.lock()?.sync_finished(signal)
}
fn reset(&self) -> Result<(), Self::Error> {
self.lock()?.reset()
}
fn wipe(&self) -> Result<(), Self::Error> {
self.lock()?.wipe()
}
fn finalize(&self) -> Result<(), Self::Error> {
self.lock()?.finalize()
}
fn sync_id(&self) -> Result<Option<String>, Self::Error> {
self.lock()?.sync_id()
}
fn reset_sync_id(&self) -> Result<String, Self::Error> {
self.lock()?.reset_sync_id()
}
fn ensure_current_sync_id(&self, new_sync_id: &str) -> Result<String, Self::Error> {
self.lock()?.ensure_current_sync_id(new_sync_id)
}
}

View File

@ -3,6 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#![warn(rust_2018_idioms)]
mod bridged_engine;
mod changeset;
pub mod client;
mod payload;
@ -11,6 +12,7 @@ mod server_timestamp;
mod store;
pub mod telemetry;
pub use bridged_engine::{ApplyResults, BridgedEngine};
pub use changeset::{IncomingChangeset, OutgoingChangeset, RecordChangeset};
pub use payload::Payload;
pub use request::{CollectionRequest, RequestOrder};

View File

@ -0,0 +1 @@
{"files":{"Cargo.toml":"23ed53b7db21b1015cbb1deafe950f80068691dfe7f2256e9f9237a94910a4d8","README.md":"1fd617294339930ee1ad5172377648b268cce0216fc3971facbfe7c6839e9ab1","build.rs":"2b827a62155a3d724cdb4c198270ea467439e537403f82fa873321ac55a69a63","sql/create_schema.sql":"d50b22cb17fc5d4e2aa4d001e853bd2f67eb3ffdbb1ac29013067dceacaec80e","src/api.rs":"e045fd8f39a8774f5bd05054dcc50381bbd112ffc6638c42540792cdd001811d","src/db.rs":"7f74bcbd1f5bef3bc64f6eccbc89bccda51e130537692fed9cc9a417ff100c29","src/error.rs":"86ba215ec5a889d1ccca9dcd141e42f75914744a4803598ccf3894da4a7f7475","src/lib.rs":"1d40f86404bfd1bb70abe778fa306d8ad937fb47e281a6844975f1b2f37a6468","src/schema.rs":"0d5291ba9a553706e81d27a0d65618100b1bcb16edafe34139c715f84c84f1b4","src/store.rs":"dc1836bfa88b164783d218595358ba531de8eb87165ba3c1ea4075f71c1c3e21"},"package":null}

View File

@ -0,0 +1,38 @@
[package]
name = "webext-storage"
edition = "2018"
version = "0.1.0"
authors = ["sync-team@mozilla.com"]
license = "MPL-2.0"
[features]
log_query_plans = ["sql-support/log_query_plans"]
default = []
[dependencies]
error-support = { path = "../support/error" }
failure = "0.1.6"
interrupt-support = { path = "../support/interrupt" }
lazy_static = "1.4.0"
log = "0.4"
serde = "1"
serde_json = "1"
serde_derive = "1"
sql-support = { path = "../support/sql" }
sync-guid = { path = "../support/guid", features = ["rusqlite_support", "random"] }
url = { version = "2.1", features = ["serde"] }
[dependencies.rusqlite]
version = "0.23.1"
features = ["functions", "bundled"]
[dev-dependencies]
env_logger = "0.7.0"
# A *direct* dep on the -sys crate is required for our build.rs
# to see the DEP_SQLITE3_LINK_TARGET env var that cargo sets
# on its behalf.
libsqlite3-sys = "0.18.0"
[build-dependencies]
nss_build_common = { path = "../support/rc_crypto/nss/nss_build_common" }

View File

@ -0,0 +1,91 @@
# WebExtension Storage Component
The WebExtension Storage component can be used to power an implementation of the
[`chrome.storage.sync`](https://developer.chrome.com/extensions/storage) WebExtension API,
which gives each WebExtensions its own private key-value store that will sync between a user's
devices. This particular implementation sits atop [Firefox Sync](../sync_manager/README.md).
With a small amount of work, this component would also be capable of powering an implementation
of `chrome.storage.local`, but this is not an explicit goal at this stage.
* [Features](#features)
* [Using the component](#using-the-component)
* [Working on the component](#working-on-the-component)
## Features
The WebExtension Storage component offers:
1. Local storage of key-value data indexed by WebExtension ID.
1. Basic Create, Read, Update and Delete (CRUD) operations for items in the database.
1. Syncing of stored data between applications, via Firefox Sync.
The component ***does not*** offer, but may offer in the future:
1. Separate storage for key-value data that does not sync, per the
`chrome.storage.local` WebExtension API.
1. Import functionality from previous WebExtension storage implementations backed by
[Kinto](https://kinto-storage.org).
The component ***does not*** offer, and we have no concrete plans to offer:
1. Any facilities for loading or running WebExtensions, or exposing this data to them.
1. Any helpers to secure data access between different WebExtensions.
As a consuming application, you will need to implement code that plumbs this component in to your
WebExtensions infrastructure, so that each WebExtension gets access to its own data (and only its
own data) stored in this component.
## Using the component
### Prerequisites
To use this component for local storage of WebExtension data, you will need to know how to integrate appservices components
into an application on your target platform:
* **Firefox Desktop**: There's some custom bridging code in mozilla-central.
* **Android**: Bindings not yet available; please reach out on slack to discuss!
* **iOS**: Bindings not yet available; please reach out on slack to discuss!
* **Other Platforms**: We don't know yet; please reach out on slack to discuss!
### Core Concepts
* We assume each WebExtension is uniquely identified by an immutable **extension id**.
* A **WebExtenstion Store** is a database that maps extension ids to key-value JSON maps, one per extension.
It exposes methods that mirror those of the [`chrome.storage` spec](https://developer.chrome.com/extensions/storage)
(e.g. `get`, `set`, and `delete`) and which take an extension id as their first argument.
## Working on the component
### Prerequisites
To effectively work on the WebExtension Storage component, you will need to be familiar with:
* Our general [guidelines for contributors](../../docs/contributing.md).
* The [core concepts](#core-concepts) for users of the component, outlined above.
* The way we [generate ffi bindings](../../docs/howtos/building-a-rust-component.md) and expose them to
[Kotlin](../../docs/howtos/exposing-rust-components-to-kotlin.md) and
[Swift](../../docs/howtos/exposing-rust-components-to-swift.md).
* The key ideas behind [how Firefox Sync works](../../docs/synconomicon/) and the [sync15 crate](../sync15/README.md).
### Storage Overview
This component stores WebExtension data in a SQLite database, one row per extension id.
The key-value map data for each extension is stored as serialized JSON in a `TEXT` field;
this is nice and simple and helps ensure that the stored data has the semantics we want,
which are pretty much just the semantics of JSON.
For syncing, we maintain a "mirror" table which contains one item per record known to
exist on the server. These items are identified by a randomly-generated GUID, in order
to hide the raw extension ids from the sync server.
When uploading records to the server, we write one
[encrypted BSO](https://mozilla-services.readthedocs.io/en/latest/sync/storageformat5.html#collection-records)
per extension. Its server-visible id is the randomly-generated GUID, and its encrypted payload
contains the plaintext extension id and corresponding key-value map data.
The end result is something like this (highly simplified!) diagram:
[![storage overview diagram](https://docs.google.com/drawings/d/e/2PACX-1vSvCk0uJlXYTtWHmjxhL-mNLGL_q7F50LavltedREH8Ijuqjl875jKYd9PdJ5SrD3mhVOFqANs6A_NB/pub?w=727&h=546)](https://docs.google.com/drawings/d/1MlkFQJ7SUnW4WSEAF9e-2O34EnsAwUFi3Xcf0Lj3Hc8/)
The details of the encryption are handled by the [sync15 crate](../sync15/README.md), following
the formats defied in [sync storage format v5](https://mozilla-services.readthedocs.io/en/latest/sync/storageformat5.html#collection-records).

View File

@ -0,0 +1,16 @@
/* 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/. */
//! Work around the fact that `sqlcipher` might get enabled by a cargo feature
//! another crate in the workspace needs, without setting up nss. (This is a
//! gross hack).
fn main() {
println!("cargo:rerun-if-changed=build.rs");
// Ugh. This is really really dumb. We don't care about sqlcipher at all. really
if nss_build_common::env_str("DEP_SQLITE3_LINK_TARGET") == Some("sqlcipher".into()) {
// If NSS_DIR isn't set, we don't really care, ignore the Err case.
let _ = nss_build_common::link_nss();
}
}

View File

@ -0,0 +1,49 @@
-- 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/.
-- This is a very simple schema for a chrome.storage.* implementation. At time
-- of writing, only chrome.storage.sync is supported, but this can be trivially
-- enhanced to support chrome.storage.local (the api is identical, it's just a
-- different "bucket" and doesn't sync).
--
-- Even though the spec allows for a single extension to have any number of
-- "keys", we've made the decision to store all keys for a given extension in a
-- single row as a JSON representation of all keys and values.
-- We've done this primarily due to:
-- * The shape of the API is very JSON, and it almost encourages multiple keys
-- to be fetched at one time.
-- * The defined max sizes that extensions are allowed to store using this API
-- is sufficiently small that we don't have many concerns around record sizes.
-- * We'd strongly prefer to keep one record per extension when syncing this
-- data, so having the local store in this shape makes syncing easier.
CREATE TABLE IF NOT EXISTS storage_sync_data (
ext_id TEXT NOT NULL PRIMARY KEY,
/* The JSON payload. NULL means it's a tombstone */
data TEXT,
/* Same "sync change counter" strategy used by other components. */
sync_change_counter INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE IF NOT EXISTS storage_sync_mirror (
guid TEXT NOT NULL PRIMARY KEY,
/* The extension_id is explicitly not the GUID used on the server.
We may end up making this a regular foreign-key relationship back to
storage_sync_data, although maybe not - the ext_id may not exist in
storage_sync_data at the time we populate this table.
We can iterate here as we ramp up sync support.
*/
ext_id TEXT NOT NULL UNIQUE,
/* The JSON payload. We *do* allow NULL here - it means "deleted" */
data TEXT
);
-- This table holds key-value metadata - primarily for sync.
CREATE TABLE IF NOT EXISTS meta (
key TEXT PRIMARY KEY,
value NOT NULL
) WITHOUT ROWID;

View File

@ -0,0 +1,480 @@
/* 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/. */
use crate::error::*;
use rusqlite::{Connection, Transaction};
use serde::{ser::SerializeMap, Serialize, Serializer};
use serde_json::{Map, Value as JsonValue};
use sql_support::{self, ConnExt};
// These constants are defined by the chrome.storage.sync spec.
const QUOTA_BYTES: usize = 102_400;
const QUOTA_BYTES_PER_ITEM: usize = 8_192;
const MAX_ITEMS: usize = 512;
// Note there are also constants for "operations per minute" etc, which aren't
// enforced here.
type JsonMap = Map<String, JsonValue>;
fn get_from_db(conn: &Connection, ext_id: &str) -> Result<Option<JsonMap>> {
Ok(
match conn.try_query_one::<String>(
"SELECT data FROM storage_sync_data
WHERE ext_id = :ext_id",
&[(":ext_id", &ext_id)],
true,
)? {
Some(s) => match serde_json::from_str(&s)? {
JsonValue::Object(m) => Some(m),
// we could panic here as it's theoretically impossible, but we
// might as well treat it as not existing...
_ => None,
},
None => None,
},
)
}
fn save_to_db(tx: &Transaction<'_>, ext_id: &str, val: &JsonValue) -> Result<()> {
// The quota is enforced on the byte count, which is what .len() returns.
let sval = val.to_string();
if sval.len() > QUOTA_BYTES {
return Err(ErrorKind::QuotaError(QuotaReason::TotalBytes).into());
}
// XXX - sync support will need to do the change_counter thing here.
tx.execute_named(
"INSERT OR REPLACE INTO storage_sync_data(ext_id, data)
VALUES (:ext_id, :data)",
&[(":ext_id", &ext_id), (":data", &sval)],
)?;
Ok(())
}
fn remove_from_db(tx: &Transaction<'_>, ext_id: &str) -> Result<()> {
// XXX - sync support will need to do the tombstone thing here.
tx.execute_named(
"DELETE FROM storage_sync_data
WHERE ext_id = :ext_id",
&[(":ext_id", &ext_id)],
)?;
Ok(())
}
// This is a "helper struct" for the callback part of the chrome.storage spec,
// but shaped in a way to make it more convenient from the rust side of the
// world. The strings are all json, we keeping them as strings here makes
// various things easier and avoid a round-trip to/from json/string.
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StorageValueChange {
#[serde(skip_serializing)]
key: String,
#[serde(skip_serializing_if = "Option::is_none")]
old_value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
new_value: Option<String>,
}
// This is, largely, a helper so that this serializes correctly as per the
// chrome.storage.sync spec. If not for custom serialization it should just
// be a plain vec
#[derive(Debug, Clone, PartialEq)]
pub struct StorageChanges {
changes: Vec<StorageValueChange>,
}
impl StorageChanges {
fn new() -> Self {
Self {
changes: Vec::new(),
}
}
fn with_capacity(n: usize) -> Self {
Self {
changes: Vec::with_capacity(n),
}
}
fn is_empty(&self) -> bool {
self.changes.is_empty()
}
fn push(&mut self, change: StorageValueChange) {
self.changes.push(change)
}
}
// and it serializes as a map.
impl Serialize for StorageChanges {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(self.changes.len()))?;
for change in &self.changes {
map.serialize_entry(&change.key, change)?;
}
map.end()
}
}
/// The implementation of `storage[.sync].set()`. On success this returns the
/// StorageChanges defined by the chrome API - it's assumed the caller will
/// arrange to deliver this to observers as defined in that API.
pub fn set(tx: &Transaction<'_>, ext_id: &str, val: JsonValue) -> Result<StorageChanges> {
let val_map = match val {
JsonValue::Object(m) => m,
// Not clear what the error semantics should be yet. For now, pretend an empty map.
_ => Map::new(),
};
let mut current = get_from_db(tx, ext_id)?.unwrap_or_default();
let mut changes = StorageChanges::with_capacity(val_map.len());
// iterate over the value we are adding/updating.
for (k, v) in val_map.into_iter() {
let old_value = current.remove(&k);
if current.len() >= MAX_ITEMS {
return Err(ErrorKind::QuotaError(QuotaReason::MaxItems).into());
}
// Setup the change entry for this key, and we can leverage it to check
// for the quota.
let new_value_s = v.to_string();
// Reading the chrome docs literally re the quota, the length of the key
// is just the string len, but the value is the json val, as bytes
if k.len() + new_value_s.len() >= QUOTA_BYTES_PER_ITEM {
return Err(ErrorKind::QuotaError(QuotaReason::ItemBytes).into());
}
let change = StorageValueChange {
key: k.clone(),
old_value: old_value.map(|ov| ov.to_string()),
new_value: Some(new_value_s),
};
changes.push(change);
current.insert(k, v);
}
save_to_db(tx, ext_id, &JsonValue::Object(current))?;
Ok(changes)
}
// A helper which takes a param indicating what keys should be returned and
// converts that to a vec of real strings. Also returns "default" values to
// be used if no item exists for that key.
fn get_keys(keys: JsonValue) -> Vec<(String, Option<JsonValue>)> {
match keys {
JsonValue::String(s) => vec![(s, None)],
JsonValue::Array(keys) => {
// because nothing with json is ever simple, each key may not be
// a string. We ignore any which aren't.
keys.iter()
.filter_map(|v| v.as_str().map(|s| (s.to_string(), None)))
.collect()
}
JsonValue::Object(m) => m.into_iter().map(|(k, d)| (k, Some(d))).collect(),
_ => vec![],
}
}
/// The implementation of `storage[.sync].get()` - on success this always
/// returns a Json object.
pub fn get(conn: &Connection, ext_id: &str, keys: JsonValue) -> Result<JsonValue> {
// key is optional, or string or array of string or object keys
let maybe_existing = get_from_db(conn, ext_id)?;
let mut existing = match maybe_existing {
None => return Ok(JsonValue::Object(Map::new())),
Some(v) => v,
};
// take the quick path for null, where we just return the entire object.
if keys.is_null() {
return Ok(JsonValue::Object(existing));
}
// OK, so we need to build a list of keys to get.
let keys_and_defaults = get_keys(keys);
let mut result = Map::with_capacity(keys_and_defaults.len());
for (key, maybe_default) in keys_and_defaults {
// XXX - If a key is requested that doesn't exist, we have 2 options:
// (1) have the key in the result with the value null, or (2) the key
// simply doesn't exist in the result. We assume (2), but should verify
// that's what chrome does.
if let Some(v) = existing.remove(&key) {
result.insert(key, v);
} else if let Some(def) = maybe_default {
result.insert(key, def);
}
}
Ok(JsonValue::Object(result))
}
/// The implementation of `storage[.sync].remove()`. On success this returns the
/// StorageChanges defined by the chrome API - it's assumed the caller will
/// arrange to deliver this to observers as defined in that API.
pub fn remove(tx: &Transaction<'_>, ext_id: &str, keys: JsonValue) -> Result<StorageChanges> {
let mut existing = match get_from_db(tx, ext_id)? {
None => return Ok(StorageChanges::new()),
Some(v) => v,
};
let keys_and_defs = get_keys(keys);
let mut result = StorageChanges::with_capacity(keys_and_defs.len());
for (key, _) in keys_and_defs {
if let Some(v) = existing.remove(&key) {
result.push(StorageValueChange {
key,
old_value: Some(v.to_string()),
new_value: None,
});
}
}
if !result.is_empty() {
save_to_db(tx, ext_id, &JsonValue::Object(existing))?;
}
Ok(result)
}
/// The implementation of `storage[.sync].clear()`. On success this returns the
/// StorageChanges defined by the chrome API - it's assumed the caller will
/// arrange to deliver this to observers as defined in that API.
pub fn clear(tx: &Transaction<'_>, ext_id: &str) -> Result<StorageChanges> {
let existing = match get_from_db(tx, ext_id)? {
None => return Ok(StorageChanges::new()),
Some(v) => v,
};
let mut result = StorageChanges::with_capacity(existing.len());
for (key, val) in existing.into_iter() {
result.push(StorageValueChange {
key: key.to_string(),
new_value: None,
old_value: Some(val.to_string()),
});
}
remove_from_db(tx, ext_id)?;
Ok(result)
}
// TODO - get_bytes_in_use()
#[cfg(test)]
mod tests {
use super::*;
use crate::db::test::new_mem_db;
use serde_json::json;
#[test]
fn test_serialize_storage_changes() -> Result<()> {
let c = StorageChanges {
changes: vec![StorageValueChange {
key: "key".to_string(),
old_value: Some("old".to_string()),
new_value: None,
}],
};
assert_eq!(serde_json::to_string(&c)?, r#"{"key":{"oldValue":"old"}}"#);
Ok(())
}
fn make_changes(changes: &[(&str, Option<JsonValue>, Option<JsonValue>)]) -> StorageChanges {
let mut r = StorageChanges::with_capacity(changes.len());
for (name, old_value, new_value) in changes {
r.push(StorageValueChange {
key: (*name).to_string(),
old_value: old_value.as_ref().map(|v| v.to_string()),
new_value: new_value.as_ref().map(|v| v.to_string()),
});
}
r
}
#[test]
fn test_simple() -> Result<()> {
let ext_id = "x";
let db = new_mem_db();
let mut conn = db.writer.lock().unwrap();
let tx = conn.transaction()?;
// an empty store.
for q in vec![
JsonValue::Null,
json!("foo"),
json!(["foo"]),
json!({ "foo": null }),
json!({"foo": "default"}),
]
.into_iter()
{
assert_eq!(get(&tx, &ext_id, q)?, json!({}));
}
// Single item in the store.
set(&tx, &ext_id, json!({"foo": "bar" }))?;
for q in vec![
JsonValue::Null,
json!("foo"),
json!(["foo"]),
json!({ "foo": null }),
json!({"foo": "default"}),
]
.into_iter()
{
assert_eq!(get(&tx, &ext_id, q)?, json!({"foo": "bar" }));
}
// more complex stuff, including changes checking.
assert_eq!(
set(&tx, &ext_id, json!({"foo": "new", "other": "also new" }))?,
make_changes(&[
("foo", Some(json!("bar")), Some(json!("new"))),
("other", None, Some(json!("also new")))
])
);
assert_eq!(
get(&tx, &ext_id, JsonValue::Null)?,
json!({"foo": "new", "other": "also new"})
);
assert_eq!(get(&tx, &ext_id, json!("foo"))?, json!({"foo": "new"}));
assert_eq!(
get(&tx, &ext_id, json!(["foo", "other"]))?,
json!({"foo": "new", "other": "also new"})
);
assert_eq!(
get(&tx, &ext_id, json!({"foo": null, "default": "yo"}))?,
json!({"foo": "new", "default": "yo"})
);
assert_eq!(
remove(&tx, &ext_id, json!("foo"))?,
make_changes(&[("foo", Some(json!("new")), None)]),
);
// XXX - other variants.
assert_eq!(
clear(&tx, &ext_id)?,
make_changes(&[("other", Some(json!("also new")), None)]),
);
assert_eq!(get(&tx, &ext_id, JsonValue::Null)?, json!({}));
Ok(())
}
#[test]
fn test_check_get_impl() -> Result<()> {
// This is a port of checkGetImpl in test_ext_storage.js in Desktop.
let ext_id = "x";
let db = new_mem_db();
let mut conn = db.writer.lock().unwrap();
let tx = conn.transaction()?;
let prop = "test-prop";
let value = "test-value";
set(&tx, ext_id, json!({ prop: value }))?;
// this is the checkGetImpl part!
let mut data = get(&tx, &ext_id, json!(null))?;
assert_eq!(value, json!(data[prop]), "null getter worked for {}", prop);
data = get(&tx, &ext_id, json!(prop))?;
assert_eq!(
value,
json!(data[prop]),
"string getter worked for {}",
prop
);
assert_eq!(
data.as_object().unwrap().len(),
1,
"string getter should return an object with a single property"
);
data = get(&tx, &ext_id, json!([prop]))?;
assert_eq!(value, json!(data[prop]), "array getter worked for {}", prop);
assert_eq!(
data.as_object().unwrap().len(),
1,
"array getter with a single key should return an object with a single property"
);
// checkGetImpl() uses `{ [prop]: undefined }` - but json!() can't do that :(
// Hopefully it's just testing a simple object, so we use `{ prop: null }`
data = get(&tx, &ext_id, json!({ prop: null }))?;
assert_eq!(
value,
json!(data[prop]),
"object getter worked for {}",
prop
);
assert_eq!(
data.as_object().unwrap().len(),
1,
"object getter with a single key should return an object with a single property"
);
Ok(())
}
#[test]
fn test_bug_1621162() -> Result<()> {
// apparently Firefox, unlike Chrome, will not optimize the changes.
// See bug 1621162 for more!
let db = new_mem_db();
let mut conn = db.writer.lock().unwrap();
let tx = conn.transaction()?;
let ext_id = "xyz";
set(&tx, &ext_id, json!({"foo": "bar" }))?;
assert_eq!(
set(&tx, &ext_id, json!({"foo": "bar" }))?,
make_changes(&[("foo", Some(json!("bar")), Some(json!("bar")))]),
);
Ok(())
}
#[test]
fn test_quota_maxitems() -> Result<()> {
let db = new_mem_db();
let mut conn = db.writer.lock().unwrap();
let tx = conn.transaction()?;
let ext_id = "xyz";
for i in 1..MAX_ITEMS + 1 {
set(
&tx,
&ext_id,
json!({ format!("key-{}", i): format!("value-{}", i) }),
)?;
}
let e = set(&tx, &ext_id, json!({"another": "another"})).unwrap_err();
match e.kind() {
ErrorKind::QuotaError(QuotaReason::MaxItems) => {}
_ => panic!("unexpected error type"),
};
Ok(())
}
#[test]
fn test_quota_bytesperitem() -> Result<()> {
let db = new_mem_db();
let mut conn = db.writer.lock().unwrap();
let tx = conn.transaction()?;
let ext_id = "xyz";
// A string 5 bytes less than the max. This should be counted as being
// 3 bytes less than the max as the quotes are counted.
let val = "x".repeat(QUOTA_BYTES_PER_ITEM - 5);
// Key length doesn't push it over.
set(&tx, &ext_id, json!({ "x": val }))?;
// Key length does push it over.
let e = set(&tx, &ext_id, json!({ "xxxx": val })).unwrap_err();
match e.kind() {
ErrorKind::QuotaError(QuotaReason::ItemBytes) => {}
_ => panic!("unexpected error type"),
};
Ok(())
}
}

View File

@ -0,0 +1,230 @@
/* 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/. */
use crate::error::*;
use crate::schema;
use rusqlite::types::{FromSql, ToSql};
use rusqlite::Connection;
use rusqlite::OpenFlags;
use sql_support::ConnExt;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use url::Url;
/// The entry-point to getting a database connection. No enforcement of this
/// as a singleton is made - that's up to the caller. If you make multiple
/// StorageDbs pointing at the same physical database, you are going to have a
/// bad time. We only support a single writer connection - so that's the only
/// thing we store. It's still a bit overkill, but there's only so many yaks
/// in a day.
pub struct StorageDb {
pub writer: Arc<Mutex<Connection>>,
}
impl StorageDb {
/// Create a new, or fetch an already open, StorageDb backed by a file on disk.
pub fn new(db_path: impl AsRef<Path>) -> Result<Self> {
let db_path = normalize_path(db_path)?;
Self::new_named(db_path)
}
/// Create a new, or fetch an already open, memory-based StorageDb. You must
/// provide a name, but you are still able to have a single writer and many
/// reader connections to the same memory DB open.
#[cfg(test)]
pub fn new_memory(db_path: &str) -> Result<Self> {
let name = PathBuf::from(format!("file:{}?mode=memory&cache=shared", db_path));
Self::new_named(name)
}
fn new_named(db_path: PathBuf) -> Result<Self> {
// We always create the read-write connection for an initial open so
// we can create the schema and/or do version upgrades.
let flags = OpenFlags::SQLITE_OPEN_NO_MUTEX
| OpenFlags::SQLITE_OPEN_URI
| OpenFlags::SQLITE_OPEN_CREATE
| OpenFlags::SQLITE_OPEN_READ_WRITE;
let conn = Connection::open_with_flags(db_path.clone(), flags)?;
match init_sql_connection(&conn, true) {
Ok(()) => Ok(Self {
writer: Arc::new(Mutex::new(conn)),
}),
Err(e) => {
// like with places, failure to upgrade means "you lose your data"
if let ErrorKind::DatabaseUpgradeError = e.kind() {
fs::remove_file(&db_path)?;
Self::new_named(db_path)
} else {
Err(e)
}
}
}
}
}
fn init_sql_connection(conn: &Connection, is_writable: bool) -> Result<()> {
let initial_pragmas = "
-- We don't care about temp tables being persisted to disk.
PRAGMA temp_store = 2;
-- we unconditionally want write-ahead-logging mode
PRAGMA journal_mode=WAL;
-- foreign keys seem worth enforcing!
PRAGMA foreign_keys = ON;
";
conn.execute_batch(initial_pragmas)?;
define_functions(&conn)?;
conn.set_prepared_statement_cache_capacity(128);
if is_writable {
let tx = conn.unchecked_transaction()?;
schema::init(&conn)?;
tx.commit()?;
};
Ok(())
}
fn define_functions(_c: &Connection) -> Result<()> {
Ok(())
}
// These should be somewhere else...
pub fn put_meta(db: &Connection, key: &str, value: &dyn ToSql) -> Result<()> {
db.conn().execute_named_cached(
"REPLACE INTO meta (key, value) VALUES (:key, :value)",
&[(":key", &key), (":value", value)],
)?;
Ok(())
}
pub fn get_meta<T: FromSql>(db: &Connection, key: &str) -> Result<Option<T>> {
let res = db.conn().try_query_one(
"SELECT value FROM meta WHERE key = :key",
&[(":key", &key)],
true,
)?;
Ok(res)
}
pub fn delete_meta(db: &Connection, key: &str) -> Result<()> {
db.conn()
.execute_named_cached("DELETE FROM meta WHERE key = :key", &[(":key", &key)])?;
Ok(())
}
// Utilities for working with paths.
// (From places_utils - ideally these would be shared, but the use of
// ErrorKind values makes that non-trivial.
/// `Path` is basically just a `str` with no validation, and so in practice it
/// could contain a file URL. Rusqlite takes advantage of this a bit, and says
/// `AsRef<Path>` but really means "anything sqlite can take as an argument".
///
/// Swift loves using file urls (the only support it has for file manipulation
/// is through file urls), so it's handy to support them if possible.
fn unurl_path(p: impl AsRef<Path>) -> PathBuf {
p.as_ref()
.to_str()
.and_then(|s| Url::parse(s).ok())
.and_then(|u| {
if u.scheme() == "file" {
u.to_file_path().ok()
} else {
None
}
})
.unwrap_or_else(|| p.as_ref().to_owned())
}
/// If `p` is a file URL, return it, otherwise try and make it one.
///
/// Errors if `p` is a relative non-url path, or if it's a URL path
/// that's isn't a `file:` URL.
pub fn ensure_url_path(p: impl AsRef<Path>) -> Result<Url> {
if let Some(u) = p.as_ref().to_str().and_then(|s| Url::parse(s).ok()) {
if u.scheme() == "file" {
Ok(u)
} else {
Err(ErrorKind::IllegalDatabasePath(p.as_ref().to_owned()).into())
}
} else {
let p = p.as_ref();
let u = Url::from_file_path(p).map_err(|_| ErrorKind::IllegalDatabasePath(p.to_owned()))?;
Ok(u)
}
}
/// As best as possible, convert `p` into an absolute path, resolving
/// all symlinks along the way.
///
/// If `p` is a file url, it's converted to a path before this.
fn normalize_path(p: impl AsRef<Path>) -> Result<PathBuf> {
let path = unurl_path(p);
if let Ok(canonical) = path.canonicalize() {
return Ok(canonical);
}
// It probably doesn't exist yet. This is an error, although it seems to
// work on some systems.
//
// We resolve this by trying to canonicalize the parent directory, and
// appending the requested file name onto that. If we can't canonicalize
// the parent, we return an error.
//
// Also, we return errors if the path ends in "..", if there is no
// parent directory, etc.
let file_name = path
.file_name()
.ok_or_else(|| ErrorKind::IllegalDatabasePath(path.clone()))?;
let parent = path
.parent()
.ok_or_else(|| ErrorKind::IllegalDatabasePath(path.clone()))?;
let mut canonical = parent.canonicalize()?;
canonical.push(file_name);
Ok(canonical)
}
// Helpers for tests
#[cfg(test)]
pub mod test {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
// A helper for our tests to get their own memory Api.
static ATOMIC_COUNTER: AtomicUsize = AtomicUsize::new(0);
pub fn new_mem_db() -> StorageDb {
let _ = env_logger::try_init();
let counter = ATOMIC_COUNTER.fetch_add(1, Ordering::Relaxed);
StorageDb::new_memory(&format!("test-api-{}", counter)).expect("should get an API")
}
}
#[cfg(test)]
mod tests {
use super::test::*;
use super::*;
// Sanity check that we can create a database.
#[test]
fn test_open() {
new_mem_db();
// XXX - should we check anything else? Seems a bit pointless, but if
// we move the meta functions away from here then it's better than
// nothing.
}
#[test]
fn test_meta() -> Result<()> {
let db = new_mem_db();
let writer = db.writer.lock().unwrap();
assert_eq!(get_meta::<String>(&writer, "foo")?, None);
put_meta(&writer, "foo", &"bar".to_string())?;
assert_eq!(get_meta(&writer, "foo")?, Some("bar".to_string()));
delete_meta(&writer, "foo")?;
assert_eq!(get_meta::<String>(&writer, "foo")?, None);
Ok(())
}
}

View File

@ -0,0 +1,65 @@
/* 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/. */
use failure::Fail;
use interrupt_support::Interrupted;
#[derive(Debug)]
pub enum QuotaReason {
TotalBytes,
ItemBytes,
MaxItems,
}
#[derive(Debug, Fail)]
pub enum ErrorKind {
#[fail(display = "Quota exceeded: {:?}", _0)]
QuotaError(QuotaReason),
#[fail(display = "Error parsing JSON data: {}", _0)]
JsonError(#[fail(cause)] serde_json::Error),
#[fail(display = "Error executing SQL: {}", _0)]
SqlError(#[fail(cause)] rusqlite::Error),
#[fail(display = "A connection of this type is already open")]
ConnectionAlreadyOpen,
#[fail(display = "An invalid connection type was specified")]
InvalidConnectionType,
#[fail(display = "IO error: {}", _0)]
IoError(#[fail(cause)] std::io::Error),
#[fail(display = "Operation interrupted")]
InterruptedError(#[fail(cause)] Interrupted),
#[fail(display = "Tried to close connection on wrong StorageApi instance")]
WrongApiForClose,
// This will happen if you provide something absurd like
// "/" or "" as your database path. For more subtley broken paths,
// we'll likely return an IoError.
#[fail(display = "Illegal database path: {:?}", _0)]
IllegalDatabasePath(std::path::PathBuf),
#[fail(display = "UTF8 Error: {}", _0)]
Utf8Error(#[fail(cause)] std::str::Utf8Error),
#[fail(display = "Database cannot be upgraded")]
DatabaseUpgradeError,
#[fail(display = "Database version {} is not supported", _0)]
UnsupportedDatabaseVersion(i64),
}
error_support::define_error! {
ErrorKind {
(JsonError, serde_json::Error),
(SqlError, rusqlite::Error),
(IoError, std::io::Error),
(InterruptedError, Interrupted),
(Utf8Error, std::str::Utf8Error),
}
}

View File

@ -0,0 +1,26 @@
/* 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/. */
#![allow(unknown_lints)]
#![warn(rust_2018_idioms)]
mod api;
pub mod db;
pub mod error;
mod schema;
pub mod store;
// This is what we roughly expect the "bridge" used by desktop to do.
// It's primarily here to avoid dead-code warnings (but I don't want to disable
// those warning, as stuff that remains after this is suspect!)
pub fn delme_demo_usage() -> error::Result<()> {
use serde_json::json;
let store = store::Store::new("webext-storage.db")?;
store.set("ext-id", json!({}))?;
store.get("ext-id", json!({}))?;
store.remove("ext-id", json!({}))?;
store.clear("ext-id")?;
Ok(())
}

View File

@ -0,0 +1,72 @@
/* 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/. */
// XXXXXX - This has been cloned from places/src/schema.rs, which has the
// comment:
// // This has been cloned from logins/src/schema.rs, on Thom's
// // wip-sync-sql-store branch.
// // We should work out how to turn this into something that can use a shared
// // db.rs.
//
// And we really should :) But not now.
use crate::error::Result;
use rusqlite::{Connection, NO_PARAMS};
use sql_support::ConnExt;
const VERSION: i64 = 1; // let's avoid bumping this and migrating for now!
const CREATE_SCHEMA_SQL: &str = include_str!("../sql/create_schema.sql");
fn get_current_schema_version(db: &Connection) -> Result<i64> {
Ok(db.query_one::<i64>("PRAGMA user_version")?)
}
pub fn init(db: &Connection) -> Result<()> {
let user_version = get_current_schema_version(db)?;
if user_version == 0 {
create(db)?;
} else if user_version != VERSION {
if user_version < VERSION {
panic!("no migrations yet!");
} else {
log::warn!(
"Loaded future schema version {} (we only understand version {}). \
Optimistically ",
user_version,
VERSION
);
// Downgrade the schema version, so that anything added with our
// schema is migrated forward when the newer library reads our
// database.
db.execute_batch(&format!("PRAGMA user_version = {};", VERSION))?;
}
}
Ok(())
}
fn create(db: &Connection) -> Result<()> {
log::debug!("Creating schema");
db.execute_batch(CREATE_SCHEMA_SQL)?;
db.execute(
&format!("PRAGMA user_version = {version}", version = VERSION),
NO_PARAMS,
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::test::new_mem_db;
#[test]
fn test_create_schema_twice() {
let db = new_mem_db();
let conn = db.writer.lock().unwrap();
conn.execute_batch(CREATE_SCHEMA_SQL)
.expect("should allow running twice");
}
}

View File

@ -0,0 +1,72 @@
/* 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/. */
use crate::api::{self, StorageChanges};
use crate::db::StorageDb;
use crate::error::*;
use std::path::Path;
use serde_json::Value as JsonValue;
pub struct Store {
db: StorageDb,
}
impl Store {
// functions to create instances.
pub fn new(db_path: impl AsRef<Path>) -> Result<Self> {
Ok(Self {
db: StorageDb::new(db_path)?,
})
}
#[cfg(test)]
pub fn new_memory(db_path: &str) -> Result<Self> {
Ok(Self {
db: StorageDb::new_memory(db_path)?,
})
}
// The "public API".
pub fn set(&self, ext_id: &str, val: JsonValue) -> Result<StorageChanges> {
let mut conn = self.db.writer.lock().unwrap();
let tx = conn.transaction()?;
let result = api::set(&tx, ext_id, val)?;
tx.commit()?;
Ok(result)
}
pub fn get(&self, ext_id: &str, keys: JsonValue) -> Result<JsonValue> {
// Don't care about transactions here.
let conn = self.db.writer.lock().unwrap();
api::get(&conn, ext_id, keys)
}
pub fn remove(&self, ext_id: &str, keys: JsonValue) -> Result<StorageChanges> {
let mut conn = self.db.writer.lock().unwrap();
let tx = conn.transaction()?;
let result = api::remove(&tx, ext_id, keys)?;
tx.commit()?;
Ok(result)
}
pub fn clear(&self, ext_id: &str) -> Result<StorageChanges> {
let mut conn = self.db.writer.lock().unwrap();
let tx = conn.transaction()?;
let result = api::clear(&tx, ext_id)?;
tx.commit()?;
Ok(result)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_send() {
fn ensure_send<T: Send>() {}
// Compile will fail if not send.
ensure_send::<Store>();
}
}

View File

@ -45,6 +45,7 @@ sqlite3_create_collation
sqlite3_create_collation16
sqlite3_create_function
sqlite3_create_function16
sqlite3_create_function_v2
sqlite3_create_module
sqlite3_data_count
sqlite3_db_config
@ -104,6 +105,7 @@ sqlite3_result_error
sqlite3_result_error16
sqlite3_result_error_code
sqlite3_result_error_nomem
sqlite3_result_error_toobig
sqlite3_result_int
sqlite3_result_int64
sqlite3_result_null
@ -112,6 +114,7 @@ sqlite3_result_text16
sqlite3_result_text16be
sqlite3_result_text16le
sqlite3_result_value
sqlite3_result_zeroblob
sqlite3_rollback_hook
sqlite3_set_authorizer
sqlite3_set_auxdata

View File

@ -32,6 +32,7 @@ wasm_library_sandboxing = ["gkrust-shared/wasm_library_sandboxing"]
webgpu = ["gkrust-shared/webgpu"]
remote_agent = ["gkrust-shared/remote"]
glean = ["gkrust-shared/glean"]
new_webext_storage = ["gkrust-shared/new_webext_storage"]
[dependencies]
bench-collections-gtest = { path = "../../../../xpcom/rust/gtest/bench-collections" }

View File

@ -33,6 +33,7 @@ wasm_library_sandboxing = ["gkrust-shared/wasm_library_sandboxing"]
webgpu = ["gkrust-shared/webgpu"]
remote_agent = ["gkrust-shared/remote"]
glean = ["gkrust-shared/glean"]
new_webext_storage = ["gkrust-shared/new_webext_storage"]
[dependencies]
gkrust-shared = { path = "shared" }

View File

@ -79,3 +79,6 @@ if CONFIG['MOZ_GLEAN']:
if CONFIG['MOZ_USING_WASM_SANDBOXING']:
gkrust_features += ['wasm_library_sandboxing']
if CONFIG['MOZ_NEW_WEBEXT_STORAGE']:
gkrust_features += ['new_webext_storage']

View File

@ -53,6 +53,7 @@ unic-langid = { version = "0.8", features = ["likelysubtags"] }
unic-langid-ffi = { path = "../../../../intl/locale/rust/unic-langid-ffi" }
fluent-langneg = { version = "0.12.1", features = ["cldr"] }
fluent-langneg-ffi = { path = "../../../../intl/locale/rust/fluent-langneg-ffi" }
webext-storage = { git = "https://github.com/mozilla/application-services", rev = "c17198fa5a88295f2cca722586c539280e10201c", optional = true }
# Note: `modern_sqlite` means rusqlite's bindings file be for a sqlite with
# version less than or equal to what we link to. This isn't a problem because we
@ -63,7 +64,7 @@ rusqlite = { version = "0.23.1", features = ["modern_sqlite", "in_gecko"] }
fluent = { version = "0.11" , features = ["fluent-pseudo"] }
fluent-ffi = { path = "../../../../intl/l10n/rust/fluent-ffi" }
sync15-traits = { git = "https://github.com/mozilla/application-services", rev = "120e51dd5f2aab4194cf0f7e93b2a8923f4504bb" }
sync15-traits = { git = "https://github.com/mozilla/application-services", rev = "c17198fa5a88295f2cca722586c539280e10201c" }
[build-dependencies]
rustc_version = "0.2"
@ -96,6 +97,7 @@ wasm_library_sandboxing = ["rlbox_lucet_sandbox"]
webgpu = ["wgpu_bindings"]
remote_agent = ["remote"]
glean = ["fog"]
new_webext_storage = ["webext-storage"]
[lib]
path = "lib.rs"

View File

@ -52,6 +52,9 @@ extern crate xulstore;
extern crate audio_thread_priority;
#[cfg(feature = "new_webext_storage")]
extern crate webext_storage_bridge;
#[cfg(feature = "webrtc")]
extern crate mdns_service;
extern crate neqo_glue;

View File

@ -1989,6 +1989,19 @@ def glean(milestone):
set_config('MOZ_GLEAN', True, when=glean)
set_define('MOZ_GLEAN', True, when=glean)
# New WebExtension `storage.sync` implementation in Rust
# ==============================================================
@depends(milestone)
def new_webext_storage(milestone):
if milestone.is_nightly:
return True
set_config('MOZ_NEW_WEBEXT_STORAGE', True, when=new_webext_storage)
set_define('MOZ_NEW_WEBEXT_STORAGE', True, when=new_webext_storage)
# dump_syms
# ==============================================================