1use std::{borrow::Cow, fmt};
4
5use indexmap::IndexMap;
6use rusqlite::{Connection as RusqliteConnection, ffi};
7use toasty_core::{
8 Result,
9 driver::{ExecResponse, QueryLogConfig, log::QueryLog},
10 stmt,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum JournalMode {
17 Delete,
19 Truncate,
21 Persist,
23 Memory,
25 Wal,
27 Off,
29}
30
31impl JournalMode {
32 fn as_str(self) -> &'static str {
33 match self {
34 Self::Delete => "DELETE",
35 Self::Truncate => "TRUNCATE",
36 Self::Persist => "PERSIST",
37 Self::Memory => "MEMORY",
38 Self::Wal => "WAL",
39 Self::Off => "OFF",
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub enum Synchronous {
48 Off,
50 Normal,
53 Full,
55 Extra,
58}
59
60impl Synchronous {
61 fn as_str(self) -> &'static str {
62 match self {
63 Self::Off => "OFF",
64 Self::Normal => "NORMAL",
65 Self::Full => "FULL",
66 Self::Extra => "EXTRA",
67 }
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74pub enum LockingMode {
75 Normal,
77 Exclusive,
79}
80
81impl LockingMode {
82 fn as_str(self) -> &'static str {
83 match self {
84 Self::Normal => "NORMAL",
85 Self::Exclusive => "EXCLUSIVE",
86 }
87 }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
93pub enum AutoVacuum {
94 None,
96 Full,
98 Incremental,
100}
101
102impl AutoVacuum {
103 fn as_str(self) -> &'static str {
104 match self {
105 Self::None => "NONE",
106 Self::Full => "FULL",
107 Self::Incremental => "INCREMENTAL",
108 }
109 }
110}
111
112impl From<JournalMode> for Cow<'static, str> {
113 fn from(mode: JournalMode) -> Self {
114 Cow::Borrowed(mode.as_str())
115 }
116}
117
118impl From<Synchronous> for Cow<'static, str> {
119 fn from(synchronous: Synchronous) -> Self {
120 Cow::Borrowed(synchronous.as_str())
121 }
122}
123
124impl From<LockingMode> for Cow<'static, str> {
125 fn from(mode: LockingMode) -> Self {
126 Cow::Borrowed(mode.as_str())
127 }
128}
129
130impl From<AutoVacuum> for Cow<'static, str> {
131 fn from(auto_vacuum: AutoVacuum) -> Self {
132 Cow::Borrowed(auto_vacuum.as_str())
133 }
134}
135
136const SECRET: &[&str] = &["key", "rekey", "hexkey", "hexrekey", "cipher_salt"];
139
140#[derive(Clone)]
147pub(crate) struct Pragmas {
148 entries: IndexMap<Cow<'static, str>, Option<Cow<'static, str>>>,
149}
150
151impl Default for Pragmas {
152 fn default() -> Self {
153 let slots = [
154 "key",
157 "cipher_plaintext_header_size",
158 "cipher_salt",
159 "kdf_iter",
160 "cipher_kdf_algorithm",
161 "cipher_use_hmac",
162 "cipher_compatibility",
163 "cipher_page_size",
164 "cipher_hmac_algorithm",
165 "page_size",
167 "locking_mode",
175 "auto_vacuum",
176 "journal_mode",
177 "foreign_keys",
178 "synchronous",
179 ];
180
181 let mut pragmas = Self {
182 entries: slots
183 .into_iter()
184 .map(|name| (Cow::Borrowed(name), None))
185 .collect(),
186 };
187
188 pragmas.set("foreign_keys", "ON");
191 pragmas
192 }
193}
194
195impl Pragmas {
196 pub(crate) fn set(
199 &mut self,
200 name: impl Into<Cow<'static, str>>,
201 value: impl Into<Cow<'static, str>>,
202 ) {
203 self.entries.insert(name.into(), Some(value.into()));
204 }
205
206 fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
207 self.entries
208 .iter()
209 .filter_map(|(name, value)| Some((name.as_ref(), value.as_deref()?)))
210 }
211
212 pub(crate) fn apply(
214 &self,
215 conn: &RusqliteConnection,
216 query_log: &QueryLogConfig,
217 ) -> Result<()> {
218 for (name, value) in self.iter() {
219 exec(conn, name, value, query_log)?;
220 }
221
222 Ok(())
223 }
224}
225
226impl fmt::Debug for Pragmas {
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 f.debug_map()
230 .entries(self.iter().map(|(name, value)| {
231 let value = if SECRET.contains(&name) {
232 "[redacted]"
233 } else {
234 value
235 };
236 (name, value)
237 }))
238 .finish()
239 }
240}
241
242fn exec(
243 conn: &RusqliteConnection,
244 name: &str,
245 value: &str,
246 query_log: &QueryLogConfig,
247) -> Result<()> {
248 let secret = SECRET.contains(&name);
249 let sql = format!("PRAGMA {name} = {value}");
250
251 let logged = if secret {
252 Cow::Owned(format!("PRAGMA {name} = [redacted]"))
253 } else {
254 Cow::Borrowed(sql.as_str())
255 };
256 let log = QueryLog::sql(
257 query_log,
258 "sqlite",
259 &logged,
260 std::iter::empty::<&stmt::Value>(),
261 );
262
263 let result = conn
266 .execute_batch(&sql)
267 .map_err(|err| {
268 let err = if secret { redact(err) } else { err };
271 toasty_core::Error::driver_operation_failed(err)
272 })
273 .map(|()| ExecResponse::count(0));
274 log.finish(&result);
275
276 result.map(|_| ())
277}
278
279fn redact(err: rusqlite::Error) -> rusqlite::Error {
281 let code = err
282 .sqlite_error()
283 .copied()
284 .unwrap_or_else(|| ffi::Error::new(ffi::SQLITE_ERROR));
285 rusqlite::Error::SqliteFailure(code, None)
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 fn order(pragmas: &Pragmas) -> Vec<&str> {
293 pragmas.iter().map(|(name, _)| name).collect()
294 }
295
296 #[test]
297 fn only_foreign_keys_is_set_by_default() {
298 let pragmas = Pragmas::default();
299 assert_eq!(pragmas.iter().collect::<Vec<_>>(), [("foreign_keys", "ON")]);
300 }
301
302 #[test]
303 fn slotted_pragmas_apply_in_slot_order() {
304 let mut pragmas = Pragmas::default();
305 pragmas.set("journal_mode", "WAL");
306 pragmas.set("auto_vacuum", "FULL");
307 pragmas.set("page_size", "8192");
308 pragmas.set("locking_mode", "NORMAL");
309 pragmas.set("key", "'secret'");
310
311 assert_eq!(
312 order(&pragmas),
313 [
314 "key",
315 "page_size",
316 "locking_mode",
317 "auto_vacuum",
318 "journal_mode",
319 "foreign_keys"
320 ]
321 );
322 }
323
324 #[test]
325 fn unslotted_pragmas_follow_in_the_order_first_set() {
326 let mut pragmas = Pragmas::default();
327 pragmas.set("mmap_size", "0");
328 pragmas.set("journal_mode", "WAL");
329 pragmas.set("cache_size", "-64000");
330 pragmas.set("mmap_size", "1");
331
332 assert_eq!(
333 order(&pragmas),
334 ["journal_mode", "foreign_keys", "mmap_size", "cache_size"]
335 );
336 }
337
338 #[test]
339 fn names_match_exactly() {
340 let mut pragmas = Pragmas::default();
341 pragmas.set("JOURNAL_MODE", "WAL");
342
343 assert_eq!(order(&pragmas), ["foreign_keys", "JOURNAL_MODE"]);
344 }
345}