Skip to main content

matrix_sdk_sqlite/
lib.rs

1// Copyright 2022 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![cfg_attr(
16    not(any(
17        feature = "state-store",
18        feature = "crypto-store",
19        feature = "event-cache-store",
20        feature = "media-store"
21    )),
22    allow(dead_code, unused_imports)
23)]
24
25mod connection;
26#[cfg(feature = "crypto-store")]
27mod crypto_store;
28mod error;
29#[cfg(feature = "event-cache-store")]
30mod event_cache_store;
31#[cfg(feature = "media-store")]
32mod media_store;
33#[cfg(feature = "state-store")]
34mod state_store;
35mod utils;
36use std::{
37    cmp::max,
38    fmt,
39    path::{Path, PathBuf},
40};
41
42use deadpool::managed::PoolConfig;
43use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
44
45#[cfg(feature = "crypto-store")]
46pub use self::crypto_store::SqliteCryptoStore;
47pub use self::error::OpenStoreError;
48#[cfg(feature = "event-cache-store")]
49pub use self::event_cache_store::SqliteEventCacheStore;
50#[cfg(feature = "media-store")]
51pub use self::media_store::SqliteMediaStore;
52#[cfg(feature = "state-store")]
53pub use self::state_store::{DATABASE_NAME as STATE_STORE_DATABASE_NAME, SqliteStateStore};
54
55#[cfg(feature = "uniffi")]
56uniffi::setup_scaffolding!();
57
58#[cfg(test)]
59matrix_sdk_test_utils::init_tracing_for_tests!();
60
61/// An enum used to store the secret that gives access to a store
62#[derive(Clone, Debug, PartialEq, Zeroize, ZeroizeOnDrop)]
63pub enum Secret {
64    // Cryptographic key used to open the store
65    Key(Zeroizing<Vec<u8>>),
66    // Passphrase used to open the store, ideally human chosen
67    PassPhrase(Zeroizing<String>),
68    // Randomly generated passphrase, for which the store caches a
69    // cheaply-derivable copy of its cipher and skips derivation on later opens
70    HighEntropyPassPhrase {
71        key: Zeroizing<Vec<u8>>,
72        #[zeroize(skip)]
73        base64_variant: Base64Variant,
74    },
75}
76
77/// Enum controlling how the high-entropy passphrase used to be created on the
78/// client side.
79///
80/// This allows us to replicate how a random key was converted into a passphrase
81/// to migrate from said passphrase to the plain key.
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
84pub enum Base64Variant {
85    /// Unpadded base64 was used to create the high-entropy passphrase.
86    Unpadded,
87    /// Standard padded base64 was used to create the high-entropy passphrase.
88    Padded,
89}
90
91/// A configuration structure used for opening a store.
92#[derive(Clone)]
93pub struct SqliteStoreConfig {
94    /// Path to the database, without the file name.
95    path: PathBuf,
96    /// Secret to open the store, if any
97    secret: Option<Secret>,
98    /// The pool configuration for [`deadpool`].
99    pool_config: PoolConfig,
100    /// The runtime configuration to apply when opening an SQLite connection.
101    runtime_config: RuntimeConfig,
102}
103
104impl fmt::Debug for SqliteStoreConfig {
105    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106        formatter
107            .debug_struct("SqliteStoreConfig")
108            .field("path", &self.path)
109            .field("pool_config", &self.pool_config)
110            .field("runtime_config", &self.runtime_config)
111            .finish_non_exhaustive()
112    }
113}
114
115/// The minimum size of the connections pool.
116///
117/// We need at least 2 connections: one connection for write operations, and one
118/// connection for read operations.
119const POOL_MINIMUM_SIZE: usize = 2;
120
121impl SqliteStoreConfig {
122    /// Create a new [`SqliteStoreConfig`] with a path representing the
123    /// directory containing the store database.
124    pub fn new<P>(path: P) -> Self
125    where
126        P: AsRef<Path>,
127    {
128        Self {
129            path: path.as_ref().to_path_buf(),
130            pool_config: PoolConfig::new(max(POOL_MINIMUM_SIZE, num_cpus::get_physical() * 4)),
131            runtime_config: RuntimeConfig::default(),
132            secret: None,
133        }
134    }
135
136    /// Similar to [`SqliteStoreConfig::new`], but with defaults tailored for a
137    /// low memory usage environment.
138    ///
139    /// The following defaults are set:
140    ///
141    /// - The `pool_max_size` is set to the number of physical CPU, so one
142    ///   connection per physical thread,
143    /// - The `cache_size` is set to 500Kib,
144    /// - The `journal_size_limit` is set to 2Mib.
145    pub fn with_low_memory_config<P>(path: P) -> Self
146    where
147        P: AsRef<Path>,
148    {
149        Self::new(path)
150            // Maximum one connection per physical thread.
151            .pool_max_size(num_cpus::get_physical())
152            // Cache size is 500Kib.
153            .cache_size(500_000)
154            // Journal size limit is 2Mib.
155            .journal_size_limit(2_000_000)
156    }
157
158    /// Override the path.
159    pub fn path<P>(mut self, path: P) -> Self
160    where
161        P: AsRef<Path>,
162    {
163        self.path = path.as_ref().to_path_buf();
164        self
165    }
166
167    /// Define the passphrase if the store is encoded.
168    ///
169    /// Assumed to be possibly human-chosen, so an expensive derivation is run
170    /// over it on every open. If it is randomly generated, use
171    /// [`SqliteStoreConfig::high_entropy_passphrase`] instead.
172    pub fn passphrase(mut self, passphrase: Option<&str>) -> Self {
173        self.secret =
174            passphrase.map(|passphrase| Secret::PassPhrase(Zeroizing::new(passphrase.to_owned())));
175        self
176    }
177
178    /// Define the passphrase if the store is encoded, declaring that it was
179    /// randomly generated rather than chosen by a human.
180    ///
181    /// Do NOT use this with human-chosen passphrases, as doing so would remove
182    /// their brute-force protection.
183    ///
184    /// This migrates a passphrase-based store whose passphrase was created by
185    /// base64-encoding a randomly generated key to a key-based setup.
186    ///
187    /// Once this function has been called, [`SqliteStoreConfig::passphrase`]
188    /// can no longer be used with the passphrase.
189    ///
190    /// [`SqliteStoreConfig::key`] can be used with the original key, before it
191    /// was base64-encoded.
192    pub fn high_entropy_passphrase(
193        mut self,
194        passphrase: Option<&[u8]>,
195        base64_variant: Base64Variant,
196    ) -> Self {
197        if let Some(passphrase) = passphrase {
198            let key = Zeroizing::new(passphrase.to_vec());
199            self.secret = Some(Secret::HighEntropyPassPhrase { key, base64_variant });
200        }
201
202        self
203    }
204
205    /// Define the key if the store is encoded.
206    ///
207    /// Assumed to be high entropy so no derivation is run over it.
208    pub fn key(mut self, key: Option<&[u8]>) -> Self {
209        if let Some(key) = key {
210            let key = Zeroizing::new(key.to_vec());
211            self.secret = Some(Secret::Key(key));
212        }
213
214        self
215    }
216
217    /// Define the maximum pool size for [`deadpool`].
218    ///
219    /// See [`deadpool::managed::PoolConfig::max_size`] to learn more.
220    pub fn pool_max_size(mut self, max_size: usize) -> Self {
221        self.pool_config.max_size = max(POOL_MINIMUM_SIZE, max_size);
222        self
223    }
224
225    /// Optimize the database.
226    ///
227    /// The SQLite documentation recommends to run this regularly and after any
228    /// schema change. The easiest is to do it consistently when the store is
229    /// constructed, after eventual migrations.
230    ///
231    /// See [`PRAGMA optimize`] to learn more.
232    ///
233    /// The default value is `true`.
234    ///
235    /// [`PRAGMA optimize`]: https://www.sqlite.org/pragma.html#pragma_optimize
236    pub fn optimize(mut self, optimize: bool) -> Self {
237        self.runtime_config.optimize = optimize;
238        self
239    }
240
241    /// Define the maximum size in **bytes** the SQLite cache can use.
242    ///
243    /// See [`PRAGMA cache_size`] to learn more.
244    ///
245    /// The default value is 2Mib.
246    ///
247    /// [`PRAGMA cache_size`]: https://www.sqlite.org/pragma.html#pragma_cache_size
248    pub fn cache_size(mut self, cache_size: u32) -> Self {
249        self.runtime_config.cache_size = cache_size;
250        self
251    }
252
253    /// Limit the size of the WAL file, in **bytes**.
254    ///
255    /// By default, while the DB connections of the databases are open,
256    /// [the size of the WAL file can keep increasing][size_wal_file] depending
257    /// on the size needed for the transactions. A critical case is `VACUUM`
258    /// which basically writes the content of the DB file to the WAL file before
259    /// writing it back to the DB file, so we end up taking twice the size of
260    /// the database.
261    ///
262    /// By setting this limit, the WAL file is truncated after its content is
263    /// written to the database, if it is bigger than the limit.
264    ///
265    /// See [`PRAGMA journal_size_limit`] to learn more. The value `limit`
266    /// corresponds to `N` in `PRAGMA journal_size_limit = N`.
267    ///
268    /// The default value is 10Mib.
269    ///
270    /// [size_wal_file]: https://www.sqlite.org/wal.html#avoiding_excessively_large_wal_files
271    /// [`PRAGMA journal_size_limit`]: https://www.sqlite.org/pragma.html#pragma_journal_size_limit
272    pub fn journal_size_limit(mut self, limit: u32) -> Self {
273        self.runtime_config.journal_size_limit = limit;
274        self
275    }
276
277    /// Define how often SQLite syncs the database to the storage device.
278    ///
279    /// [`Synchronous::Normal`] is faster, but a power loss can drop the most
280    /// recent transactions. Defaults to [`Synchronous::Full`].
281    ///
282    /// See [`PRAGMA synchronous`] to learn more.
283    ///
284    /// [`PRAGMA synchronous`]: https://www.sqlite.org/pragma.html#pragma_synchronous
285    pub fn synchronous(mut self, synchronous: Synchronous) -> Self {
286        self.runtime_config.synchronous = synchronous;
287        self
288    }
289
290    /// Returns the pool configuration.
291    pub(crate) fn pool_config(&self) -> PoolConfig {
292        self.pool_config
293    }
294
295    /// Returns the runtime configuration.
296    pub(crate) fn runtime_config(&self) -> RuntimeConfig {
297        self.runtime_config
298    }
299
300    /// Build a pool of active connections to a particular database.
301    pub fn build_pool_of_connections(
302        &self,
303        database_name: &str,
304    ) -> Result<connection::Pool, connection::CreatePoolError> {
305        let path = self.path.join(database_name);
306        let manager = connection::Manager::new(path, self.runtime_config);
307
308        connection::Pool::builder(manager)
309            .config(self.pool_config)
310            .runtime(connection::RUNTIME)
311            .build()
312            .map_err(connection::CreatePoolError::Build)
313    }
314}
315
316/// How often SQLite syncs the database to the storage device.
317///
318/// See [`PRAGMA synchronous`] to learn more.
319///
320/// [`PRAGMA synchronous`]: https://www.sqlite.org/pragma.html#pragma_synchronous
321#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
322pub enum Synchronous {
323    /// SQLite never syncs. A crash of the operating system can corrupt the
324    /// database.
325    Off,
326
327    /// SQLite syncs at the most critical moments. In WAL mode, that is one
328    /// `fsync` per checkpoint.
329    Normal,
330
331    /// SQLite syncs at every transaction. In WAL mode, that is one `fsync` of
332    /// the WAL file per commit. This is the default of SQLite.
333    #[default]
334    Full,
335
336    /// Like [`Synchronous::Full`], plus one `fsync` of the directory when a
337    /// rollback journal is deleted. This has no effect in WAL mode.
338    Extra,
339}
340
341impl Synchronous {
342    fn as_str(self) -> &'static str {
343        match self {
344            Self::Off => "OFF",
345            Self::Normal => "NORMAL",
346            Self::Full => "FULL",
347            Self::Extra => "EXTRA",
348        }
349    }
350}
351
352/// This type represents values to set at runtime when a database is opened.
353///
354/// The per-connection part is applied by
355/// [`connection::Manager`] to every connection it creates. The rest is applied
356/// by [`utils::SqliteAsyncConnExt::apply_runtime_config`].
357#[derive(Clone, Copy, Debug)]
358pub(crate) struct RuntimeConfig {
359    /// If `true`, [`utils::SqliteAsyncConnExt::optimize`] will be called.
360    optimize: bool,
361
362    /// Regardless of the value, `PRAGMA cache_size` will always be set to this
363    /// value.
364    cache_size: u32,
365
366    /// Regardless of the value, `PRAGMA journal_size_limit` will always be set
367    /// to this value.
368    journal_size_limit: u32,
369
370    /// Regardless of the value, `PRAGMA synchronous` will always be set to this
371    /// value.
372    synchronous: Synchronous,
373}
374
375impl RuntimeConfig {
376    /// The pragmas to run on every new connection, as `synchronous`,
377    /// `cache_size` and `journal_size_limit` all apply to one connection only.
378    pub(crate) fn connection_pragmas(&self) -> String {
379        // `N` in `PRAGMA cache_size = -N` is expressed in kibibytes, while
380        // `cache_size` is expressed in bytes.
381        let cache_size_in_kib = self.cache_size / 1024;
382
383        format!(
384            "PRAGMA synchronous = {}; \
385             PRAGMA cache_size = -{cache_size_in_kib}; \
386             PRAGMA journal_size_limit = {};",
387            self.synchronous.as_str(),
388            self.journal_size_limit,
389        )
390    }
391}
392
393impl Default for RuntimeConfig {
394    fn default() -> Self {
395        Self {
396            // Optimize is always applied.
397            optimize: true,
398            // A cache of 2Mib.
399            cache_size: 2_000_000,
400            // A limit of 10Mib.
401            journal_size_limit: 10_000_000,
402            synchronous: Synchronous::default(),
403        }
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use std::{
410        ops::Not,
411        path::{Path, PathBuf},
412    };
413
414    use zeroize::Zeroizing;
415
416    use super::{POOL_MINIMUM_SIZE, Secret, SqliteStoreConfig, Synchronous};
417
418    #[test]
419    fn test_new() {
420        let store_config = SqliteStoreConfig::new(Path::new("foo"));
421
422        assert_eq!(store_config.pool_config.max_size, num_cpus::get_physical() * 4);
423        assert!(store_config.runtime_config.optimize);
424        assert_eq!(store_config.runtime_config.cache_size, 2_000_000);
425        assert_eq!(store_config.runtime_config.journal_size_limit, 10_000_000);
426        assert_eq!(store_config.runtime_config.synchronous, Synchronous::Full);
427    }
428
429    #[test]
430    fn test_with_low_memory_config() {
431        let store_config = SqliteStoreConfig::with_low_memory_config(Path::new("foo"));
432
433        assert_eq!(store_config.pool_config.max_size, num_cpus::get_physical());
434        assert!(store_config.runtime_config.optimize);
435        assert_eq!(store_config.runtime_config.cache_size, 500_000);
436        assert_eq!(store_config.runtime_config.journal_size_limit, 2_000_000);
437        assert_eq!(store_config.runtime_config.synchronous, Synchronous::Full);
438    }
439
440    #[test]
441    fn test_store_config_when_passphrase() {
442        let store_config = SqliteStoreConfig::new(Path::new("foo"))
443            .passphrase(Some("bar"))
444            .pool_max_size(42)
445            .optimize(false)
446            .cache_size(43)
447            .journal_size_limit(44)
448            .synchronous(Synchronous::Off);
449
450        assert_eq!(store_config.path, PathBuf::from("foo"));
451        assert_eq!(store_config.secret, Some(Secret::PassPhrase("bar".to_owned().into())));
452        assert_eq!(store_config.pool_config.max_size, 42);
453        assert!(store_config.runtime_config.optimize.not());
454        assert_eq!(store_config.runtime_config.cache_size, 43);
455        assert_eq!(store_config.runtime_config.journal_size_limit, 44);
456        assert_eq!(store_config.runtime_config.synchronous, Synchronous::Off);
457    }
458
459    #[test]
460    fn test_store_config_when_key() {
461        let store_config = SqliteStoreConfig::new(Path::new("foo"))
462            .key(Some(&[
463                143, 27, 202, 78, 96, 55, 13, 149, 247, 8, 33, 120, 204, 92, 171, 66, 19, 238, 61,
464                107, 132, 211, 40, 244, 71, 190, 99, 14, 173, 225, 6, 156,
465            ]))
466            .pool_max_size(42)
467            .optimize(false)
468            .cache_size(43)
469            .journal_size_limit(44)
470            .synchronous(Synchronous::Extra);
471
472        assert_eq!(store_config.path, PathBuf::from("foo"));
473        assert_eq!(
474            store_config.secret,
475            Some(Secret::Key(Zeroizing::new(vec![
476                143, 27, 202, 78, 96, 55, 13, 149, 247, 8, 33, 120, 204, 92, 171, 66, 19, 238, 61,
477                107, 132, 211, 40, 244, 71, 190, 99, 14, 173, 225, 6, 156,
478            ])))
479        );
480        assert_eq!(store_config.pool_config.max_size, 42);
481        assert!(store_config.runtime_config.optimize.not());
482        assert_eq!(store_config.runtime_config.cache_size, 43);
483        assert_eq!(store_config.runtime_config.journal_size_limit, 44);
484        assert_eq!(store_config.runtime_config.synchronous, Synchronous::Extra);
485    }
486
487    #[test]
488    fn test_store_config_path() {
489        let store_config = SqliteStoreConfig::new(Path::new("foo")).path(Path::new("bar"));
490
491        assert_eq!(store_config.path, PathBuf::from("bar"));
492    }
493
494    #[test]
495    fn test_pool_size_has_a_minimum() {
496        let store_config = SqliteStoreConfig::new(Path::new("foo")).pool_max_size(1);
497
498        assert_eq!(store_config.pool_config.max_size, POOL_MINIMUM_SIZE);
499    }
500}