webext_storage/
db.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use crate::error::*;
6use crate::schema;
7use interrupt_support::{SqlInterruptHandle, SqlInterruptScope};
8use parking_lot::Mutex;
9use rusqlite::types::{FromSql, ToSql};
10use rusqlite::Connection;
11use rusqlite::OpenFlags;
12use sql_support::open_database::open_database_with_flags;
13use sql_support::ConnExt;
14use std::ops::Deref;
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17use url::Url;
18
19/// A `StorageDb` wraps a read-write SQLite connection, and handles schema
20/// migrations and recovering from database file corruption. It can be used
21/// anywhere a `rusqlite::Connection` is expected, thanks to its `Deref{Mut}`
22/// implementations.
23///
24/// We only support a single writer connection - so that's the only thing we
25/// store. It's still a bit overkill, but there's only so many yaks in a day.
26pub enum WebExtStorageDb {
27    Open(Connection),
28    Closed,
29}
30
31pub struct StorageDb {
32    pub writer: WebExtStorageDb,
33    interrupt_handle: Arc<SqlInterruptHandle>,
34}
35
36impl StorageDb {
37    /// Create a new, or fetch an already open, StorageDb backed by a file on disk.
38    pub fn new(db_path: impl AsRef<Path>) -> Result<Self> {
39        let db_path = normalize_path(db_path)?;
40        Self::new_named(db_path)
41    }
42
43    /// Create a new, or fetch an already open, memory-based StorageDb. You must
44    /// provide a name, but you are still able to have a single writer and many
45    /// reader connections to the same memory DB open.
46    #[cfg(test)]
47    pub fn new_memory(db_path: &str) -> Result<Self> {
48        let name = PathBuf::from(format!("file:{}?mode=memory&cache=shared", db_path));
49        Self::new_named(name)
50    }
51
52    fn new_named(db_path: PathBuf) -> Result<Self> {
53        // We always create the read-write connection for an initial open so
54        // we can create the schema and/or do version upgrades.
55        let flags = OpenFlags::SQLITE_OPEN_NO_MUTEX
56            | OpenFlags::SQLITE_OPEN_URI
57            | OpenFlags::SQLITE_OPEN_CREATE
58            | OpenFlags::SQLITE_OPEN_READ_WRITE;
59
60        let conn = open_database_with_flags(db_path, flags, &schema::WebExtMigrationLogin)?;
61        Ok(Self {
62            interrupt_handle: Arc::new(SqlInterruptHandle::new(&conn)),
63            writer: WebExtStorageDb::Open(conn),
64        })
65    }
66
67    pub fn interrupt_handle(&self) -> Arc<SqlInterruptHandle> {
68        Arc::clone(&self.interrupt_handle)
69    }
70
71    #[allow(dead_code)]
72    pub fn begin_interrupt_scope(&self) -> Result<SqlInterruptScope> {
73        Ok(self.interrupt_handle.begin_interrupt_scope()?)
74    }
75
76    /// Closes the database connection. If there are any unfinalized prepared
77    /// statements on the connection, `close` will fail and the `StorageDb` will
78    /// remain open and the connection will be leaked - we used to return the
79    /// underlying connection so the caller can retry but (a) that's very tricky
80    /// in an Arc<Mutex<>> world and (b) we never actually took advantage of
81    /// that retry capability.
82    pub fn close(&mut self) -> Result<()> {
83        let conn = match std::mem::replace(&mut self.writer, WebExtStorageDb::Closed) {
84            WebExtStorageDb::Open(conn) => conn,
85            WebExtStorageDb::Closed => return Ok(()),
86        };
87        conn.close().map_err(|(_, y)| Error::SqlError(y))
88    }
89
90    pub(crate) fn get_connection(&self) -> Result<&Connection> {
91        let db = &self.writer;
92        match db {
93            WebExtStorageDb::Open(y) => Ok(y),
94            WebExtStorageDb::Closed => Err(Error::DatabaseConnectionClosed),
95        }
96    }
97}
98
99// We almost exclusively use this ThreadSafeStorageDb
100pub struct ThreadSafeStorageDb {
101    db: Mutex<StorageDb>,
102    // This "outer" interrupt_handle not protected by the mutex means
103    // consumers can interrupt us when the mutex is held - which it always will
104    // be if we are doing anything interruptible!
105    interrupt_handle: Arc<SqlInterruptHandle>,
106}
107
108impl ThreadSafeStorageDb {
109    pub fn new(db: StorageDb) -> Self {
110        Self {
111            interrupt_handle: db.interrupt_handle(),
112            db: Mutex::new(db),
113        }
114    }
115
116    pub fn interrupt_handle(&self) -> Arc<SqlInterruptHandle> {
117        Arc::clone(&self.interrupt_handle)
118    }
119
120    #[allow(dead_code)]
121    pub fn begin_interrupt_scope(&self) -> Result<SqlInterruptScope> {
122        Ok(self.interrupt_handle.begin_interrupt_scope()?)
123    }
124}
125
126// Deref to a Mutex<StorageDb>, which is how we will use ThreadSafeStorageDb most of the time
127impl Deref for ThreadSafeStorageDb {
128    type Target = Mutex<StorageDb>;
129
130    #[inline]
131    fn deref(&self) -> &Mutex<StorageDb> {
132        &self.db
133    }
134}
135
136// Also implement AsRef<SqlInterruptHandle> so that we can interrupt this at shutdown
137impl AsRef<SqlInterruptHandle> for ThreadSafeStorageDb {
138    fn as_ref(&self) -> &SqlInterruptHandle {
139        &self.interrupt_handle
140    }
141}
142
143pub(crate) mod sql_fns {
144    use rusqlite::{functions::Context, Result};
145    use sync_guid::Guid as SyncGuid;
146
147    #[inline(never)]
148    pub fn generate_guid(_ctx: &Context<'_>) -> Result<SyncGuid> {
149        Ok(SyncGuid::random())
150    }
151}
152
153// These should be somewhere else...
154pub fn put_meta(db: &Connection, key: &str, value: &dyn ToSql) -> Result<()> {
155    db.conn().execute_cached(
156        "REPLACE INTO meta (key, value) VALUES (:key, :value)",
157        rusqlite::named_params! { ":key": key, ":value": value },
158    )?;
159    Ok(())
160}
161
162pub fn get_meta<T: FromSql>(db: &Connection, key: &str) -> Result<Option<T>> {
163    let res = db.conn().try_query_one(
164        "SELECT value FROM meta WHERE key = :key",
165        &[(":key", &key)],
166        true,
167    )?;
168    Ok(res)
169}
170
171pub fn delete_meta(db: &Connection, key: &str) -> Result<()> {
172    db.conn()
173        .execute_cached("DELETE FROM meta WHERE key = :key", &[(":key", &key)])?;
174    Ok(())
175}
176
177// Utilities for working with paths.
178// (From places_utils - ideally these would be shared, but the use of
179// ErrorKind values makes that non-trivial.
180
181/// `Path` is basically just a `str` with no validation, and so in practice it
182/// could contain a file URL. Rusqlite takes advantage of this a bit, and says
183/// `AsRef<Path>` but really means "anything sqlite can take as an argument".
184///
185/// Swift loves using file urls (the only support it has for file manipulation
186/// is through file urls), so it's handy to support them if possible.
187fn unurl_path(p: impl AsRef<Path>) -> PathBuf {
188    p.as_ref()
189        .to_str()
190        .and_then(|s| Url::parse(s).ok())
191        .and_then(|u| {
192            if u.scheme() == "file" {
193                u.to_file_path().ok()
194            } else {
195                None
196            }
197        })
198        .unwrap_or_else(|| p.as_ref().to_owned())
199}
200
201/// If `p` is a file URL, return it, otherwise try and make it one.
202///
203/// Errors if `p` is a relative non-url path, or if it's a URL path
204/// that's isn't a `file:` URL.
205#[allow(dead_code)]
206pub fn ensure_url_path(p: impl AsRef<Path>) -> Result<Url> {
207    if let Some(u) = p.as_ref().to_str().and_then(|s| Url::parse(s).ok()) {
208        if u.scheme() == "file" {
209            Ok(u)
210        } else {
211            Err(Error::IllegalDatabasePath(p.as_ref().to_owned()))
212        }
213    } else {
214        let p = p.as_ref();
215        let u = Url::from_file_path(p).map_err(|_| Error::IllegalDatabasePath(p.to_owned()))?;
216        Ok(u)
217    }
218}
219
220/// As best as possible, convert `p` into an absolute path, resolving
221/// all symlinks along the way.
222///
223/// If `p` is a file url, it's converted to a path before this.
224fn normalize_path(p: impl AsRef<Path>) -> Result<PathBuf> {
225    let path = unurl_path(p);
226    if let Ok(canonical) = path.canonicalize() {
227        return Ok(canonical);
228    }
229    // It probably doesn't exist yet. This is an error, although it seems to
230    // work on some systems.
231    //
232    // We resolve this by trying to canonicalize the parent directory, and
233    // appending the requested file name onto that. If we can't canonicalize
234    // the parent, we return an error.
235    //
236    // Also, we return errors if the path ends in "..", if there is no
237    // parent directory, etc.
238    let file_name = path
239        .file_name()
240        .ok_or_else(|| Error::IllegalDatabasePath(path.clone()))?;
241
242    let parent = path
243        .parent()
244        .ok_or_else(|| Error::IllegalDatabasePath(path.clone()))?;
245
246    let mut canonical = parent.canonicalize()?;
247    canonical.push(file_name);
248    Ok(canonical)
249}
250
251// Helpers for tests
252#[cfg(test)]
253pub mod test {
254    use super::*;
255    use std::sync::atomic::{AtomicUsize, Ordering};
256
257    // A helper for our tests to get their own memory Api.
258    static ATOMIC_COUNTER: AtomicUsize = AtomicUsize::new(0);
259
260    pub fn new_mem_db() -> StorageDb {
261        error_support::init_for_tests();
262        let counter = ATOMIC_COUNTER.fetch_add(1, Ordering::Relaxed);
263        StorageDb::new_memory(&format!("test-api-{}", counter)).expect("should get an API")
264    }
265
266    pub fn new_mem_thread_safe_storage_db() -> Arc<ThreadSafeStorageDb> {
267        Arc::new(ThreadSafeStorageDb::new(new_mem_db()))
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::test::*;
274    use super::*;
275
276    // Sanity check that we can create a database.
277    #[test]
278    fn test_open() {
279        new_mem_db();
280        // XXX - should we check anything else? Seems a bit pointless, but if
281        // we move the meta functions away from here then it's better than
282        // nothing.
283    }
284
285    #[test]
286    fn test_meta() -> Result<()> {
287        let db = new_mem_db();
288        let conn = &db.get_connection()?;
289        assert_eq!(get_meta::<String>(conn, "foo")?, None);
290        put_meta(conn, "foo", &"bar".to_string())?;
291        assert_eq!(get_meta(conn, "foo")?, Some("bar".to_string()));
292        delete_meta(conn, "foo")?;
293        assert_eq!(get_meta::<String>(conn, "foo")?, None);
294        Ok(())
295    }
296}