Skip to main content

tor_dirmgr/storage/
sqlite.rs

1//! Net document storage backed by sqlite3.
2//!
3//! We store most objects in sqlite tables, except for very large ones,
4//! which we store as "blob" files in a separate directory.
5
6use super::ExpirationConfig;
7use crate::docmeta::{AuthCertMeta, ConsensusMeta};
8use crate::err::ReadOnlyStorageError;
9use crate::storage::{InputString, Store};
10use crate::{Error, Result};
11
12use fs_mistrust::CheckedDir;
13use tor_basic_utils::PathExt as _;
14use tor_error::{internal, into_internal, warn_report};
15use tor_netdoc::doc::authcert::AuthCertKeyIds;
16use tor_netdoc::doc::microdesc::MdDigest;
17use tor_netdoc::doc::netstatus::{ConsensusFlavor, Lifetime};
18#[cfg(feature = "routerdesc")]
19use tor_netdoc::doc::routerdesc::RdDigest;
20use web_time_compat::SystemTimeExt;
21
22#[cfg(feature = "bridge-client")]
23pub(crate) use {crate::storage::CachedBridgeDescriptor, tor_guardmgr::bridge::BridgeConfig};
24
25use std::collections::{HashMap, HashSet};
26use std::fs::OpenOptions;
27use std::path::{Path, PathBuf};
28use std::result::Result as StdResult;
29use std::sync::Arc;
30use std::time::SystemTime;
31
32use fslock_guard::LockFileGuard;
33use rusqlite::{OpenFlags, OptionalExtension, Transaction, params};
34use time::OffsetDateTime;
35use tracing::{trace, warn};
36
37/// Possible status of a lockfile.
38///
39/// (Sqlite does its own locking, but we would like to cover the blobs directory
40/// as well)
41enum LockFile {
42    /// We are not even trying to lock, but permitting write operations
43    /// regardless.
44    ///
45    /// This is the implementation we use for ephemeral testing databases.
46    /// Don't use it in production!
47    NotLocking,
48
49    /// We aren't locked.
50    ///
51    /// The provided path is the path to the lockfile that we will try to open if
52    /// we
53    Unlocked(PathBuf),
54
55    /// We have the lock.
56    ///
57    Locked(
58        // We never need to read this field; we only need to hold it so that the
59        // lock file isn't closed.
60        #[allow(unused)] LockFileGuard,
61    ),
62}
63
64/// Local directory cache using a Sqlite3 connection.
65pub(crate) struct SqliteStore {
66    /// Connection to the sqlite3 database.
67    conn: rusqlite::Connection,
68    /// Location for the sqlite3 database; used to reopen it.
69    sql_path: Option<PathBuf>,
70    /// Location to store blob files.
71    blob_dir: CheckedDir,
72    /// Lockfile to prevent concurrent write attempts from different
73    /// processes.
74    ///
75    /// If this is LockFile::NotLocking we aren't using a lockfile.  Watch out!
76    ///
77    /// (sqlite supports that with connection locking, but we want to
78    /// be a little more coarse-grained here)
79    lockfile: LockFile,
80}
81
82/// # Some notes on blob consistency, and the lack thereof.
83///
84/// We store large documents (currently, consensuses) in separate files,
85/// called "blobs",
86/// outside of the sqlite database.
87/// We do this for performance reasons: for large objects,
88/// mmap is far more efficient than sqlite in RAM and CPU.
89///
90/// In the sqlite database, we keep track of our blobs
91/// using the ExtDocs table.
92/// This scheme makes it possible for the blobs and the table
93/// get out of sync.
94///
95/// In summary:
96///   - _Vanished_ blobs (ones present only in ExtDocs) are possible;
97///     we try to tolerate them.
98///   - _Orphaned_ blobs (ones present only on the disk) are possible;
99///     we try to tolerate them.
100///   - _Corrupted_ blobs (ones with the wrong contents) are possible
101///     but (we hope) unlikely;
102///     we do not currently try to tolerate them.
103///
104/// In more detail:
105///
106/// Here are the practices we use when _writing_ blobs:
107///
108/// - We always create a blob before updating the ExtDocs table,
109///   and remove an entry from the ExtDocs before deleting the blob.
110/// - If we decide to roll back the transaction that adds the row to ExtDocs,
111///   we delete the blob after doing so.
112/// - We use [`CheckedDir::write_and_replace`] to store blobs,
113///   so a half-formed blob shouldn't be common.
114///   (We assume that "close" and "rename" are serialized by the OS,
115///   so that _if_ the rename happens, the file is completely written.)
116/// - Blob filenames include a digest of the file contents,
117///   so collisions are unlikely.
118///
119/// Here are the practices we use when _deleting_ blobs:
120/// - First, we drop the row from the ExtDocs table.
121///   Only then do we delete the file.
122///
123/// These practices can result in _orphaned_ blobs
124/// (ones with no row in the ExtDoc table),
125/// or in _half-written_ blobs files with tempfile names
126/// (which also have no row in the ExtDoc table).
127/// This happens if we crash at the wrong moment.
128/// Such blobs can be safely removed;
129/// we do so in [`SqliteStore::remove_unreferenced_blobs`].
130///
131/// Despite our efforts, _vanished_ blobs
132/// (entries in the ExtDoc table with no corresponding file)
133/// are also possible.  They could happen for these reasons:
134/// - The filesystem might not serialize or sync things in a way that's
135///   consistent with the DB.
136/// - An automatic process might remove random cache files.
137/// - The user might run around deleting things to free space.
138///
139/// We try to tolerate vanished blobs.
140///
141/// _Corrupted_ blobs are also possible.  They can happen on FS corruption,
142/// or on somebody messing around with the cache directory manually.
143/// We do not attempt to tolerate corrupted blobs.
144///
145/// ## On trade-offs
146///
147/// TODO: The practices described above are more likely
148/// to create _orphaned_ blobs than _vanished_ blobs.
149/// We initially made this trade-off decision on the mistaken theory
150/// that we could avoid vanished blobs entirely.
151/// We _may_ want to revisit this choice,
152/// on the rationale that we can respond to vanished blobs as soon as we notice they're gone,
153/// whereas we can only handle orphaned blobs with a periodic cleanup.
154/// On the other hand, since we need to handle both cases,
155/// it may not matter very much in practice.
156#[allow(unused)]
157mod blob_consistency {}
158
159/// Specific error returned when a blob will not be read.
160///
161/// This error is an internal type: it's never returned to the user.
162#[derive(Debug)]
163enum AbsentBlob {
164    /// We did not find a blob file on the disk.
165    VanishedFile,
166    /// We did not even find a blob to read in ExtDocs.
167    NothingToRead,
168}
169
170impl SqliteStore {
171    /// Construct or open a new SqliteStore at some location on disk.
172    /// The provided location must be a directory, or a possible
173    /// location for a directory: the directory will be created if
174    /// necessary.
175    ///
176    /// If readonly is true, the result will be a read-only store.
177    /// Otherwise, when readonly is false, the result may be
178    /// read-only or read-write, depending on whether we can acquire
179    /// the lock.
180    ///
181    /// # Limitations:
182    ///
183    /// The file locking that we use to ensure that only one dirmgr is
184    /// writing to a given storage directory at a time is currently
185    /// _per process_. Therefore, you might get unexpected results if
186    /// two SqliteStores are created in the same process with the
187    /// path.
188    pub(crate) fn from_path_and_mistrust<P: AsRef<Path>>(
189        path: P,
190        mistrust: &fs_mistrust::Mistrust,
191        mut readonly: bool,
192    ) -> Result<Self> {
193        let path = path.as_ref();
194        let sqlpath = path.join("dir.sqlite3");
195        let blobpath = path.join("dir_blobs/");
196        let lockpath = path.join("dir.lock");
197
198        let verifier = mistrust.verifier().permit_readable().check_content();
199
200        let blob_dir = if readonly {
201            verifier.secure_dir(blobpath)?
202        } else {
203            verifier.make_secure_dir(blobpath)?
204        };
205
206        // Check permissions on the sqlite and lock files; don't require them to
207        // exist.
208        for p in [&lockpath, &sqlpath] {
209            match mistrust
210                .verifier()
211                .permit_readable()
212                .require_file()
213                .check(p)
214            {
215                Ok(()) | Err(fs_mistrust::Error::NotFound(_)) => {}
216                Err(e) => return Err(e.into()),
217            }
218        }
219
220        let lockfile = if !readonly {
221            match LockFileGuard::try_lock(&lockpath).map_err(Error::from_lockfile)? {
222                Some(guard) => LockFile::Locked(guard),
223                None => {
224                    // We couldn't get the lock.
225                    readonly = true;
226                    LockFile::Unlocked(lockpath)
227                }
228            }
229        } else {
230            LockFile::Unlocked(lockpath)
231        };
232
233        let flags = if readonly {
234            OpenFlags::SQLITE_OPEN_READ_ONLY
235        } else {
236            OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE
237        };
238        let conn = rusqlite::Connection::open_with_flags(&sqlpath, flags)?;
239        let mut store = SqliteStore::from_conn_internal(conn, blob_dir, readonly)?;
240        store.sql_path = Some(sqlpath);
241        store.lockfile = lockfile;
242        Ok(store)
243    }
244
245    /// Construct a new SqliteStore from a database connection and a location
246    /// for blob files.
247    ///
248    /// Used for testing with a memory-backed database.
249    ///
250    /// Note: `blob_dir` must not be used for anything other than storing the blobs associated with
251    /// this database, since we will freely remove unreferenced files from this directory.
252    #[cfg(test)]
253    fn from_conn(conn: rusqlite::Connection, blob_dir: CheckedDir) -> Result<Self> {
254        Self::from_conn_internal(conn, blob_dir, false)
255    }
256
257    /// Construct a new SqliteStore from a database connection and a location
258    /// for blob files.
259    ///
260    /// The `readonly` argument specifies whether the database connection should be read-only.
261    fn from_conn_internal(
262        conn: rusqlite::Connection,
263        blob_dir: CheckedDir,
264        readonly: bool,
265    ) -> Result<Self> {
266        // sqlite (as of Jun 2024) does not enforce foreign keys automatically unless you set this
267        // pragma on the connection.
268        conn.pragma_update(None, "foreign_keys", "ON")?;
269
270        let mut result = SqliteStore {
271            conn,
272            blob_dir,
273            lockfile: LockFile::NotLocking,
274            sql_path: None,
275        };
276
277        result.check_schema(readonly)?;
278
279        Ok(result)
280    }
281
282    /// Check whether this database has a schema format we can read, and
283    /// install or upgrade the schema if necessary.
284    fn check_schema(&mut self, readonly: bool) -> Result<()> {
285        let tx = self.conn.transaction()?;
286        let db_n_tables: u32 = tx.query_row(
287            "SELECT COUNT(name) FROM sqlite_master
288             WHERE type='table'
289             AND name NOT LIKE 'sqlite_%'",
290            [],
291            |row| row.get(0),
292        )?;
293        let db_exists = db_n_tables > 0;
294
295        // Update the schema from current_vsn to the latest (does not commit)
296        let update_schema = |tx: &rusqlite::Transaction, current_vsn| {
297            for (from_vsn, update) in UPDATE_SCHEMA.iter().enumerate() {
298                let from_vsn = u32::try_from(from_vsn).expect("schema version >2^32");
299                let new_vsn = from_vsn + 1;
300                if current_vsn < new_vsn {
301                    tx.execute_batch(update)?;
302                    tx.execute(UPDATE_SCHEMA_VERSION, params![new_vsn, new_vsn])?;
303                }
304            }
305            Ok::<_, Error>(())
306        };
307
308        if !db_exists {
309            if !readonly {
310                tx.execute_batch(INSTALL_V0_SCHEMA)?;
311                update_schema(&tx, 0)?;
312                tx.commit()?;
313            } else {
314                // The other process should have created the database!
315                return Err(Error::ReadOnlyStorage(ReadOnlyStorageError::NoDatabase));
316            }
317            return Ok(());
318        }
319
320        let (version, readable_by): (u32, u32) = tx.query_row(
321            "SELECT version, readable_by FROM TorSchemaMeta
322             WHERE name = 'TorDirStorage'",
323            [],
324            |row| Ok((row.get(0)?, row.get(1)?)),
325        )?;
326
327        if version < SCHEMA_VERSION {
328            if !readonly {
329                update_schema(&tx, version)?;
330                tx.commit()?;
331            } else {
332                return Err(Error::ReadOnlyStorage(
333                    ReadOnlyStorageError::IncompatibleSchema {
334                        schema: version,
335                        supported: SCHEMA_VERSION,
336                    },
337                ));
338            }
339
340            return Ok(());
341        } else if readable_by > SCHEMA_VERSION {
342            return Err(Error::UnrecognizedSchema {
343                schema: readable_by,
344                supported: SCHEMA_VERSION,
345            });
346        }
347
348        // rolls back the transaction, but nothing was done.
349        Ok(())
350    }
351
352    /// Read a blob from disk, mapping it if possible.
353    ///
354    /// Return `Ok(Err(.))` if the file for the blob was not found on disk;
355    /// returns an error in other cases.
356    ///
357    /// (See [`blob_consistency`] for information on why the blob might be absent.)
358    fn read_blob(&self, path: &str) -> Result<StdResult<InputString, AbsentBlob>> {
359        let file = match self.blob_dir.open(path, OpenOptions::new().read(true)) {
360            Ok(file) => file,
361            Err(fs_mistrust::Error::NotFound(_)) => {
362                warn!(
363                    "{:?} was listed in the database, but its corresponding file had been deleted",
364                    path
365                );
366                return Ok(Err(AbsentBlob::VanishedFile));
367            }
368            Err(e) => return Err(e.into()),
369        };
370
371        InputString::load(file)
372            .map_err(|err| Error::CacheFile {
373                action: "loading",
374                fname: PathBuf::from(path),
375                error: Arc::new(err),
376            })
377            .map(Ok)
378    }
379
380    /// Write a file to disk as a blob, and record it in the ExtDocs table.
381    ///
382    /// Return a SavedBlobHandle that describes where the blob is, and which
383    /// can be used either to commit the blob or delete it.
384    ///
385    /// See [`blob_consistency`] for more information on guarantees.
386    fn save_blob_internal(
387        &mut self,
388        contents: &[u8],
389        doctype: &str,
390        digest_type: &str,
391        digest: &[u8],
392        expires: OffsetDateTime,
393    ) -> Result<blob_handle::SavedBlobHandle<'_>> {
394        let digest = hex::encode(digest);
395        let digeststr = format!("{}-{}", digest_type, digest);
396        let fname = format!("{}_{}", doctype, digeststr);
397
398        let full_path = self.blob_dir.join(&fname)?;
399        let unlinker = blob_handle::Unlinker::new(&full_path);
400        self.blob_dir
401            .write_and_replace(&fname, contents)
402            .map_err(|e| match e {
403                fs_mistrust::Error::Io { err, .. } => Error::CacheFile {
404                    action: "saving",
405                    fname: full_path,
406                    error: err,
407                },
408                err => err.into(),
409            })?;
410
411        let tx = self.conn.unchecked_transaction()?;
412        tx.execute(INSERT_EXTDOC, params![digeststr, expires, doctype, fname])?;
413
414        Ok(blob_handle::SavedBlobHandle::new(
415            tx, fname, digeststr, unlinker,
416        ))
417    }
418
419    /// As `latest_consensus`, but do not retry.
420    fn latest_consensus_internal(
421        &self,
422        flavor: ConsensusFlavor,
423        pending: Option<bool>,
424    ) -> Result<StdResult<InputString, AbsentBlob>> {
425        trace!(?flavor, ?pending, "Loading latest consensus from cache");
426        let rv: Option<(OffsetDateTime, OffsetDateTime, String)> = match pending {
427            None => self
428                .conn
429                .query_row(FIND_CONSENSUS, params![flavor.name()], |row| row.try_into())
430                .optional()?,
431            Some(pending_val) => self
432                .conn
433                .query_row(
434                    FIND_CONSENSUS_P,
435                    params![pending_val, flavor.name()],
436                    |row| row.try_into(),
437                )
438                .optional()?,
439        };
440
441        if let Some((_va, _vu, filename)) = rv {
442            // TODO blobs: If the cache is inconsistent (because this blob is _vanished_), and the cache has not yet
443            // been cleaned, this may fail to find the latest consensus that we actually have.
444            self.read_blob(&filename)
445        } else {
446            Ok(Err(AbsentBlob::NothingToRead))
447        }
448    }
449
450    /// Save a blob to disk and commit it.
451    #[cfg(test)]
452    fn save_blob(
453        &mut self,
454        contents: &[u8],
455        doctype: &str,
456        digest_type: &str,
457        digest: &[u8],
458        expires: OffsetDateTime,
459    ) -> Result<String> {
460        let h = self.save_blob_internal(contents, doctype, digest_type, digest, expires)?;
461        let fname = h.fname().to_string();
462        h.commit()?;
463        Ok(fname)
464    }
465
466    /// Return the valid-after time for the latest non non-pending consensus,
467    #[cfg(test)]
468    // We should revise the tests to use latest_consensus_meta instead.
469    fn latest_consensus_time(&self, flavor: ConsensusFlavor) -> Result<Option<OffsetDateTime>> {
470        Ok(self
471            .latest_consensus_meta(flavor)?
472            .map(|m| m.lifetime().valid_after().into()))
473    }
474
475    /// Remove the blob with name `fname`, but do not give an error on failure.
476    ///
477    /// See [`blob_consistency`]: we should call this only having first ensured
478    /// that the blob is removed from the ExtDocs table.
479    fn remove_blob_or_warn<P: AsRef<Path>>(&self, fname: P) {
480        let fname = fname.as_ref();
481        if let Err(e) = self.blob_dir.remove_file(fname) {
482            warn_report!(e, "Unable to remove {}", fname.display_lossy());
483        }
484    }
485
486    /// Delete any blob files that are old enough, and not mentioned in the ExtDocs table.
487    ///
488    /// There shouldn't typically be any, but we don't want to let our cache grow infinitely
489    /// if we have a bug.
490    fn remove_unreferenced_blobs(
491        &self,
492        now: OffsetDateTime,
493        expiration: &ExpirationConfig,
494    ) -> Result<()> {
495        // Now, look for any unreferenced blobs that are a bit old.
496        for ent in self.blob_dir.read_directory(".")?.flatten() {
497            let md_error = |io_error| Error::CacheFile {
498                action: "getting metadata",
499                fname: ent.file_name().into(),
500                error: Arc::new(io_error),
501            };
502            if ent
503                .metadata()
504                .map_err(md_error)?
505                .modified()
506                .map_err(md_error)?
507                + expiration.consensuses
508                >= now
509            {
510                // this file is sufficiently recent that we should not remove it, just to be cautious.
511                continue;
512            }
513            let filename = match ent.file_name().into_string() {
514                Ok(s) => s,
515                Err(os_str) => {
516                    // This filename wasn't utf-8.  We will never create one of these.
517                    warn!(
518                        "Removing bizarre file '{}' from blob store.",
519                        os_str.to_string_lossy()
520                    );
521                    self.remove_blob_or_warn(ent.file_name());
522                    continue;
523                }
524            };
525            let found: (u32,) =
526                self.conn
527                    .query_row(COUNT_EXTDOC_BY_PATH, params![&filename], |row| {
528                        row.try_into()
529                    })?;
530            if found == (0,) {
531                warn!("Removing unreferenced file '{}' from blob store", &filename);
532                self.remove_blob_or_warn(ent.file_name());
533            }
534        }
535
536        Ok(())
537    }
538
539    /// Remove any entry in the ExtDocs table for which a blob file is vanished.
540    ///
541    /// This method is `O(n)` in the size of the ExtDocs table and the size of the directory.
542    /// It doesn't take self, to avoid problems with the borrow checker.
543    fn remove_entries_for_vanished_blobs<'a>(
544        blob_dir: &CheckedDir,
545        tx: &Transaction<'a>,
546    ) -> Result<usize> {
547        let in_directory: HashSet<PathBuf> = blob_dir
548            .read_directory(".")?
549            .flatten()
550            .map(|dir_entry| PathBuf::from(dir_entry.file_name()))
551            .collect();
552        let in_db: Vec<String> = tx
553            .prepare(FIND_ALL_EXTDOC_FILENAMES)?
554            .query_map([], |row| row.get::<_, String>(0))?
555            .collect::<StdResult<Vec<String>, _>>()?;
556
557        let mut n_removed = 0;
558        for fname in in_db {
559            if in_directory.contains(Path::new(&fname)) {
560                // The blob is present; great!
561                continue;
562            }
563
564            n_removed += tx.execute(DELETE_EXTDOC_BY_FILENAME, [fname])?;
565        }
566
567        Ok(n_removed)
568    }
569}
570
571impl Store for SqliteStore {
572    fn is_readonly(&self) -> bool {
573        match &self.lockfile {
574            LockFile::NotLocking => false, // no locks used; we can always write.
575            LockFile::Unlocked(_) => true, // lock in use but we don't have it; can't write.
576            LockFile::Locked(_) => false,  // we have the lock; we can write.
577        }
578    }
579
580    fn upgrade_to_readwrite(&mut self) -> Result<bool> {
581        let Some(sql_path) = self.sql_path.as_ref() else {
582            // This is an ephemeral database with no disk representation.
583            return Ok(true);
584        };
585
586        let lockpath = match &self.lockfile {
587            LockFile::NotLocking => {
588                // This should be unreachable.
589                return Err(
590                    internal!("No lockfile open; cannot upgrade to read-write storage").into(),
591                );
592            }
593            LockFile::Locked(_) => return Ok(true),
594            LockFile::Unlocked(path) => path,
595        };
596        // We aren't locked. Try to fix that.
597        let Some(guard) = LockFileGuard::try_lock(lockpath).map_err(Error::from_lockfile)? else {
598            // Somebody else has the lock.
599            return Ok(false);
600        };
601
602        // Open a fresh RW sql connection. If it fails, we'll unlock the guard
603        // and remain in our old state.
604        let new_conn = rusqlite::Connection::open(sql_path)?;
605        self.conn = new_conn;
606        self.lockfile = LockFile::Locked(guard);
607        Ok(true)
608    }
609    fn expire_all(&mut self, expiration: &ExpirationConfig) -> Result<()> {
610        let tx = self.conn.transaction()?;
611        // This works around a false positive; see
612        //   https://github.com/rust-lang/rust-clippy/issues/8114
613        #[allow(clippy::let_and_return)]
614        let expired_blobs: Vec<String> = {
615            let mut stmt = tx.prepare(FIND_EXPIRED_EXTDOCS)?;
616            let names: Vec<String> = stmt
617                .query_map([], |row| row.get::<_, String>(0))?
618                .collect::<StdResult<Vec<String>, _>>()?;
619            names
620        };
621
622        let now = now_utc();
623        tx.execute(DROP_OLD_EXTDOCS, [])?;
624
625        // In theory bad system clocks might generate table rows with times far in the future.
626        // However, for data which is cached here which comes from the network consensus,
627        // we rely on the fact that no consensus from the future exists, so this can't happen.
628        tx.execute(DROP_OLD_MICRODESCS, [now - expiration.microdescs])?;
629        tx.execute(DROP_OLD_AUTHCERTS, [now - expiration.authcerts])?;
630        tx.execute(DROP_OLD_CONSENSUSES, [now - expiration.consensuses])?;
631        tx.execute(DROP_OLD_ROUTERDESCS, [now - expiration.router_descs])?;
632
633        // Bridge descriptors come from bridges and bridges might send crazy times,
634        // so we need to discard any that look like they are from the future,
635        // since otherwise wrong far-future timestamps might live in our DB indefinitely.
636        #[cfg(feature = "bridge-client")]
637        tx.execute(DROP_OLD_BRIDGEDESCS, [now, now])?;
638
639        // Find all consensus blobs that are no longer referenced,
640        // and delete their entries from extdocs.
641        let remove_consensus_blobs = {
642            // TODO: This query can be O(n); but that won't matter for clients.
643            // For relays, we may want to add an index to speed it up, if we use this code there too.
644            let mut stmt = tx.prepare(FIND_UNREFERENCED_CONSENSUS_EXTDOCS)?;
645            let filenames: Vec<String> = stmt
646                .query_map([], |row| row.get::<_, String>(0))?
647                .collect::<StdResult<Vec<String>, _>>()?;
648            drop(stmt);
649            let mut stmt = tx.prepare(DELETE_EXTDOC_BY_FILENAME)?;
650            for fname in filenames.iter() {
651                stmt.execute([fname])?;
652            }
653            filenames
654        };
655
656        tx.commit()?;
657        // Now that the transaction has been committed, these blobs are
658        // unreferenced in the ExtDocs table, and we can remove them from disk.
659        let mut remove_blob_files: HashSet<_> = expired_blobs.iter().collect();
660        remove_blob_files.extend(remove_consensus_blobs.iter());
661
662        for name in remove_blob_files {
663            let fname = self.blob_dir.join(name);
664            if let Ok(fname) = fname {
665                if let Err(e) = std::fs::remove_file(&fname) {
666                    warn_report!(
667                        e,
668                        "Couldn't remove orphaned blob file {}",
669                        fname.display_lossy()
670                    );
671                }
672            }
673        }
674
675        self.remove_unreferenced_blobs(now, expiration)?;
676
677        Ok(())
678    }
679
680    // Note: We cannot, and do not, call this function when a transaction already exists.
681    fn latest_consensus(
682        &self,
683        flavor: ConsensusFlavor,
684        pending: Option<bool>,
685    ) -> Result<Option<InputString>> {
686        match self.latest_consensus_internal(flavor, pending)? {
687            Ok(s) => return Ok(Some(s)),
688            Err(AbsentBlob::NothingToRead) => return Ok(None),
689            Err(AbsentBlob::VanishedFile) => {
690                // If we get here, the file was vanished.  Clean up the DB and try again.
691            }
692        }
693
694        // We use unchecked_transaction() here because this API takes a non-mutable `SqliteStore`.
695        // `unchecked_transaction()` will give an error if it is used
696        // when a transaction already exists.
697        // That's fine: We don't call this function from inside this module,
698        // when a transaction might exist,
699        // and we can't call multiple SqliteStore functions at once: it isn't sync.
700        // Here we enforce that:
701        static_assertions::assert_not_impl_any!(SqliteStore: Sync);
702
703        // If we decide that this is unacceptable,
704        // then since sqlite doesn't really support concurrent use of a connection,
705        // we _could_ change the Store::latest_consensus API take &mut self,
706        // or we could add a mutex,
707        // or we could just not use a transaction object.
708        let tx = self.conn.unchecked_transaction()?;
709        Self::remove_entries_for_vanished_blobs(&self.blob_dir, &tx)?;
710        tx.commit()?;
711
712        match self.latest_consensus_internal(flavor, pending)? {
713            Ok(s) => Ok(Some(s)),
714            Err(AbsentBlob::NothingToRead) => Ok(None),
715            Err(AbsentBlob::VanishedFile) => {
716                warn!("Somehow remove_entries_for_vanished_blobs didn't resolve a VanishedFile");
717                Ok(None)
718            }
719        }
720    }
721
722    fn latest_consensus_meta(&self, flavor: ConsensusFlavor) -> Result<Option<ConsensusMeta>> {
723        let mut stmt = self.conn.prepare(FIND_LATEST_CONSENSUS_META)?;
724        let mut rows = stmt.query(params![flavor.name()])?;
725        if let Some(row) = rows.next()? {
726            Ok(Some(cmeta_from_row(row)?))
727        } else {
728            Ok(None)
729        }
730    }
731    #[cfg(test)]
732    fn consensus_by_meta(&self, cmeta: &ConsensusMeta) -> Result<InputString> {
733        if let Some((text, _)) =
734            self.consensus_by_sha3_digest_of_signed_part(cmeta.sha3_256_of_signed())?
735        {
736            Ok(text)
737        } else {
738            Err(Error::CacheCorruption(
739                "couldn't find a consensus we thought we had.",
740            ))
741        }
742    }
743    fn consensus_by_sha3_digest_of_signed_part(
744        &self,
745        d: &[u8; 32],
746    ) -> Result<Option<(InputString, ConsensusMeta)>> {
747        let digest = hex::encode(d);
748        let mut stmt = self
749            .conn
750            .prepare(FIND_CONSENSUS_AND_META_BY_DIGEST_OF_SIGNED)?;
751        let mut rows = stmt.query(params![digest])?;
752        if let Some(row) = rows.next()? {
753            let meta = cmeta_from_row(row)?;
754            let fname: String = row.get(5)?;
755            if let Ok(text) = self.read_blob(&fname)? {
756                return Ok(Some((text, meta)));
757            }
758        }
759        Ok(None)
760    }
761    fn store_consensus(
762        &mut self,
763        cmeta: &ConsensusMeta,
764        flavor: ConsensusFlavor,
765        pending: bool,
766        contents: &str,
767    ) -> Result<()> {
768        let lifetime = cmeta.lifetime();
769        let sha3_of_signed = cmeta.sha3_256_of_signed();
770        let sha3_of_whole = cmeta.sha3_256_of_whole();
771        let valid_after: OffsetDateTime = lifetime.valid_after().into();
772        let fresh_until: OffsetDateTime = lifetime.fresh_until().into();
773        let valid_until: OffsetDateTime = lifetime.valid_until().into();
774
775        /// How long to keep a consensus around after it has expired
776        const CONSENSUS_LIFETIME: time::Duration = time::Duration::days(4);
777
778        // After a few days have passed, a consensus is no good for
779        // anything at all, not even diffs.
780        let expires = valid_until + CONSENSUS_LIFETIME;
781
782        let doctype = format!("con_{}", flavor.name());
783
784        let h = self.save_blob_internal(
785            contents.as_bytes(),
786            &doctype,
787            "sha3-256",
788            &sha3_of_whole[..],
789            expires,
790        )?;
791        h.tx().execute(
792            INSERT_CONSENSUS,
793            params![
794                valid_after,
795                fresh_until,
796                valid_until,
797                flavor.name(),
798                pending,
799                hex::encode(sha3_of_signed),
800                h.digest_string()
801            ],
802        )?;
803        h.commit()?;
804        Ok(())
805    }
806    fn mark_consensus_usable(&mut self, cmeta: &ConsensusMeta) -> Result<()> {
807        let d = hex::encode(cmeta.sha3_256_of_whole());
808        let digest = format!("sha3-256-{}", d);
809
810        let tx = self.conn.transaction()?;
811        let n = tx.execute(MARK_CONSENSUS_NON_PENDING, params![digest])?;
812        trace!("Marked {} consensuses usable", n);
813        tx.commit()?;
814
815        Ok(())
816    }
817    fn delete_consensus(&mut self, cmeta: &ConsensusMeta) -> Result<()> {
818        let d = hex::encode(cmeta.sha3_256_of_whole());
819        let digest = format!("sha3-256-{}", d);
820
821        // TODO: We should probably remove the blob as well, but for now
822        // this is enough.
823        let tx = self.conn.transaction()?;
824        tx.execute(REMOVE_CONSENSUS, params![digest])?;
825        tx.commit()?;
826
827        Ok(())
828    }
829
830    fn authcerts(&self, certs: &[AuthCertKeyIds]) -> Result<HashMap<AuthCertKeyIds, String>> {
831        let mut result = HashMap::new();
832        // TODO(nickm): Do I need to get a transaction here for performance?
833        let mut stmt = self.conn.prepare(FIND_AUTHCERT)?;
834
835        for ids in certs {
836            let id_digest = hex::encode(ids.id_fingerprint.as_bytes());
837            let sk_digest = hex::encode(ids.sk_fingerprint.as_bytes());
838            if let Some(contents) = stmt
839                .query_row(params![id_digest, sk_digest], |row| row.get::<_, String>(0))
840                .optional()?
841            {
842                result.insert(*ids, contents);
843            }
844        }
845
846        Ok(result)
847    }
848    fn store_authcerts(&mut self, certs: &[(AuthCertMeta, &str)]) -> Result<()> {
849        let tx = self.conn.transaction()?;
850        let mut stmt = tx.prepare(INSERT_AUTHCERT)?;
851        for (meta, content) in certs {
852            let ids = meta.key_ids();
853            let id_digest = hex::encode(ids.id_fingerprint.as_bytes());
854            let sk_digest = hex::encode(ids.sk_fingerprint.as_bytes());
855            let published: OffsetDateTime = meta.published().into();
856            let expires: OffsetDateTime = meta.expires().into();
857            stmt.execute(params![id_digest, sk_digest, published, expires, content])?;
858        }
859        stmt.finalize()?;
860        tx.commit()?;
861        Ok(())
862    }
863
864    fn microdescs(&self, digests: &[MdDigest]) -> Result<HashMap<MdDigest, String>> {
865        let mut result = HashMap::new();
866        let mut stmt = self.conn.prepare(FIND_MD)?;
867
868        // TODO(nickm): Should I speed this up with a transaction, or
869        // does it not matter for queries?
870        for md_digest in digests {
871            let h_digest = hex::encode(md_digest);
872            if let Some(contents) = stmt
873                .query_row(params![h_digest], |row| row.get::<_, String>(0))
874                .optional()?
875            {
876                result.insert(*md_digest, contents);
877            }
878        }
879
880        Ok(result)
881    }
882    fn store_microdescs(&mut self, digests: &[(&str, &MdDigest)], when: SystemTime) -> Result<()> {
883        let when: OffsetDateTime = when.into();
884
885        let tx = self.conn.transaction()?;
886        let mut stmt = tx.prepare(INSERT_MD)?;
887
888        for (content, md_digest) in digests {
889            let h_digest = hex::encode(md_digest);
890            stmt.execute(params![h_digest, when, content])?;
891        }
892        stmt.finalize()?;
893        tx.commit()?;
894        Ok(())
895    }
896    fn update_microdescs_listed(&mut self, digests: &[MdDigest], when: SystemTime) -> Result<()> {
897        let tx = self.conn.transaction()?;
898        let mut stmt = tx.prepare(UPDATE_MD_LISTED)?;
899        let when: OffsetDateTime = when.into();
900
901        for md_digest in digests {
902            let h_digest = hex::encode(md_digest);
903            stmt.execute(params![when, h_digest])?;
904        }
905
906        stmt.finalize()?;
907        tx.commit()?;
908        Ok(())
909    }
910
911    #[cfg(feature = "routerdesc")]
912    fn routerdescs(&self, digests: &[RdDigest]) -> Result<HashMap<RdDigest, String>> {
913        let mut result = HashMap::new();
914        let mut stmt = self.conn.prepare(FIND_RD)?;
915
916        // TODO(nickm): Should I speed this up with a transaction, or
917        // does it not matter for queries?
918        for rd_digest in digests {
919            let h_digest = hex::encode(rd_digest);
920            if let Some(contents) = stmt
921                .query_row(params![h_digest], |row| row.get::<_, String>(0))
922                .optional()?
923            {
924                result.insert(*rd_digest, contents);
925            }
926        }
927
928        Ok(result)
929    }
930    #[cfg(feature = "routerdesc")]
931    fn store_routerdescs(&mut self, digests: &[(&str, SystemTime, &RdDigest)]) -> Result<()> {
932        let tx = self.conn.transaction()?;
933        let mut stmt = tx.prepare(INSERT_RD)?;
934
935        for (content, when, rd_digest) in digests {
936            let when: OffsetDateTime = (*when).into();
937            let h_digest = hex::encode(rd_digest);
938            stmt.execute(params![h_digest, when, content])?;
939        }
940        stmt.finalize()?;
941        tx.commit()?;
942        Ok(())
943    }
944
945    #[cfg(feature = "bridge-client")]
946    fn lookup_bridgedesc(&self, bridge: &BridgeConfig) -> Result<Option<CachedBridgeDescriptor>> {
947        let bridge_line = bridge.to_string();
948        Ok(self
949            .conn
950            .query_row(FIND_BRIDGEDESC, params![bridge_line], |row| {
951                let (fetched, document): (OffsetDateTime, _) = row.try_into()?;
952                let fetched = fetched.into();
953                Ok(CachedBridgeDescriptor { fetched, document })
954            })
955            .optional()?)
956    }
957
958    #[cfg(feature = "bridge-client")]
959    fn store_bridgedesc(
960        &mut self,
961        bridge: &BridgeConfig,
962        entry: CachedBridgeDescriptor,
963        until: SystemTime,
964    ) -> Result<()> {
965        if self.is_readonly() {
966            // Hopefully whoever *does* have the lock will update the cache.
967            // Otherwise it will contain a stale entry forever
968            // (which we'll ignore, but waste effort on).
969            return Ok(());
970        }
971        let bridge_line = bridge.to_string();
972        let row = params![
973            bridge_line,
974            OffsetDateTime::from(entry.fetched),
975            OffsetDateTime::from(until),
976            entry.document,
977        ];
978        self.conn.execute(INSERT_BRIDGEDESC, row)?;
979        Ok(())
980    }
981
982    #[cfg(feature = "bridge-client")]
983    fn delete_bridgedesc(&mut self, bridge: &BridgeConfig) -> Result<()> {
984        if self.is_readonly() {
985            // This is called when we find corrupted or stale cache entries,
986            // to stop us wasting time on them next time.
987            // Hopefully whoever *does* have the lock will do this.
988            return Ok(());
989        }
990        let bridge_line = bridge.to_string();
991        self.conn.execute(DELETE_BRIDGEDESC, params![bridge_line])?;
992        Ok(())
993    }
994
995    fn update_protocol_recommendations(
996        &mut self,
997        valid_after: SystemTime,
998        protocols: &tor_netdoc::doc::netstatus::ProtoStatuses,
999    ) -> Result<()> {
1000        let json =
1001            serde_json::to_string(&protocols).map_err(into_internal!("Cannot encode protocols"))?;
1002        let params = params![OffsetDateTime::from(valid_after), json];
1003        self.conn.execute(UPDATE_PROTOCOL_STATUS, params)?;
1004        Ok(())
1005    }
1006
1007    fn cached_protocol_recommendations(
1008        &self,
1009    ) -> Result<Option<(SystemTime, tor_netdoc::doc::netstatus::ProtoStatuses)>> {
1010        let opt_row: Option<(OffsetDateTime, String)> = self
1011            .conn
1012            .query_row(FIND_LATEST_PROTOCOL_STATUS, [], |row| {
1013                Ok((row.get(0)?, row.get(1)?))
1014            })
1015            .optional()?;
1016
1017        let (date, json) = match opt_row {
1018            Some(v) => v,
1019            None => return Ok(None),
1020        };
1021
1022        let date = date.into();
1023        let statuses: tor_netdoc::doc::netstatus::ProtoStatuses =
1024            serde_json::from_str(json.as_str()).map_err(|e| Error::BadJsonInCache(Arc::new(e)))?;
1025
1026        Ok(Some((date, statuses)))
1027    }
1028}
1029
1030/// Functionality related to uncommitted blobs.
1031mod blob_handle {
1032    use std::path::{Path, PathBuf};
1033
1034    use crate::Result;
1035    use rusqlite::Transaction;
1036    use tor_basic_utils::PathExt as _;
1037    use tor_error::warn_report;
1038
1039    /// Handle to a blob that we have saved to disk but
1040    /// not yet committed to
1041    /// the database, and the database transaction where we added a reference to it.
1042    ///
1043    /// Used to either commit the blob (by calling [`SavedBlobHandle::commit`]),
1044    /// or roll it back (by dropping the [`SavedBlobHandle`] without committing it.)
1045    #[must_use]
1046    pub(super) struct SavedBlobHandle<'a> {
1047        /// Transaction we're using to add the blob to the ExtDocs table.
1048        ///
1049        /// Note that struct fields are dropped in declaration order,
1050        /// so when we drop an uncommitted SavedBlobHandle,
1051        /// we roll back the transaction before we delete the file.
1052        /// (In practice, either order would be fine.)
1053        tx: Transaction<'a>,
1054        /// Filename for the file, with respect to the blob directory.
1055        fname: String,
1056        /// Declared digest string for this blob. Of the format
1057        /// "digesttype-hexstr".
1058        digeststr: String,
1059        /// An 'unlinker' for the blob file.
1060        unlinker: Unlinker,
1061    }
1062
1063    impl<'a> SavedBlobHandle<'a> {
1064        /// Construct a SavedBlobHandle from its parts.
1065        pub(super) fn new(
1066            tx: Transaction<'a>,
1067            fname: String,
1068            digeststr: String,
1069            unlinker: Unlinker,
1070        ) -> Self {
1071            Self {
1072                tx,
1073                fname,
1074                digeststr,
1075                unlinker,
1076            }
1077        }
1078
1079        /// Return a reference to the underlying database transaction.
1080        pub(super) fn tx(&self) -> &Transaction<'a> {
1081            &self.tx
1082        }
1083        /// Return the digest string of the saved blob.
1084        /// Other tables use this as a foreign key into ExtDocs.digest
1085        pub(super) fn digest_string(&self) -> &str {
1086            self.digeststr.as_ref()
1087        }
1088        /// Return the filename of this blob within the blob directory.
1089        #[allow(unused)] // used for testing.
1090        pub(super) fn fname(&self) -> &str {
1091            self.fname.as_ref()
1092        }
1093        /// Commit the relevant database transaction.
1094        pub(super) fn commit(self) -> Result<()> {
1095            // The blob has been written to disk, so it is safe to
1096            // commit the transaction.
1097            // If the commit returns an error, self.unlinker will remove the blob.
1098            // (This could result in a vanished blob if the commit reports an error,
1099            // but the transaction is still visible in the database.)
1100            self.tx.commit()?;
1101            // If we reach this point, we don't want to remove the file.
1102            self.unlinker.forget();
1103            Ok(())
1104        }
1105    }
1106
1107    /// Handle to a file which we might have to delete.
1108    ///
1109    /// When this handle is dropped, the file gets deleted, unless you have
1110    /// first called [`Unlinker::forget`].
1111    pub(super) struct Unlinker {
1112        /// The location of the file to remove, or None if we shouldn't
1113        /// remove it.
1114        p: Option<PathBuf>,
1115    }
1116    impl Unlinker {
1117        /// Make a new Unlinker for a given filename.
1118        pub(super) fn new<P: AsRef<Path>>(p: P) -> Self {
1119            Unlinker {
1120                p: Some(p.as_ref().to_path_buf()),
1121            }
1122        }
1123        /// Forget about this unlinker, so that the corresponding file won't
1124        /// get dropped.
1125        fn forget(mut self) {
1126            self.p = None;
1127        }
1128    }
1129    impl Drop for Unlinker {
1130        fn drop(&mut self) {
1131            if let Some(p) = self.p.take() {
1132                if let Err(e) = std::fs::remove_file(&p) {
1133                    warn_report!(
1134                        e,
1135                        "Couldn't remove rolled-back blob file {}",
1136                        p.display_lossy()
1137                    );
1138                }
1139            }
1140        }
1141    }
1142}
1143
1144/// Convert a hexadecimal sha3-256 digest from the database into an array.
1145fn digest_from_hex(s: &str) -> Result<[u8; 32]> {
1146    let mut bytes = [0_u8; 32];
1147    hex::decode_to_slice(s, &mut bytes[..]).map_err(Error::BadHexInCache)?;
1148    Ok(bytes)
1149}
1150
1151/// Convert a hexadecimal sha3-256 "digest string" as used in the
1152/// digest column from the database into an array.
1153fn digest_from_dstr(s: &str) -> Result<[u8; 32]> {
1154    if let Some(stripped) = s.strip_prefix("sha3-256-") {
1155        digest_from_hex(stripped)
1156    } else {
1157        Err(Error::CacheCorruption("Invalid digest in database"))
1158    }
1159}
1160
1161/// Create a ConsensusMeta from a `Row` returned by one of
1162/// `FIND_LATEST_CONSENSUS_META` or `FIND_CONSENSUS_AND_META_BY_DIGEST`.
1163fn cmeta_from_row(row: &rusqlite::Row<'_>) -> Result<ConsensusMeta> {
1164    let va: OffsetDateTime = row.get(0)?;
1165    let fu: OffsetDateTime = row.get(1)?;
1166    let vu: OffsetDateTime = row.get(2)?;
1167    let d_signed: String = row.get(3)?;
1168    let d_all: String = row.get(4)?;
1169    let lifetime = Lifetime::new(va.into(), fu.into(), vu.into())
1170        .map_err(|_| Error::CacheCorruption("inconsistent lifetime in database"))?;
1171    Ok(ConsensusMeta::new(
1172        lifetime,
1173        digest_from_hex(&d_signed)?,
1174        digest_from_dstr(&d_all)?,
1175    ))
1176}
1177
1178/// Return `SystemTime::get()` as an OffsetDateTime in UTC.
1179fn now_utc() -> OffsetDateTime {
1180    SystemTime::get().into()
1181}
1182
1183/// Set up the tables for the arti cache schema in a sqlite database.
1184const INSTALL_V0_SCHEMA: &str = "
1185  -- Helps us version the schema.  The schema here corresponds to a
1186  -- version number called 'version', and it should be readable by
1187  -- anybody who is compliant with versions of at least 'readable_by'.
1188  CREATE TABLE TorSchemaMeta (
1189     name TEXT NOT NULL PRIMARY KEY,
1190     version INTEGER NOT NULL,
1191     readable_by INTEGER NOT NULL
1192  );
1193
1194  INSERT INTO TorSchemaMeta (name, version, readable_by) VALUES ( 'TorDirStorage', 0, 0 );
1195
1196  -- Keeps track of external blobs on disk.
1197  CREATE TABLE ExtDocs (
1198    -- Records a digest of the file contents, in the form '<digest_type>-hexstr'
1199    digest TEXT PRIMARY KEY NOT NULL,
1200    -- When was this file created?
1201    created DATE NOT NULL,
1202    -- After what time will this file definitely be useless?
1203    expires DATE NOT NULL,
1204    -- What is the type of this file? Currently supported are 'con_<flavor>'.
1205    --   (Before tor-dirmgr ~0.28.0, we would erroneously record 'con_flavor' as 'sha3-256';
1206    --   Nothing depended on this yet, but will be used in the future
1207    --   as we add more large-document types.)
1208    type TEXT NOT NULL,
1209    -- Filename for this file within our blob directory.
1210    filename TEXT NOT NULL
1211  );
1212
1213  -- All the microdescriptors we know about.
1214  CREATE TABLE Microdescs (
1215    sha256_digest TEXT PRIMARY KEY NOT NULL,
1216    last_listed DATE NOT NULL,
1217    contents BLOB NOT NULL
1218  );
1219
1220  -- All the authority certificates we know.
1221  CREATE TABLE Authcerts (
1222    id_digest TEXT NOT NULL,
1223    sk_digest TEXT NOT NULL,
1224    published DATE NOT NULL,
1225    expires DATE NOT NULL,
1226    contents BLOB NOT NULL,
1227    PRIMARY KEY (id_digest, sk_digest)
1228  );
1229
1230  -- All the consensuses we're storing.
1231  CREATE TABLE Consensuses (
1232    valid_after DATE NOT NULL,
1233    fresh_until DATE NOT NULL,
1234    valid_until DATE NOT NULL,
1235    flavor TEXT NOT NULL,
1236    pending BOOLEAN NOT NULL,
1237    sha3_of_signed_part TEXT NOT NULL,
1238    digest TEXT NOT NULL,
1239    FOREIGN KEY (digest) REFERENCES ExtDocs (digest) ON DELETE CASCADE
1240  );
1241  CREATE INDEX Consensuses_vu on CONSENSUSES(valid_until);
1242
1243";
1244
1245/// Update the database schema, from each version to the next
1246const UPDATE_SCHEMA: &[&str] = &["
1247  -- Update the database schema from version 0 to version 1.
1248  CREATE TABLE RouterDescs (
1249    sha1_digest TEXT PRIMARY KEY NOT NULL,
1250    published DATE NOT NULL,
1251    contents BLOB NOT NULL
1252  );
1253","
1254  -- Update the database schema from version 1 to version 2.
1255  -- We create this table even if the bridge-client feature is disabled, but then don't touch it at all.
1256  CREATE TABLE BridgeDescs (
1257    bridge_line TEXT PRIMARY KEY NOT NULL,
1258    fetched DATE NOT NULL,
1259    until DATE NOT NULL,
1260    contents BLOB NOT NULL
1261  );
1262","
1263 -- Update the database schema from version 2 to version 3.
1264
1265 -- Table to hold our latest ProtocolStatuses object, to tell us if we're obsolete.
1266 -- We hold this independently from our consensus,
1267 -- since we want to read it very early in our startup process,
1268 -- even if the consensus is expired.
1269 CREATE TABLE ProtocolStatus (
1270    -- Enforce that there is only one row in this table.
1271    -- (This is a bit kludgy, but I am assured that it is a common practice.)
1272    zero INTEGER PRIMARY KEY NOT NULL,
1273    -- valid-after date of the consensus from which we got this status
1274    date DATE NOT NULL,
1275    -- ProtoStatuses object, encoded as json
1276    statuses TEXT NOT NULL
1277 );
1278"];
1279
1280/// Update the database schema version tracking, from each version to the next
1281const UPDATE_SCHEMA_VERSION: &str = "
1282  UPDATE TorSchemaMeta SET version=? WHERE version<?;
1283";
1284
1285/// Version number used for this version of the arti cache schema.
1286const SCHEMA_VERSION: u32 = UPDATE_SCHEMA.len() as u32;
1287
1288/// Query: find the latest-expiring microdesc consensus with a given
1289/// pending status.
1290const FIND_CONSENSUS_P: &str = "
1291  SELECT valid_after, valid_until, filename
1292  FROM Consensuses
1293  INNER JOIN ExtDocs ON ExtDocs.digest = Consensuses.digest
1294  WHERE pending = ? AND flavor = ?
1295  ORDER BY valid_until DESC
1296  LIMIT 1;
1297";
1298
1299/// Query: find the latest-expiring microdesc consensus, regardless of
1300/// pending status.
1301const FIND_CONSENSUS: &str = "
1302  SELECT valid_after, valid_until, filename
1303  FROM Consensuses
1304  INNER JOIN ExtDocs ON ExtDocs.digest = Consensuses.digest
1305  WHERE flavor = ?
1306  ORDER BY valid_until DESC
1307  LIMIT 1;
1308";
1309
1310/// Query: Find the valid-after time for the latest-expiring
1311/// non-pending consensus of a given flavor.
1312const FIND_LATEST_CONSENSUS_META: &str = "
1313  SELECT valid_after, fresh_until, valid_until, sha3_of_signed_part, digest
1314  FROM Consensuses
1315  WHERE pending = 0 AND flavor = ?
1316  ORDER BY valid_until DESC
1317  LIMIT 1;
1318";
1319
1320/// Look up a consensus by its digest-of-signed-part string.
1321const FIND_CONSENSUS_AND_META_BY_DIGEST_OF_SIGNED: &str = "
1322  SELECT valid_after, fresh_until, valid_until, sha3_of_signed_part, Consensuses.digest, filename
1323  FROM Consensuses
1324  INNER JOIN ExtDocs on ExtDocs.digest = Consensuses.digest
1325  WHERE Consensuses.sha3_of_signed_part = ?
1326  LIMIT 1;
1327";
1328
1329/// Query: Update the consensus whose digest field is 'digest' to call it
1330/// no longer pending.
1331const MARK_CONSENSUS_NON_PENDING: &str = "
1332  UPDATE Consensuses
1333  SET pending = 0
1334  WHERE digest = ?;
1335";
1336
1337/// Query: Remove the consensus with a given digest field.
1338#[allow(dead_code)]
1339const REMOVE_CONSENSUS: &str = "
1340  DELETE FROM Consensuses
1341  WHERE digest = ?;
1342";
1343
1344/// Query: Find the authority certificate with given key digests.
1345const FIND_AUTHCERT: &str = "
1346  SELECT contents FROM AuthCerts WHERE id_digest = ? AND sk_digest = ?;
1347";
1348
1349/// Query: find the microdescriptor with a given hex-encoded sha256 digest
1350const FIND_MD: &str = "
1351  SELECT contents
1352  FROM Microdescs
1353  WHERE sha256_digest = ?
1354";
1355
1356/// Query: find the router descriptors with a given hex-encoded sha1 digest
1357#[cfg(feature = "routerdesc")]
1358const FIND_RD: &str = "
1359  SELECT contents
1360  FROM RouterDescs
1361  WHERE sha1_digest = ?
1362";
1363
1364/// Query: find every ExtDocs member that has expired.
1365const FIND_EXPIRED_EXTDOCS: &str = "
1366  SELECT filename FROM ExtDocs where expires < datetime('now');
1367";
1368
1369/// Query: find whether an ExtDoc is listed.
1370const COUNT_EXTDOC_BY_PATH: &str = "
1371  SELECT COUNT(*) FROM ExtDocs WHERE filename = ?;
1372";
1373
1374/// Query: Add a new entry to ExtDocs.
1375const INSERT_EXTDOC: &str = "
1376  INSERT OR REPLACE INTO ExtDocs ( digest, created, expires, type, filename )
1377  VALUES ( ?, datetime('now'), ?, ?, ? );
1378";
1379
1380/// Query: Add a new consensus.
1381const INSERT_CONSENSUS: &str = "
1382  INSERT OR REPLACE INTO Consensuses
1383    ( valid_after, fresh_until, valid_until, flavor, pending, sha3_of_signed_part, digest )
1384  VALUES ( ?, ?, ?, ?, ?, ?, ? );
1385";
1386
1387/// Query: Add a new AuthCert
1388const INSERT_AUTHCERT: &str = "
1389  INSERT OR REPLACE INTO Authcerts
1390    ( id_digest, sk_digest, published, expires, contents)
1391  VALUES ( ?, ?, ?, ?, ? );
1392";
1393
1394/// Query: Add a new microdescriptor
1395const INSERT_MD: &str = "
1396  INSERT OR REPLACE INTO Microdescs ( sha256_digest, last_listed, contents )
1397  VALUES ( ?, ?, ? );
1398";
1399
1400/// Query: Add a new router descriptor
1401#[allow(unused)]
1402#[cfg(feature = "routerdesc")]
1403const INSERT_RD: &str = "
1404  INSERT OR REPLACE INTO RouterDescs ( sha1_digest, published, contents )
1405  VALUES ( ?, ?, ? );
1406";
1407
1408/// Query: Change the time when a given microdescriptor was last listed.
1409const UPDATE_MD_LISTED: &str = "
1410  UPDATE Microdescs
1411  SET last_listed = max(last_listed, ?)
1412  WHERE sha256_digest = ?;
1413";
1414
1415/// Query: Find a cached bridge descriptor
1416#[cfg(feature = "bridge-client")]
1417const FIND_BRIDGEDESC: &str = "SELECT fetched, contents FROM BridgeDescs WHERE bridge_line = ?;";
1418/// Query: Record a cached bridge descriptor
1419#[cfg(feature = "bridge-client")]
1420const INSERT_BRIDGEDESC: &str = "
1421  INSERT OR REPLACE INTO BridgeDescs ( bridge_line, fetched, until, contents )
1422  VALUES ( ?, ?, ?, ? );
1423";
1424/// Query: Remove a cached bridge descriptor
1425#[cfg(feature = "bridge-client")]
1426#[allow(dead_code)]
1427const DELETE_BRIDGEDESC: &str = "DELETE FROM BridgeDescs WHERE bridge_line = ?;";
1428
1429/// Query: Find all consensus extdocs that are not referenced in the consensus table.
1430///
1431/// Note: use of `sha3-256` is a synonym for `con_%` is a workaround.
1432const FIND_UNREFERENCED_CONSENSUS_EXTDOCS: &str = "
1433    SELECT filename FROM ExtDocs WHERE
1434         (type LIKE 'con_%' OR type = 'sha3-256')
1435    AND NOT EXISTS
1436         (SELECT digest FROM Consensuses WHERE Consensuses.digest = ExtDocs.digest);";
1437
1438/// Query: Discard every expired extdoc.
1439///
1440/// External documents aren't exposed through [`Store`].
1441const DROP_OLD_EXTDOCS: &str = "DELETE FROM ExtDocs WHERE expires < datetime('now');";
1442
1443/// Query: Discard an extdoc with a given path.
1444const DELETE_EXTDOC_BY_FILENAME: &str = "DELETE FROM ExtDocs WHERE filename = ?;";
1445
1446/// Query: List all extdoc filenames.
1447const FIND_ALL_EXTDOC_FILENAMES: &str = "SELECT filename FROM ExtDocs;";
1448
1449/// Query: Get the latest protocol status.
1450const FIND_LATEST_PROTOCOL_STATUS: &str = "SELECT date, statuses FROM ProtocolStatus WHERE zero=0;";
1451/// Query: Update the latest protocol status.
1452const UPDATE_PROTOCOL_STATUS: &str = "INSERT OR REPLACE INTO ProtocolStatus VALUES ( 0, ?, ? );";
1453
1454/// Query: Discard every router descriptor that hasn't been listed for 3
1455/// months.
1456// TODO: Choose a more realistic time.
1457const DROP_OLD_ROUTERDESCS: &str = "DELETE FROM RouterDescs WHERE published < ?;";
1458/// Query: Discard every microdescriptor that hasn't been listed for 3 months.
1459// TODO: Choose a more realistic time.
1460const DROP_OLD_MICRODESCS: &str = "DELETE FROM Microdescs WHERE last_listed < ?;";
1461/// Query: Discard every expired authority certificate.
1462const DROP_OLD_AUTHCERTS: &str = "DELETE FROM Authcerts WHERE expires < ?;";
1463/// Query: Discard every consensus that's been expired for at least
1464/// two days.
1465const DROP_OLD_CONSENSUSES: &str = "DELETE FROM Consensuses WHERE valid_until < ?;";
1466/// Query: Discard every bridge descriptor that is too old, or from the future.  (Both ?=now.)
1467#[cfg(feature = "bridge-client")]
1468const DROP_OLD_BRIDGEDESCS: &str = "DELETE FROM BridgeDescs WHERE ? > until OR fetched > ?;";
1469
1470#[cfg(test)]
1471pub(crate) mod test {
1472    #![allow(clippy::unwrap_used)]
1473    use super::*;
1474    use crate::storage::EXPIRATION_DEFAULTS;
1475    use digest::Digest;
1476    use hex_literal::hex;
1477    use tempfile::{TempDir, tempdir};
1478    use time::ext::NumericalDuration;
1479    use tor_llcrypto::d::Sha3_256;
1480
1481    pub(crate) fn new_empty() -> Result<(TempDir, SqliteStore)> {
1482        let tmp_dir = tempdir().unwrap();
1483        let sql_path = tmp_dir.path().join("db.sql");
1484        let conn = rusqlite::Connection::open(sql_path)?;
1485        let blob_path = tmp_dir.path().join("blobs");
1486        let blob_dir = fs_mistrust::Mistrust::builder()
1487            .dangerously_trust_everyone()
1488            .build()
1489            .unwrap()
1490            .verifier()
1491            .make_secure_dir(blob_path)
1492            .unwrap();
1493        let store = SqliteStore::from_conn(conn, blob_dir)?;
1494
1495        Ok((tmp_dir, store))
1496    }
1497
1498    #[test]
1499    fn init() -> Result<()> {
1500        let tmp_dir = tempdir().unwrap();
1501        let blob_dir = fs_mistrust::Mistrust::builder()
1502            .dangerously_trust_everyone()
1503            .build()
1504            .unwrap()
1505            .verifier()
1506            .secure_dir(&tmp_dir)
1507            .unwrap();
1508        let sql_path = tmp_dir.path().join("db.sql");
1509        // Initial setup: everything should work.
1510        {
1511            let conn = rusqlite::Connection::open(&sql_path)?;
1512            let _store = SqliteStore::from_conn(conn, blob_dir.clone())?;
1513        }
1514        // Second setup: shouldn't need to upgrade.
1515        {
1516            let conn = rusqlite::Connection::open(&sql_path)?;
1517            let _store = SqliteStore::from_conn(conn, blob_dir.clone())?;
1518        }
1519        // Third setup: shouldn't need to upgrade.
1520        {
1521            let conn = rusqlite::Connection::open(&sql_path)?;
1522            conn.execute_batch("UPDATE TorSchemaMeta SET version = 9002;")?;
1523            let _store = SqliteStore::from_conn(conn, blob_dir.clone())?;
1524        }
1525        // Fourth: this says we can't read it, so we'll get an error.
1526        {
1527            let conn = rusqlite::Connection::open(&sql_path)?;
1528            conn.execute_batch("UPDATE TorSchemaMeta SET readable_by = 9001;")?;
1529            let val = SqliteStore::from_conn(conn, blob_dir);
1530            assert!(val.is_err());
1531        }
1532        Ok(())
1533    }
1534
1535    #[test]
1536    fn bad_blob_fname() -> Result<()> {
1537        let (_tmp_dir, store) = new_empty()?;
1538
1539        assert!(store.blob_dir.join("abcd").is_ok());
1540        assert!(store.blob_dir.join("abcd..").is_ok());
1541        assert!(store.blob_dir.join("..abcd..").is_ok());
1542        assert!(store.blob_dir.join(".abcd").is_ok());
1543
1544        assert!(store.blob_dir.join("..").is_err());
1545        assert!(store.blob_dir.join("../abcd").is_err());
1546        assert!(store.blob_dir.join("/abcd").is_err());
1547
1548        Ok(())
1549    }
1550
1551    #[test]
1552    fn blobs() -> Result<()> {
1553        let (_tmp_dir, mut store) = new_empty()?;
1554
1555        let now = now_utc();
1556        let one_week = 1.weeks();
1557
1558        let fname1 = store.save_blob(
1559            b"Hello world",
1560            "greeting",
1561            "sha1",
1562            &hex!("7b502c3a1f48c8609ae212cdfb639dee39673f5e"),
1563            now + one_week,
1564        )?;
1565
1566        let fname2 = store.save_blob(
1567            b"Goodbye, dear friends",
1568            "greeting",
1569            "sha1",
1570            &hex!("2149c2a7dbf5be2bb36fb3c5080d0fb14cb3355c"),
1571            now - one_week,
1572        )?;
1573
1574        assert_eq!(
1575            fname1,
1576            "greeting_sha1-7b502c3a1f48c8609ae212cdfb639dee39673f5e"
1577        );
1578        assert_eq!(
1579            &std::fs::read(store.blob_dir.join(&fname1)?).unwrap()[..],
1580            b"Hello world"
1581        );
1582        assert_eq!(
1583            &std::fs::read(store.blob_dir.join(&fname2)?).unwrap()[..],
1584            b"Goodbye, dear friends"
1585        );
1586
1587        let n: u32 = store
1588            .conn
1589            .query_row("SELECT COUNT(filename) FROM ExtDocs", [], |row| row.get(0))?;
1590        assert_eq!(n, 2);
1591
1592        let blob = store.read_blob(&fname2)?.unwrap();
1593        assert_eq!(blob.as_str().unwrap(), "Goodbye, dear friends");
1594
1595        // Now expire: the second file should go away.
1596        store.expire_all(&EXPIRATION_DEFAULTS)?;
1597        assert_eq!(
1598            &std::fs::read(store.blob_dir.join(&fname1)?).unwrap()[..],
1599            b"Hello world"
1600        );
1601        assert!(std::fs::read(store.blob_dir.join(&fname2)?).is_err());
1602        let n: u32 = store
1603            .conn
1604            .query_row("SELECT COUNT(filename) FROM ExtDocs", [], |row| row.get(0))?;
1605        assert_eq!(n, 1);
1606
1607        Ok(())
1608    }
1609
1610    #[test]
1611    fn consensus() -> Result<()> {
1612        use tor_netdoc::doc::netstatus;
1613
1614        let (_tmp_dir, mut store) = new_empty()?;
1615        let now = now_utc();
1616        let one_hour = 1.hours();
1617
1618        assert_eq!(
1619            store.latest_consensus_time(ConsensusFlavor::Microdesc)?,
1620            None
1621        );
1622
1623        let cmeta = ConsensusMeta::new(
1624            netstatus::Lifetime::new(
1625                now.into(),
1626                (now + one_hour).into(),
1627                SystemTime::from(now + one_hour * 2),
1628            )
1629            .unwrap(),
1630            [0xAB; 32],
1631            [0xBC; 32],
1632        );
1633
1634        store.store_consensus(
1635            &cmeta,
1636            ConsensusFlavor::Microdesc,
1637            true,
1638            "Pretend this is a consensus",
1639        )?;
1640
1641        {
1642            assert_eq!(
1643                store.latest_consensus_time(ConsensusFlavor::Microdesc)?,
1644                None
1645            );
1646            let consensus = store
1647                .latest_consensus(ConsensusFlavor::Microdesc, None)?
1648                .unwrap();
1649            assert_eq!(consensus.as_str()?, "Pretend this is a consensus");
1650            let consensus = store.latest_consensus(ConsensusFlavor::Microdesc, Some(false))?;
1651            assert!(consensus.is_none());
1652        }
1653
1654        store.mark_consensus_usable(&cmeta)?;
1655
1656        {
1657            assert_eq!(
1658                store.latest_consensus_time(ConsensusFlavor::Microdesc)?,
1659                now.into()
1660            );
1661            let consensus = store
1662                .latest_consensus(ConsensusFlavor::Microdesc, None)?
1663                .unwrap();
1664            assert_eq!(consensus.as_str()?, "Pretend this is a consensus");
1665            let consensus = store
1666                .latest_consensus(ConsensusFlavor::Microdesc, Some(false))?
1667                .unwrap();
1668            assert_eq!(consensus.as_str()?, "Pretend this is a consensus");
1669        }
1670
1671        {
1672            let consensus_text = store.consensus_by_meta(&cmeta)?;
1673            assert_eq!(consensus_text.as_str()?, "Pretend this is a consensus");
1674
1675            let (is, _cmeta2) = store
1676                .consensus_by_sha3_digest_of_signed_part(&[0xAB; 32])?
1677                .unwrap();
1678            assert_eq!(is.as_str()?, "Pretend this is a consensus");
1679
1680            let cmeta3 = ConsensusMeta::new(
1681                netstatus::Lifetime::new(
1682                    now.into(),
1683                    (now + one_hour).into(),
1684                    SystemTime::from(now + one_hour * 2),
1685                )
1686                .unwrap(),
1687                [0x99; 32],
1688                [0x99; 32],
1689            );
1690            assert!(store.consensus_by_meta(&cmeta3).is_err());
1691
1692            assert!(
1693                store
1694                    .consensus_by_sha3_digest_of_signed_part(&[0x99; 32])?
1695                    .is_none()
1696            );
1697        }
1698
1699        {
1700            assert!(
1701                store
1702                    .consensus_by_sha3_digest_of_signed_part(&[0xAB; 32])?
1703                    .is_some()
1704            );
1705            store.delete_consensus(&cmeta)?;
1706            assert!(
1707                store
1708                    .consensus_by_sha3_digest_of_signed_part(&[0xAB; 32])?
1709                    .is_none()
1710            );
1711        }
1712
1713        Ok(())
1714    }
1715
1716    #[test]
1717    fn authcerts() -> Result<()> {
1718        let (_tmp_dir, mut store) = new_empty()?;
1719        let now = now_utc();
1720        let one_hour = 1.hours();
1721
1722        let keyids = AuthCertKeyIds {
1723            id_fingerprint: [3; 20].into(),
1724            sk_fingerprint: [4; 20].into(),
1725        };
1726        let keyids2 = AuthCertKeyIds {
1727            id_fingerprint: [4; 20].into(),
1728            sk_fingerprint: [3; 20].into(),
1729        };
1730
1731        let m1 = AuthCertMeta::new(keyids, now.into(), SystemTime::from(now + one_hour * 24));
1732
1733        store.store_authcerts(&[(m1, "Pretend this is a cert")])?;
1734
1735        let certs = store.authcerts(&[keyids, keyids2])?;
1736        assert_eq!(certs.len(), 1);
1737        assert_eq!(certs.get(&keyids).unwrap(), "Pretend this is a cert");
1738
1739        Ok(())
1740    }
1741
1742    #[test]
1743    fn microdescs() -> Result<()> {
1744        let (_tmp_dir, mut store) = new_empty()?;
1745
1746        let now = now_utc();
1747        let one_day = 1.days();
1748
1749        let d1 = [5_u8; 32];
1750        let d2 = [7; 32];
1751        let d3 = [42; 32];
1752        let d4 = [99; 32];
1753
1754        let long_ago: OffsetDateTime = now - one_day * 100;
1755        store.store_microdescs(
1756            &[
1757                ("Fake micro 1", &d1),
1758                ("Fake micro 2", &d2),
1759                ("Fake micro 3", &d3),
1760            ],
1761            long_ago.into(),
1762        )?;
1763
1764        store.update_microdescs_listed(&[d2], now.into())?;
1765
1766        let mds = store.microdescs(&[d2, d3, d4])?;
1767        assert_eq!(mds.len(), 2);
1768        assert_eq!(mds.get(&d1), None);
1769        assert_eq!(mds.get(&d2).unwrap(), "Fake micro 2");
1770        assert_eq!(mds.get(&d3).unwrap(), "Fake micro 3");
1771        assert_eq!(mds.get(&d4), None);
1772
1773        // Now we'll expire.  that should drop everything but d2.
1774        store.expire_all(&EXPIRATION_DEFAULTS)?;
1775        let mds = store.microdescs(&[d2, d3, d4])?;
1776        assert_eq!(mds.len(), 1);
1777        assert_eq!(mds.get(&d2).unwrap(), "Fake micro 2");
1778
1779        Ok(())
1780    }
1781
1782    #[test]
1783    #[cfg(feature = "routerdesc")]
1784    fn routerdescs() -> Result<()> {
1785        let (_tmp_dir, mut store) = new_empty()?;
1786
1787        let now = now_utc();
1788        let one_day = 1.days();
1789        let long_ago: OffsetDateTime = now - one_day * 100;
1790        let recently = now - one_day;
1791
1792        let d1 = [5_u8; 20];
1793        let d2 = [7; 20];
1794        let d3 = [42; 20];
1795        let d4 = [99; 20];
1796
1797        store.store_routerdescs(&[
1798            ("Fake routerdesc 1", long_ago.into(), &d1),
1799            ("Fake routerdesc 2", recently.into(), &d2),
1800            ("Fake routerdesc 3", long_ago.into(), &d3),
1801        ])?;
1802
1803        let rds = store.routerdescs(&[d2, d3, d4])?;
1804        assert_eq!(rds.len(), 2);
1805        assert_eq!(rds.get(&d1), None);
1806        assert_eq!(rds.get(&d2).unwrap(), "Fake routerdesc 2");
1807        assert_eq!(rds.get(&d3).unwrap(), "Fake routerdesc 3");
1808        assert_eq!(rds.get(&d4), None);
1809
1810        // Now we'll expire.  that should drop everything but d2.
1811        store.expire_all(&EXPIRATION_DEFAULTS)?;
1812        let rds = store.routerdescs(&[d2, d3, d4])?;
1813        assert_eq!(rds.len(), 1);
1814        assert_eq!(rds.get(&d2).unwrap(), "Fake routerdesc 2");
1815
1816        Ok(())
1817    }
1818
1819    #[test]
1820    fn from_path_rw() -> Result<()> {
1821        let tmp = tempdir().unwrap();
1822        let mistrust = fs_mistrust::Mistrust::new_dangerously_trust_everyone();
1823
1824        // Nothing there: can't open read-only
1825        let r = SqliteStore::from_path_and_mistrust(tmp.path(), &mistrust, true);
1826        assert!(r.is_err());
1827        assert!(!tmp.path().join("dir_blobs").try_exists().unwrap());
1828
1829        // Opening it read-write will crate the files
1830        {
1831            let mut store = SqliteStore::from_path_and_mistrust(tmp.path(), &mistrust, false)?;
1832            assert!(tmp.path().join("dir_blobs").is_dir());
1833            assert!(matches!(&store.lockfile, LockFile::Locked(_)));
1834            assert!(!store.is_readonly());
1835            assert!(store.upgrade_to_readwrite()?); // no-op.
1836        }
1837
1838        // At this point, we can successfully make a read-only connection.
1839        {
1840            let mut store2 = SqliteStore::from_path_and_mistrust(tmp.path(), &mistrust, true)?;
1841            assert!(store2.is_readonly());
1842
1843            // Nobody else is locking this, so we can upgrade.
1844            assert!(store2.upgrade_to_readwrite()?); // no-op.
1845            assert!(!store2.is_readonly());
1846        }
1847        Ok(())
1848    }
1849
1850    #[test]
1851    fn orphaned_blobs() -> Result<()> {
1852        let (_tmp_dir, mut store) = new_empty()?;
1853        /*
1854        for ent in store.blob_dir.read_directory(".")?.flatten() {
1855            println!("{:?}", ent);
1856        }
1857        */
1858        assert_eq!(store.blob_dir.read_directory(".")?.count(), 0);
1859
1860        let now = now_utc();
1861        let one_week = 1.weeks();
1862        let _fname_good = store.save_blob(
1863            b"Goodbye, dear friends",
1864            "greeting",
1865            "sha1",
1866            &hex!("2149c2a7dbf5be2bb36fb3c5080d0fb14cb3355c"),
1867            now + one_week,
1868        )?;
1869        assert_eq!(store.blob_dir.read_directory(".")?.count(), 1);
1870
1871        // Now, create a two orphaned blobs: one with a recent timestamp, and one with an older
1872        // timestamp.
1873        store
1874            .blob_dir
1875            .write_and_replace("fairly_new", b"new contents will stay")?;
1876        store
1877            .blob_dir
1878            .write_and_replace("fairly_old", b"old contents will be removed")?;
1879        filetime::set_file_mtime(
1880            store.blob_dir.join("fairly_old")?,
1881            SystemTime::from(now - one_week).into(),
1882        )
1883        .expect("Can't adjust mtime");
1884
1885        assert_eq!(store.blob_dir.read_directory(".")?.count(), 3);
1886
1887        store.remove_unreferenced_blobs(now, &EXPIRATION_DEFAULTS)?;
1888        assert_eq!(store.blob_dir.read_directory(".")?.count(), 2);
1889
1890        Ok(())
1891    }
1892
1893    #[test]
1894    fn unreferenced_consensus_blob() -> Result<()> {
1895        let (_tmp_dir, mut store) = new_empty()?;
1896
1897        let now = now_utc();
1898        let one_week = 1.weeks();
1899
1900        // Make a blob that claims to be a consensus, and which has not yet expired, but which is
1901        // not listed in the consensus table.  It should get removed.
1902        let fname = store.save_blob(
1903            b"pretend this is a consensus",
1904            "con_fake",
1905            "sha1",
1906            &hex!("803e5a45eea7766a62a735e051a25a50ffb9b1cf"),
1907            now + one_week,
1908        )?;
1909
1910        assert_eq!(store.blob_dir.read_directory(".")?.count(), 1);
1911        assert_eq!(
1912            &std::fs::read(store.blob_dir.join(&fname)?).unwrap()[..],
1913            b"pretend this is a consensus"
1914        );
1915        let n: u32 = store
1916            .conn
1917            .query_row("SELECT COUNT(filename) FROM ExtDocs", [], |row| row.get(0))?;
1918        assert_eq!(n, 1);
1919
1920        store.expire_all(&EXPIRATION_DEFAULTS)?;
1921        assert_eq!(store.blob_dir.read_directory(".")?.count(), 0);
1922
1923        let n: u32 = store
1924            .conn
1925            .query_row("SELECT COUNT(filename) FROM ExtDocs", [], |row| row.get(0))?;
1926        assert_eq!(n, 0);
1927
1928        Ok(())
1929    }
1930
1931    #[test]
1932    fn vanished_blob_cleanup() -> Result<()> {
1933        let (_tmp_dir, mut store) = new_empty()?;
1934
1935        let now = now_utc();
1936        let one_week = 1.weeks();
1937
1938        // Make a few blobs.
1939        let mut fnames = vec![];
1940        for idx in 0..8 {
1941            let content = format!("Example {idx}");
1942            let digest = Sha3_256::digest(content.as_bytes());
1943            let fname = store.save_blob(
1944                content.as_bytes(),
1945                "blob",
1946                "sha3-256",
1947                digest.as_slice(),
1948                now + one_week,
1949            )?;
1950            fnames.push(fname);
1951        }
1952
1953        // Delete the odd-numbered blobs.
1954        store.blob_dir.remove_file(&fnames[1])?;
1955        store.blob_dir.remove_file(&fnames[3])?;
1956        store.blob_dir.remove_file(&fnames[5])?;
1957        store.blob_dir.remove_file(&fnames[7])?;
1958
1959        let n_removed = {
1960            let tx = store.conn.transaction()?;
1961            let n = SqliteStore::remove_entries_for_vanished_blobs(&store.blob_dir, &tx)?;
1962            tx.commit()?;
1963            n
1964        };
1965        assert_eq!(n_removed, 4);
1966
1967        // Make sure that it was the _odd-numbered_ ones that got deleted from the DB.
1968        let (n_1,): (u32,) =
1969            store
1970                .conn
1971                .query_row(COUNT_EXTDOC_BY_PATH, params![&fnames[1]], |row| {
1972                    row.try_into()
1973                })?;
1974        let (n_2,): (u32,) =
1975            store
1976                .conn
1977                .query_row(COUNT_EXTDOC_BY_PATH, params![&fnames[2]], |row| {
1978                    row.try_into()
1979                })?;
1980        assert_eq!(n_1, 0);
1981        assert_eq!(n_2, 1);
1982        Ok(())
1983    }
1984
1985    #[test]
1986    fn protocol_statuses() -> Result<()> {
1987        let (_tmp_dir, mut store) = new_empty()?;
1988
1989        let now = SystemTime::get();
1990        let hour = 1.hours();
1991
1992        let valid_after = now;
1993        let protocols = serde_json::from_str(
1994            r#"{
1995            "client":{
1996                "required":"Link=5 LinkAuth=3",
1997                "recommended":"Link=1-5 LinkAuth=2-5"
1998            },
1999            "relay":{
2000                "required":"Wombat=20-22 Knish=25-27",
2001                "recommended":"Wombat=20-30 Knish=20-30"
2002            }
2003            }"#,
2004        )
2005        .unwrap();
2006
2007        let v = store.cached_protocol_recommendations()?;
2008        assert!(v.is_none());
2009
2010        store.update_protocol_recommendations(valid_after, &protocols)?;
2011        let v = store.cached_protocol_recommendations()?.unwrap();
2012        assert_eq!(v.0, now);
2013        assert_eq!(
2014            serde_json::to_string(&protocols).unwrap(),
2015            serde_json::to_string(&v.1).unwrap()
2016        );
2017
2018        let protocols2 = serde_json::from_str(
2019            r#"{
2020            "client":{
2021                "required":"Link=5 ",
2022                "recommended":"Link=1-5"
2023            },
2024            "relay":{
2025                "required":"Wombat=20",
2026                "recommended":"Cons=6"
2027            }
2028            }"#,
2029        )
2030        .unwrap();
2031
2032        let valid_after_2 = now + hour;
2033        store.update_protocol_recommendations(valid_after_2, &protocols2)?;
2034
2035        let v = store.cached_protocol_recommendations()?.unwrap();
2036        assert_eq!(v.0, now + hour);
2037        assert_eq!(
2038            serde_json::to_string(&protocols2).unwrap(),
2039            serde_json::to_string(&v.1).unwrap()
2040        );
2041
2042        Ok(())
2043    }
2044}