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, [the
256    /// size of the WAL file can keep increasing][size_wal_file] depending on
257    /// 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
259    /// before writing it back to the DB file, so we end up taking twice the
260    /// size of 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    /// Returns the pool configuration.
278    pub(crate) fn pool_config(&self) -> PoolConfig {
279        self.pool_config
280    }
281
282    /// Returns the runtime configuration.
283    pub(crate) fn runtime_config(&self) -> RuntimeConfig {
284        self.runtime_config
285    }
286
287    /// Build a pool of active connections to a particular database.
288    pub fn build_pool_of_connections(
289        &self,
290        database_name: &str,
291    ) -> Result<connection::Pool, connection::CreatePoolError> {
292        let path = self.path.join(database_name);
293        let manager = connection::Manager::new(path);
294
295        connection::Pool::builder(manager)
296            .config(self.pool_config)
297            .runtime(connection::RUNTIME)
298            .build()
299            .map_err(connection::CreatePoolError::Build)
300    }
301}
302
303/// This type represents values to set at runtime when a database is opened.
304///
305/// This configuration is applied by
306/// [`utils::SqliteAsyncConnExt::apply_runtime_config`].
307#[derive(Clone, Copy, Debug)]
308struct RuntimeConfig {
309    /// If `true`, [`utils::SqliteAsyncConnExt::optimize`] will be called.
310    optimize: bool,
311
312    /// Regardless of the value, [`utils::SqliteAsyncConnExt::cache_size`] will
313    /// always be called with this value.
314    cache_size: u32,
315
316    /// Regardless of the value,
317    /// [`utils::SqliteAsyncConnExt::journal_size_limit`] will always be called
318    /// with this value.
319    journal_size_limit: u32,
320}
321
322impl Default for RuntimeConfig {
323    fn default() -> Self {
324        Self {
325            // Optimize is always applied.
326            optimize: true,
327            // A cache of 2Mib.
328            cache_size: 2_000_000,
329            // A limit of 10Mib.
330            journal_size_limit: 10_000_000,
331        }
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use std::{
338        ops::Not,
339        path::{Path, PathBuf},
340    };
341
342    use zeroize::Zeroizing;
343
344    use super::{POOL_MINIMUM_SIZE, Secret, SqliteStoreConfig};
345
346    #[test]
347    fn test_new() {
348        let store_config = SqliteStoreConfig::new(Path::new("foo"));
349
350        assert_eq!(store_config.pool_config.max_size, num_cpus::get_physical() * 4);
351        assert!(store_config.runtime_config.optimize);
352        assert_eq!(store_config.runtime_config.cache_size, 2_000_000);
353        assert_eq!(store_config.runtime_config.journal_size_limit, 10_000_000);
354    }
355
356    #[test]
357    fn test_with_low_memory_config() {
358        let store_config = SqliteStoreConfig::with_low_memory_config(Path::new("foo"));
359
360        assert_eq!(store_config.pool_config.max_size, num_cpus::get_physical());
361        assert!(store_config.runtime_config.optimize);
362        assert_eq!(store_config.runtime_config.cache_size, 500_000);
363        assert_eq!(store_config.runtime_config.journal_size_limit, 2_000_000);
364    }
365
366    #[test]
367    fn test_store_config_when_passphrase() {
368        let store_config = SqliteStoreConfig::new(Path::new("foo"))
369            .passphrase(Some("bar"))
370            .pool_max_size(42)
371            .optimize(false)
372            .cache_size(43)
373            .journal_size_limit(44);
374
375        assert_eq!(store_config.path, PathBuf::from("foo"));
376        assert_eq!(store_config.secret, Some(Secret::PassPhrase("bar".to_owned().into())));
377        assert_eq!(store_config.pool_config.max_size, 42);
378        assert!(store_config.runtime_config.optimize.not());
379        assert_eq!(store_config.runtime_config.cache_size, 43);
380        assert_eq!(store_config.runtime_config.journal_size_limit, 44);
381    }
382
383    #[test]
384    fn test_store_config_when_key() {
385        let store_config = SqliteStoreConfig::new(Path::new("foo"))
386            .key(Some(&[
387                143, 27, 202, 78, 96, 55, 13, 149, 247, 8, 33, 120, 204, 92, 171, 66, 19, 238, 61,
388                107, 132, 211, 40, 244, 71, 190, 99, 14, 173, 225, 6, 156,
389            ]))
390            .pool_max_size(42)
391            .optimize(false)
392            .cache_size(43)
393            .journal_size_limit(44);
394
395        assert_eq!(store_config.path, PathBuf::from("foo"));
396        assert_eq!(
397            store_config.secret,
398            Some(Secret::Key(Zeroizing::new(vec![
399                143, 27, 202, 78, 96, 55, 13, 149, 247, 8, 33, 120, 204, 92, 171, 66, 19, 238, 61,
400                107, 132, 211, 40, 244, 71, 190, 99, 14, 173, 225, 6, 156,
401            ])))
402        );
403        assert_eq!(store_config.pool_config.max_size, 42);
404        assert!(store_config.runtime_config.optimize.not());
405        assert_eq!(store_config.runtime_config.cache_size, 43);
406        assert_eq!(store_config.runtime_config.journal_size_limit, 44);
407    }
408
409    #[test]
410    fn test_store_config_path() {
411        let store_config = SqliteStoreConfig::new(Path::new("foo")).path(Path::new("bar"));
412
413        assert_eq!(store_config.path, PathBuf::from("bar"));
414    }
415
416    #[test]
417    fn test_pool_size_has_a_minimum() {
418        let store_config = SqliteStoreConfig::new(Path::new("foo")).pool_max_size(1);
419
420        assert_eq!(store_config.pool_config.max_size, POOL_MINIMUM_SIZE);
421    }
422}