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