1
//! Access to the database schema.
2
//!
3
//! This module is not intended to provide a high-level ORM, instead it serves
4
//! the purpose of initializing and upgrading the database, if necessary.
5
//!
6
//! # Synchronous or Asynchronous?
7
//!
8
//! The question on whether the database and access to it shall be synchronous
9
//! or asynchronous has been fairly long debate that eventually got settled
10
//! after realizing that an asynchronous approach does not work.  This comment
11
//! should serve as a reminder for future devs, wondering why we use certain
12
//! synchronous primitives in an otherwise asynchronous codebase.
13
//!
14
//! Early on, it was clear that we would need some sort of connection pool,
15
//! primarily for two reasons:
16
//! 1. Performing frequent open and close calls in every task would be costly.
17
//! 2. Sharing a single connection object with a Mutex would be a waste
18
//!
19
//! Because the application itself is primarily asynchronous, we decided to go
20
//! with an asynchronous connection pool as well, leading to the choose of
21
//! `deadpool` initially.
22
//!
23
//! However, soon thereafter, problems with `deadpool` became evident.  Those
24
//! problems mostly stemmed from the synchronous nature of SQLite itself.  In our
25
//! case, this problem was initially triggered by figuring out a way to solve
26
//! `SQLITE_BUSY` handling.  In the end, we decided to settle upon the following
27
//! approach: Set `PRAGMA busy_timeout` to a certain value and create write
28
//! transactions with `BEGIN EXCLUSIVE`.  This way, SQLite would try to obtain
29
//! a write transaction for `busy_timeout` milliseconds by blocking the current
30
//! thread.  Due to this blocking, async no longer made any sense and was in
31
//! fact quite counter-productive because those potential sleep could screw a
32
//! lot of things up, which became very evident while trying to test this.
33
//!
34
//! Besides, throughout refactoring the code base, we realized that, even while
35
//! still using `deadpool`, the actual "asynchronous" calls interfacing with the
36
//! database became smaller and smaller.  In the end, the asynchronous code just
37
//! involved parts of obtaining a connection and creating a transaction,
38
//! eventually resulting in a calling a synchronous function taking the
39
//! transaction handle to perform the lion's share of the operation.
40

            
41
// TODO DIRMIRROR: This could benefit from methods by wrapping the pool into a
42
// custom type.
43

            
44
use std::{
45
    collections::HashSet,
46
    fmt::Display,
47
    io::{Cursor, Write},
48
    marker::PhantomData,
49
    num::NonZero,
50
    ops::{Add, Sub},
51
    path::Path,
52
    time::{Duration, SystemTime},
53
};
54

            
55
use digest::Digest;
56
use educe::Educe;
57
use flate2::write::{DeflateEncoder, GzEncoder};
58
use r2d2::Pool;
59
use r2d2_sqlite::SqliteConnectionManager;
60
use rand::Rng;
61
use rusqlite::{
62
    OptionalExtension, ToSql, Transaction, TransactionBehavior, named_params, params,
63
    types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef},
64
};
65
use saturating_time::SaturatingTime;
66
use tor_basic_utils::RngExt;
67
use tor_dircommon::config::DirTolerance;
68
use tor_error::into_internal;
69
use tor_netdoc::doc::{
70
    authcert::{AuthCert, AuthCertKeyIds},
71
    netstatus::ConsensusFlavor,
72
};
73

            
74
use crate::{err::DatabaseError, types::FlavoredConsensusUnverified};
75

            
76
/// Version 1 of the database schema.
77
///
78
/// TODO DIRMIRROR: Before the release, figure out where to use rowid and where
79
/// to use docid.
80
const V1_SCHEMA: &str = include_str!("schema_v1.sql");
81

            
82
/// Global options set in every connection.
83
const GLOBAL_OPTIONS: &str = sql!(
84
    "
85
PRAGMA journal_mode=WAL;
86
PRAGMA foreign_keys=ON;
87
PRAGMA busy_timeout=1000;
88
"
89
);
90

            
91
/// Convenience macro for implementing a hash type in a rusqlite compatible fashion.
92
///
93
/// This macro accepts the following parameters:
94
/// 1. `name` for specifying an identifier of the type, such as [`Sha256`].
95
/// 2. `algo` for specifying the type from the rust-crypto [`digest`] ecosystem,
96
///    such as [`tor_llcrypto::d::Sha256`].
97
/// 3. The size in bytes of the hash output, such as `32` for [`Sha256`].
98
///     * Unfortunately, we cannot use something like [`Digest::output_size()`]
99
///       because it is not a constant.
100
///
101
/// It generates a struct with `name` as the identifier, which implements the
102
/// following methods:
103
/// * `digest` for wrapping around [`Digest::digest()`].
104
///
105
/// It also implements the following traits:
106
/// * [`Display`]
107
/// * [`FromSql`]
108
/// * [`ToSql`]
109
/// * [`PartialEq<&str>`] for base16 comparisons
110
/// * [`From<u8; $size>`]
111
macro_rules! impl_hash_wrapper {
112
    ($name:ident, $algo:ty, $size:literal) => {
113
        /// Database wrapper type for $name.
114
        ///
115
        /// Serves as a database friendly wrapper around [`tor_llcrypto::d`]
116
        /// with features such as SQL support.
117
        #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
118
        pub(crate) struct $name([u8; $size]);
119

            
120
        impl $name {
121
            /// Computes the hash from arbitrary data.
122
914
            pub(crate) fn digest(data: &[u8]) -> Self {
123
914
                Self(<$algo>::digest(data).into())
124
914
            }
125
        }
126

            
127
        impl Display for $name {
128
            /// Formats the hash in uppercase hexadecimal.
129
3012
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130
3012
                write!(f, "{}", hex::encode_upper(self.0))
131
3012
            }
132
        }
133

            
134
        impl FromSql for $name {
135
138
            fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
136
                // We read the hash as a hexadecimal string from the database.
137
                // Convert it to binary data and check length afterwards.
138
138
                let data: [u8; $size] = value
139
138
                    .as_str()
140
138
                    .map(hex::decode)?
141
138
                    .map_err(|e| {
142
                        FromSqlError::Other(Box::new(tor_error::internal!(
143
                            "non hex data in database? {e}"
144
                        )))
145
                    })?
146
138
                    .try_into()
147
138
                    .map_err(|_| {
148
                        FromSqlError::Other(Box::new(tor_error::internal!(
149
                            "$name with invalid length in database?"
150
                        )))
151
                    })?;
152

            
153
138
                Ok(Self(data))
154
138
            }
155
        }
156

            
157
        impl ToSql for $name {
158
3010
            fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
159
                // Because Self is only constructed with FromSql and digest
160
                // data, it is safe to assume it is valid.
161
3010
                Ok(ToSqlOutput::from(self.to_string()))
162
3010
            }
163
        }
164

            
165
        impl PartialEq<&str> for $name {
166
2
            fn eq(&self, other: &&str) -> bool {
167
2
                self.to_string() == other.to_uppercase()
168
2
            }
169
        }
170

            
171
        impl From<[u8; $size]> for $name {
172
1246
            fn from(value: [u8; $size]) -> Self {
173
1246
                Self(value)
174
1246
            }
175
        }
176
    };
177
}
178

            
179
impl_hash_wrapper!(Sha1, tor_llcrypto::d::Sha1, 20);
180
impl_hash_wrapper!(Sha256, tor_llcrypto::d::Sha256, 32);
181
impl_hash_wrapper!(Sha3_256, tor_llcrypto::d::Sha3_256, 32);
182

            
183
/// The identifier for documents in the content-addressable cache.
184
///
185
/// Right now, this is a [`Sha256`] hash, but this may change in future.
186
pub(crate) type DocumentId = Sha256;
187

            
188
/// The supported content encodings.
189
#[derive(Debug, Clone, Copy, PartialEq, strum::EnumString, strum::Display, strum::EnumIter)]
190
#[strum(serialize_all = "kebab-case", ascii_case_insensitive)]
191
pub(crate) enum ContentEncoding {
192
    /// RFC2616 section 3.5.
193
    Identity,
194
    /// RFC2616 section 3.5.
195
    Deflate,
196
    /// RFC2616 section 3.5.
197
    Gzip,
198
    /// The zstandard compression algorithm (www.zstd.net).
199
    XZstd,
200
    /// The lzma compression algorithm with a "present" value no higher than 6.
201
    XTorLzma,
202
}
203

            
204
/// A wrapper around [`SystemTime`] with convenient features.
205
///
206
/// Please use this type throughout the crate internally, instead of
207
/// [`SystemTime`].
208
///
209
/// # Conversion
210
///
211
/// This type can be safely converted from and into a [`SystemTime`], because
212
/// it is just a wrapper type.
213
///
214
/// # Saturating Arithmetic
215
///
216
/// This type implements [`Add`] and [`Sub`] for [`Duration`] and [`Timestamp`]
217
/// ([`Sub`] only) using saturating arithmetic from the [`saturating_time`]
218
/// crate.  It means that addition and subtraction can be safely performed
219
/// without the potential risk of an unexpected panic, instead wrapping to
220
/// a local maximum/minimum or [`Duration::ZERO`] depending on the type.
221
///
222
/// Note that we don't provide a saturating version of [`Duration`], so addition
223
/// or subtraction of two [`Duration`]s still needs care to avoid panics.
224
///
225
/// # SQLite Interaction
226
///
227
/// This type implements [`FromSql`] and [`ToSql`], making it convenient to
228
/// integrate into SQL statements, as the database schema represents timestamps
229
/// internally using a non-negative [`i64`] storing the seconds since the epoch.
230
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
231
pub(crate) struct Timestamp(SystemTime);
232

            
233
impl From<SystemTime> for Timestamp {
234
40306
    fn from(value: SystemTime) -> Self {
235
40306
        Self(value)
236
40306
    }
237
}
238

            
239
impl From<Timestamp> for SystemTime {
240
8
    fn from(value: Timestamp) -> Self {
241
8
        value.0
242
8
    }
243
}
244

            
245
impl Add<Duration> for Timestamp {
246
    type Output = Self;
247

            
248
    /// Performs a saturating addition wrapping to [`SystemTime::max_value()`].
249
20004
    fn add(self, rhs: Duration) -> Self::Output {
250
20004
        Self(self.0.saturating_add(rhs))
251
20004
    }
252
}
253

            
254
impl Sub<Duration> for Timestamp {
255
    type Output = Self;
256

            
257
    /// Performs a saturating subtraction wrapping to [`SystemTime::min_value()`].
258
    fn sub(self, rhs: Duration) -> Self::Output {
259
        Self(self.0.saturating_sub(rhs))
260
    }
261
}
262

            
263
impl Sub<Timestamp> for Timestamp {
264
    type Output = Duration;
265

            
266
    /// Performs a saturating duration_since wrapping to [`Duration::ZERO`].
267
20004
    fn sub(self, rhs: Timestamp) -> Self::Output {
268
        #[allow(unstable_name_collisions)]
269
20004
        self.0.saturating_duration_since(rhs.0)
270
20004
    }
271
}
272

            
273
impl FromSql for Timestamp {
274
96
    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
275
96
        let mut res = SystemTime::UNIX_EPOCH;
276
96
        res = res.saturating_add(Duration::from_secs(value.as_i64()?.try_into().unwrap_or(0)));
277
96
        Ok(Self(res))
278
96
    }
279
}
280

            
281
impl ToSql for Timestamp {
282
306
    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
283
        #[allow(unstable_name_collisions)]
284
306
        Ok(ToSqlOutput::from(
285
306
            self.0
286
306
                .saturating_duration_since(SystemTime::UNIX_EPOCH)
287
306
                .as_secs()
288
306
                .try_into()
289
306
                .unwrap_or(i64::MAX),
290
306
        ))
291
306
    }
292
}
293

            
294
/// Representation of consensus metadata from the database.
295
#[derive(Educe)]
296
#[educe(Debug, Clone, Copy, PartialEq, Eq)]
297
pub(crate) struct ConsensusMeta<T> {
298
    /// The document id uniquely identifying the consensus.
299
    pub docid: DocumentId,
300

            
301
    /// The SHA3 of the unsigned part of the consensus.
302
    pub unsigned_sha3_256: Sha3_256,
303

            
304
    /// The time after which this consensus is valid.
305
    pub valid_after: Timestamp,
306

            
307
    /// The time after which this consensus stops being fresh.
308
    pub fresh_until: Timestamp,
309

            
310
    /// The time after which this consensus stops being valid.
311
    pub valid_until: Timestamp,
312

            
313
    /// The flavor of the consensus; determined at compile time.
314
    flavor: PhantomData<T>,
315
}
316

            
317
impl<T: FlavoredConsensusUnverified> ConsensusMeta<T> {
318
    /// Obtains the (valid) consensuses from the database.
319
    ///
320
    /// This function queries the database using a [`Transaction`] in order to
321
    /// have a consistent view upon it.  It will return an [`Option`] containing
322
    /// a consensus.  In order to obtain a *valid* consensus, a [`Timestamp`]
323
    /// plus a [`DirTolerance`] are supplied, which will be used for querying
324
    /// the database in a time-constrained fashion.
325
    ///
326
    /// Supplying [`None`] as the [`Timestamp`] simply returns the consensus
327
    /// with the highest valid-after value, regardless of the current system
328
    /// time.
329
30
    pub(crate) fn query(
330
30
        tx: &Transaction,
331
30
        tolerance: &DirTolerance,
332
30
        now: Option<Timestamp>,
333
30
    ) -> Result<Vec<Self>, DatabaseError> {
334
        // Select the most recent flavored consensus document from the database.
335
        //
336
        // The `valid_after` and `valid_until` cells must be a member of the range:
337
        // `[valid_after - pre_valid_tolerance; valid_after + post_valid_tolerance]`
338
        // (inclusively).
339
30
        let mut meta_stmt = tx.prepare_cached(sql!(
340
30
            "
341
30
            SELECT docid, unsigned_sha3_256, valid_after, fresh_until, valid_until
342
30
            FROM consensus
343
30
            WHERE
344
30
              flavor = :flavor
345
30
              AND
346
30
              (
347
30
                (:now IS NULL)
348
30
                OR
349
30
                (:now >= valid_after - :pre_valid AND :now <= valid_until + :post_valid)
350
30
              )
351
30
            ORDER BY valid_after DESC
352
30
            "
353
30
        ))?;
354

            
355
        // Actually execute the query.
356
30
        let rows = meta_stmt.query_map(named_params! {
357
30
            ":flavor": T::flavor().name(),
358
30
            ":now": now,
359
30
            ":pre_valid": tolerance.pre_valid_tolerance().as_secs().try_into().unwrap_or(i64::MAX),
360
30
            ":post_valid": tolerance.post_valid_tolerance().as_secs().try_into().unwrap_or(i64::MAX),
361
20
        }, |row| {
362
            Ok(Self {
363
20
                docid: row.get(0)?,
364
20
                unsigned_sha3_256: row.get(1)?,
365
20
                valid_after: row.get(2)?,
366
20
                fresh_until: row.get(3)?,
367
20
                valid_until: row.get(4)?,
368
20
                flavor: Default::default(),
369
            })
370
20
        })?;
371

            
372
30
        Ok(rows.collect::<Result<Vec<_>, _>>()?)
373
30
    }
374

            
375
    /// Queries the raw data of a [`ConsensusMeta`].
376
2
    pub(crate) fn data(&self, tx: &Transaction<'_>) -> Result<String, DatabaseError> {
377
2
        let mut stmt = tx.prepare_cached(sql!(
378
2
            "
379
2
            SELECT content
380
2
            FROM store
381
2
            WHERE docid = :docid
382
2
            "
383
2
        ))?;
384

            
385
2
        let raw = stmt.query_one(named_params! {":docid": self.docid}, |row| {
386
2
            row.get::<_, Vec<u8>>(0)
387
2
        })?;
388
2
        let raw = String::from_utf8(raw).map_err(into_internal!("utf-8 constraint violated?"))?;
389
2
        Ok(raw)
390
2
    }
391

            
392
    /// Calculates the [`Timestamp`] at which the authorities will be queried again.
393
    ///
394
    /// # Specifications
395
    ///
396
    /// * <https://spec.torproject.org/dir-spec/directory-cache-operation.html#download-ns-from-auth>
397
20002
    pub(crate) fn lifetime<R: Rng>(&self, rng: &mut R) -> Timestamp {
398
20002
        assert!(self.fresh_until < self.valid_until);
399

            
400
20002
        let offset = rng
401
20002
            .gen_range_checked(0..=((self.valid_until - self.fresh_until).as_secs() / 2))
402
20002
            .expect("invalid range?");
403

            
404
20002
        self.fresh_until + Duration::from_secs(offset)
405
20002
    }
406

            
407
    /// Returns the missing server descriptors for this consensus.
408
6
    pub(crate) fn missing_servers(
409
6
        &self,
410
6
        tx: &Transaction<'_>,
411
6
    ) -> Result<HashSet<Sha1>, DatabaseError> {
412
6
        if T::flavor() != ConsensusFlavor::Plain {
413
            return Ok(HashSet::new());
414
6
        }
415

            
416
        // Select the missing router descriptors.
417
        //
418
        // A router descriptor is considered missing if it exists in
419
        // `consensus_router_descriptor_member` but not in `router_descriptor`
420
        // because the first entry is added once the consensus got parsed,
421
        // whereas the second entry is added once we have actually retrieved it.
422
        //
423
        // It works by doing a left join on router_descriptor and filtering for
424
        // all entries where the join is NULL, as that implies we are aware of
425
        // the descriptor but not have it stored.
426
        //
427
        // Parameters:
428
        // :docid - The docid of the consensus.
429
6
        let mut stmt = tx.prepare_cached(sql!(
430
6
            "
431
6
            SELECT cr.unsigned_sha1
432
6
            FROM consensus_router_descriptor_member AS cr
433
6
              LEFT JOIN router_descriptor AS server ON cr.unsigned_sha1 = server.unsigned_sha1
434
6
            WHERE
435
6
              cr.consensus_docid = :docid
436
6
              AND cr.unsigned_sha1 IS NOT NULL
437
6
              AND server.unsigned_sha1 IS NULL
438
6
            "
439
6
        ))?;
440

            
441
6
        let missing = stmt
442
18
            .query_map(named_params! {":docid": self.docid}, |row| row.get(0))?
443
6
            .collect::<Result<HashSet<_>, _>>()?;
444
6
        Ok(missing)
445
6
    }
446

            
447
    /// Returns the missing extra infos for this consensus to the best of our abilities.
448
    ///
449
    /// Keep in mind that this does not return **all** missing extra infos but
450
    /// only the missing extra infos of server descriptors we have.
451
4
    pub(crate) fn missing_extras(
452
4
        &self,
453
4
        tx: &Transaction<'_>,
454
4
    ) -> Result<HashSet<Sha1>, DatabaseError> {
455
4
        if T::flavor() != ConsensusFlavor::Plain {
456
            return Ok(HashSet::new());
457
4
        }
458

            
459
        // Select the missing extra infos for this consensus.
460
        //
461
        // This return value is not complete because we only know the missing
462
        // extra-infos to the best of our abilities.  In other words: We are
463
        // only aware of a missing extra-info if we have parsed the respective
464
        // server descriptor.
465
        //
466
        // It works by doing an inner join from
467
        // `consensus_router_descriptor_member` to `router_descriptor` because
468
        // we can only know about the extra-infos of which we have the server
469
        // descriptors from.  Afterwards, we do a left join with the
470
        // `router_extra_info` table and filter for all results where the left
471
        // join result is null, hence where we have a server descriptor but not
472
        // the respective extra-info.
473
        //
474
        // Parameters:
475
        // :docid - The docid of the consensus.
476
4
        let mut stmt = tx.prepare_cached(sql!(
477
4
            "
478
4
            SELECT server.extra_unsigned_sha1
479
4
            FROM consensus_router_descriptor_member AS cr
480
4
              INNER JOIN router_descriptor AS server ON cr.unsigned_sha1 = server.unsigned_sha1
481
4
              LEFT JOIN router_extra_info AS extra ON server.extra_unsigned_sha1 = extra.unsigned_sha1
482
4
            WHERE
483
4
              cr.consensus_docid = :docid
484
4
              AND server.extra_unsigned_sha1 IS NOT NULL
485
4
              AND extra.unsigned_sha1 IS NULL
486
4
            "
487
4
        ))?;
488

            
489
4
        let missing = stmt
490
4
            .query_map(named_params! {":docid": self.docid}, |row| row.get(0))?
491
4
            .collect::<Result<HashSet<_>, _>>()?;
492
4
        Ok(missing)
493
4
    }
494

            
495
    /// Returns the missing micro descriptors for this consensus.
496
6
    pub(crate) fn missing_micros(
497
6
        &self,
498
6
        tx: &Transaction<'_>,
499
6
    ) -> Result<HashSet<Sha256>, DatabaseError> {
500
6
        if T::flavor() != ConsensusFlavor::Microdesc {
501
2
            return Ok(HashSet::new());
502
4
        }
503

            
504
        // Select the missing micro descriptors.
505
        //
506
        // A micro descriptor is considered missing if it exists in
507
        // `consensus_router_descriptor_member` but not in `router_descriptor`
508
        // because the first entry is added once the consensus got parsed,
509
        // whereas the second entry is added once we have actually retrieved it.
510
        //
511
        // It works by doing a left join on router_descriptor and filtering for
512
        // all entries where the join is NULL, as that implies we are aware of
513
        // the descriptor but not have it stored.
514
        //
515
        // Parameters:
516
        // :docid - The docid of the consensus.
517
4
        let mut stmt = tx.prepare_cached(sql!(
518
4
            "
519
4
            SELECT cr.unsigned_sha2
520
4
            FROM consensus_router_descriptor_member AS cr
521
4
              LEFT JOIN router_descriptor AS micro ON cr.unsigned_sha2 = micro.unsigned_sha2
522
4
            WHERE
523
4
              cr.consensus_docid = :docid
524
4
              AND cr.unsigned_sha2 IS NOT NULL
525
4
              AND micro.unsigned_sha2 IS NULL
526
4
            "
527
4
        ))?;
528

            
529
4
        let missing = stmt
530
16
            .query_map(named_params! {":docid": self.docid}, |row| row.get(0))?
531
4
            .collect::<Result<HashSet<_>, _>>()?;
532
4
        Ok(missing)
533
6
    }
534
}
535

            
536
/// Representation of authority certificate metadata from the database.
537
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
538
pub(crate) struct AuthCertMeta {
539
    /// The document id uniquely identifying the consensus.
540
    pub docid: DocumentId,
541

            
542
    /// The SHA-1 fingerprint of the identity key.
543
    // TODO DIRMIRROR: Change this to RsaIdentity.
544
    pub kp_auth_id_rsa_sha1: Sha1,
545

            
546
    /// The SHA-1 fingerprint of the signign key.
547
    // TODO DIRMIRROR: Change this to RsaIdentity.
548
    pub kp_auth_sign_rsa_sha1: Sha1,
549

            
550
    /// The timestamp after which this certificate will be valid.
551
    pub dir_key_published: Timestamp,
552

            
553
    /// The timestamp until this certificate will be valid.
554
    pub dir_key_expires: Timestamp,
555
}
556

            
557
impl AuthCertMeta {
558
    /// Obtain the most recently published and valid certificate for each authority.
559
    ///
560
    /// Returns the found [`AuthCertMeta`] items as well as the missing
561
    /// [`AuthCertKeyIds`].
562
    ///
563
    /// # Performance
564
    ///
565
    /// This function has a performance between `O(n * log n)` and `O(n^2)`
566
    /// because it performs `signatories.len()` database queries, with each
567
    /// database query potentially taking something between `O(log n)` to
568
    /// `O(n)` to execute.  However, given that this respective value is
569
    /// oftentimes fairly small, it should not be much of a big concern.
570
12
    pub(crate) fn query(
571
12
        tx: &Transaction,
572
12
        signatories: &[AuthCertKeyIds],
573
12
        tolerance: &DirTolerance,
574
12
        now: Timestamp,
575
12
    ) -> Result<(Vec<Self>, Vec<AuthCertKeyIds>), DatabaseError> {
576
        // For every key pair in `signatories`, get the most recent valid cert.
577
        //
578
        // This query selects the most recent timestamp valid certificate from
579
        // the database for a single given key pair.  It means that this query
580
        // has to be executed as many times as there are entries in
581
        // `signatories`.
582
        //
583
        // Unfortunately, there is no neater way to do this, because the
584
        // alternative would involve using a nested set which SQLite does not
585
        // support, even with the carray extension.  An alternative might be to
586
        // precompute that string and then insert it here using `format!` but
587
        // that feels hacky, error- and injection-prone.
588
        //
589
        // Parameters:
590
        // :id_rsa: The RSA identity key fingerprint in uppercase hexadecimal.
591
        // :sk_rsa: The RSA signing key fingerprint in uppercase hexadecimal.
592
        // :now: The current system timestamp.
593
        // :pre_tolerance: The tolerance for not-yet-valid certificates.
594
        // :post_tolerance: The tolerance for expired certificates.
595
12
        let mut stmt = tx.prepare_cached(sql!(
596
12
            "
597
12
            SELECT docid, kp_auth_id_rsa_sha1, kp_auth_sign_rsa_sha1,
598
12
              dir_key_published, dir_key_expires
599
12
            FROM authority_key_certificate
600
12
            WHERE
601
12
              (:id_rsa, :sk_rsa) = (kp_auth_id_rsa_sha1, kp_auth_sign_rsa_sha1)
602
12
              AND :now >= dir_key_published - :pre_tolerance
603
12
              AND :now <= dir_key_expires + :post_tolerance
604
12
            ORDER BY dir_key_published DESC
605
12
            LIMIT 1
606
12
            "
607
12
        ))?;
608

            
609
        // Keep track of the found (and parsed) certificates and the missing ones.
610
12
        let mut found = Vec::new();
611
12
        let mut missing = Vec::new();
612

            
613
        // Iterate over every key pair and query it, adding it to found if it exists
614
        // and was parsed successfully or to missing if it does not exist within the
615
        // database.
616
38
        for kp in signatories {
617
            // Query the certificate from the database.
618
38
            let res = stmt
619
38
            .query_one(
620
38
                named_params! {
621
38
                    ":id_rsa": kp.id_fingerprint.as_hex_upper(),
622
38
                    ":sk_rsa": kp.sk_fingerprint.as_hex_upper(),
623
38
                    ":now": now,
624
38
                    ":pre_tolerance": tolerance.pre_valid_tolerance().as_secs().try_into().unwrap_or(i64::MAX),
625
38
                    ":post_tolerance": tolerance.post_valid_tolerance().as_secs().try_into().unwrap_or(i64::MAX),
626
                },
627
                |row| Ok(Self {
628
18
                    docid: row.get(0)?,
629
18
                    kp_auth_id_rsa_sha1: row.get(1)?,
630
18
                    kp_auth_sign_rsa_sha1: row.get(2)?,
631
18
                    dir_key_published: row.get(3)?,
632
18
                    dir_key_expires: row.get(4)?,
633
                })
634
            )
635
38
            .optional()?;
636

            
637
38
            match res {
638
18
                Some(cert) => found.push(cert),
639
20
                None => missing.push(*kp),
640
            }
641
        }
642

            
643
12
        Ok((found, missing))
644
12
    }
645

            
646
    /// Queries the raw data of an [`AuthCertMeta`].
647
    pub(crate) fn data(&self, tx: &Transaction<'_>) -> Result<String, DatabaseError> {
648
        let mut stmt = tx.prepare_cached(sql!(
649
            "
650
            SELECT content
651
            FROM store
652
            WHERE docid = :docid
653
            "
654
        ))?;
655

            
656
        let raw = stmt.query_one(named_params! {":docid": self.docid}, |row| {
657
            row.get::<_, Vec<u8>>(0)
658
        })?;
659
        let raw = String::from_utf8(raw).map_err(into_internal!("utf-8 constraint violated?"))?;
660
        Ok(raw)
661
    }
662

            
663
    /// Inserts a new authority certificate into the database.
664
    ///
665
    /// Keep in mind that the data in the [`AuthCert`] should correspond to the
666
    /// data found in `data`, as this method performs no parsing.
667
72
    pub(crate) fn insert<I: Iterator<Item = ContentEncoding>>(
668
72
        tx: &Transaction<'_>,
669
72
        encodings: I,
670
72
        cert: &AuthCert,
671
72
        data: &str,
672
72
    ) -> Result<(), DatabaseError> {
673
        // Inserts a new certificate into the meta table.
674
        //
675
        // Parameters:
676
        // :docid - The document id.
677
        // :id_rsa - The identity key fingerprint.
678
        // :sign_rsa - The signing key fingerprint
679
        // :published - The published timestamp.
680
        // :expires - The expires timestamp.
681
72
        let mut stmt = tx.prepare_cached(sql!(
682
72
            "
683
72
            INSERT INTO authority_key_certificate
684
72
            (docid, kp_auth_id_rsa_sha1, kp_auth_sign_rsa_sha1, dir_key_published, dir_key_expires)
685
72
            VALUES
686
72
            (:docid, :id_rsa, :sign_rsa, :published, :expires)
687
72
            ON CONFLICT DO NOTHING
688
72
            "
689
72
        ))?;
690

            
691
72
        let docid = store_insert(tx, data.as_bytes(), encodings)?;
692
72
        stmt.execute(named_params! {
693
72
            ":docid": docid,
694
72
            ":id_rsa": cert.dir_identity_key.to_rsa_identity().as_hex_upper(),
695
72
            ":sign_rsa": cert.dir_signing_key.to_rsa_identity().as_hex_upper(),
696
72
            ":published": Timestamp::from(cert.dir_key_published.0),
697
72
            ":expires": Timestamp::from(cert.dir_key_expires.0),
698
72
        })?;
699

            
700
72
        Ok(())
701
72
    }
702
}
703

            
704
/// A no-op macro just returning the supplied.
705
///
706
/// The purpose of this macro is to semantically mark [`str`] literals to be
707
/// SQL statement.
708
///
709
/// Keep in mind that the compiler will not notice if you forget this macro.
710
/// Unfortunately, you have to ensure it yourself.
711
macro_rules! sql {
712
    ($s:literal) => {
713
        $s
714
    };
715
}
716

            
717
pub(crate) use sql;
718

            
719
/// Opens a database from disk, creating a [`Pool`] for it.
720
///
721
/// This function should be the entry point for all things requiring a database
722
/// handle, as this function prepares all necessary steps required for operating
723
/// on the database correctly, such as:
724
/// * Schema initialization.
725
/// * Schema upgrade.
726
/// * Setting connection specific settings.
727
///
728
/// # `SQLITE_BUSY` Caveat
729
///
730
/// There is a problem with the handling of `SQLITE_BUSY` when opening an
731
/// SQLite database.  In WAL, opening a database might acquire an exclusive lock
732
/// for a very short amount of time, in order to perform clean-up from previous
733
/// connections alongside other tasks for maintaining database integrity?  This
734
/// means, that opening multiple SQLite databases simultaneously will result in
735
/// a busy error regardless of a busy handler, as setting a busy handler will
736
/// require an existing connection, something we are unable to obtain in the
737
/// first place.
738
///
739
/// In order to mitigate this issue, the recommended way in the SQLite community
740
/// is to simply ensure that database connections are opened sequentially,
741
/// by urging calling applications to just use a single [`Pool`] instance.
742
///
743
/// Testing this is hard unfortunately.
744
36
pub(crate) fn open<P: AsRef<Path>>(
745
36
    path: P,
746
36
) -> Result<Pool<SqliteConnectionManager>, DatabaseError> {
747
36
    let num_cores = std::thread::available_parallelism()
748
36
        .unwrap_or(NonZero::new(8).expect("8 == 0?"))
749
36
        .get() as u32;
750

            
751
36
    let manager = r2d2_sqlite::SqliteConnectionManager::file(&path);
752
36
    let pool = Pool::builder().max_size(num_cores).build(manager)?;
753

            
754
36
    rw_tx(&pool, |tx| {
755
        // Prepare the database, doing the following steps:
756
        // 1. Checking the database schema.
757
        // 2. Upgrading (in future) or initializing the database schema (if empty).
758

            
759
36
        let has_arti_dirserver_schema_version = match tx.query_one(
760
            sql!(
761
36
                "
762
36
                SELECT name
763
36
                FROM sqlite_master
764
36
                  WHERE type = 'table'
765
36
                    AND name = 'arti_dirserver_schema_version'
766
36
                "
767
            ),
768
36
            params![],
769
2
            |_| Ok(()),
770
        ) {
771
2
            Ok(()) => true,
772
34
            Err(rusqlite::Error::QueryReturnedNoRows) => false,
773
            Err(e) => return Err(DatabaseError::LowLevel(e)),
774
        };
775

            
776
36
        if has_arti_dirserver_schema_version {
777
2
            let version = tx.query_one(
778
2
                sql!("SELECT version FROM arti_dirserver_schema_version WHERE rowid = 1"),
779
2
                params![],
780
2
                |row| row.get::<_, String>(0),
781
            )?;
782

            
783
2
            match version.as_ref() {
784
2
                "1" => {}
785
2
                unknown => {
786
2
                    return Err(DatabaseError::IncompatibleSchema {
787
2
                        version: unknown.into(),
788
2
                    });
789
                }
790
            }
791
        } else {
792
34
            tx.execute_batch(V1_SCHEMA)?;
793
        }
794

            
795
34
        Ok::<_, DatabaseError>(())
796
36
    })??;
797

            
798
34
    Ok(pool)
799
36
}
800

            
801
/// Executes a closure `op` with a given read-only [`Transaction`].
802
///
803
/// The [`Transaction`] always gets rolled back the moment `op` returns.
804
///
805
/// The [`Transaction`] gets initialized with the global pragma options set.
806
///
807
/// **The closure shall not perform write operations!**
808
/// Not only do they get rolled back anyways, but upgrading the [`Transaction`]
809
/// from a read to a write transaction will lead to other simultaneous write upgrades
810
/// to fail.  Unfortunately, there is no real programmatic way to ensure this.
811
48
pub(crate) fn read_tx<U, F>(pool: &Pool<SqliteConnectionManager>, op: F) -> Result<U, DatabaseError>
812
48
where
813
48
    F: FnOnce(&Transaction<'_>) -> U,
814
{
815
48
    let mut conn = pool.get()?;
816
48
    conn.execute_batch(GLOBAL_OPTIONS)?;
817
48
    let tx = conn.transaction_with_behavior(TransactionBehavior::Deferred)?;
818
48
    let res = op(&tx);
819
48
    tx.rollback()?;
820
48
    Ok(res)
821
48
}
822

            
823
/// Executes a closure `op` with a given read-write [`Transaction`].
824
///
825
/// The [`Transaction`] always gets committed the moment `op` returns.
826
///
827
/// The [`Transaction`] gets initialized with the global pragma options set.
828
///
829
/// The [`Transaction`] gets created with [`TransactionBehavior::Immediate`],
830
/// meaning it will immediately exist as a write connection, retrying in the
831
/// case of a [`rusqlite::ErrorCode::DatabaseBusy`] until it failed after 1s.
832
58
pub(crate) fn rw_tx<U, F>(pool: &Pool<SqliteConnectionManager>, op: F) -> Result<U, DatabaseError>
833
58
where
834
58
    F: FnOnce(&Transaction<'_>) -> U,
835
{
836
58
    let mut conn = pool.get()?;
837
58
    conn.execute_batch(GLOBAL_OPTIONS)?;
838
58
    let tx = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?;
839
56
    let res = op(&tx);
840
56
    tx.commit()?;
841
56
    Ok(res)
842
58
}
843

            
844
/// Inserts `data` into store while also compressing it with given encodings.
845
///
846
/// Returns the [`DocumentId`] of `data`.
847
///
848
/// This function inserts `data` into store and also compresses it into all
849
/// given compression formats.
850
///
851
/// Duplicates get re-encoded and replaced in the database, including
852
/// [`ContentEncoding::Identity`].
853
542
pub(crate) fn store_insert<I: Iterator<Item = ContentEncoding>>(
854
542
    tx: &Transaction,
855
542
    data: &[u8],
856
542
    encodings: I,
857
542
) -> Result<DocumentId, DatabaseError> {
858
    // The statement to insert some data into the store.
859
    //
860
    // Parameters:
861
    // :docid - The docid.
862
    // :content - The binary data.
863
542
    let mut store_stmt = tx.prepare_cached(sql!(
864
542
        "
865
542
        INSERT INTO store (docid, content)
866
542
        VALUES
867
542
        (:docid, :content)
868
542
        ON CONFLICT DO NOTHING
869
542
        "
870
542
    ))?;
871

            
872
    // The statement to insert a compressed document into the metatable.
873
    //
874
    // Parameters:
875
    // :algorithm - The name of the encoding algorithm.
876
    // :identity_docid - The docid of the plain-text document in the store.
877
    // :compressed_docid - The docid of the encoded document in the store.
878
542
    let mut compressed_stmt = tx.prepare_cached(sql!(
879
542
        "
880
542
        INSERT INTO compressed_document (algorithm, identity_docid, compressed_docid)
881
542
        VALUES
882
542
        (:algorithm, :identity_docid, :compressed_docid)
883
542
        ON CONFLICT DO NOTHING
884
542
        "
885
542
    ))?;
886

            
887
    // Insert the plain document into the store.
888
542
    let identity_docid = DocumentId::digest(data);
889
542
    store_stmt.execute(named_params! {
890
542
        ":docid": identity_docid,
891
542
        ":content": data
892
542
    })?;
893

            
894
    // Compress it into all formats and insert it into store and compressed.
895
598
    for encoding in encodings {
896
598
        if encoding == ContentEncoding::Identity {
897
            // Ignore identity because we inserted that above.
898
542
            continue;
899
56
        }
900

            
901
        // We map a compression error to a bug because there is no good reason
902
        // on why it should fail, given that we compress from memory data to
903
        // memory data.  Probably because it uses the std::io::Writer interface
904
        // which itself demands use of std::io::Result.
905
56
        let compressed = compress(data, encoding).map_err(into_internal!("{encoding} failed?"))?;
906
56
        let compressed_docid = DocumentId::digest(&compressed);
907
56
        store_stmt.execute(named_params! {
908
56
            ":docid": compressed_docid,
909
56
            ":content": compressed,
910
56
        })?;
911
56
        compressed_stmt.execute(named_params! {
912
56
            ":algorithm": encoding.to_string(),
913
56
            ":identity_docid": identity_docid,
914
56
            ":compressed_docid": compressed_docid,
915
56
        })?;
916
    }
917

            
918
542
    Ok(identity_docid)
919
542
}
920

            
921
/// Compresses `data` into a specified [`ContentEncoding`].
922
///
923
/// Returns a [`Vec`] containing the encoded data.
924
66
fn compress(data: &[u8], encoding: ContentEncoding) -> Result<Vec<u8>, std::io::Error> {
925
66
    match encoding {
926
2
        ContentEncoding::Identity => Ok(data.to_vec()),
927
        ContentEncoding::Deflate => {
928
16
            let mut w = DeflateEncoder::new(Vec::new(), Default::default());
929
16
            w.write_all(data)?;
930
16
            w.finish()
931
        }
932
        ContentEncoding::Gzip => {
933
16
            let mut w = GzEncoder::new(Vec::new(), Default::default());
934
16
            w.write_all(data)?;
935
16
            w.finish()
936
        }
937
16
        ContentEncoding::XZstd => zstd::encode_all(data, Default::default()),
938
        ContentEncoding::XTorLzma => {
939
16
            let mut res = Vec::new();
940
16
            lzma_rs::lzma_compress(&mut Cursor::new(data), &mut res)?;
941
16
            Ok(res)
942
        }
943
    }
944
66
}
945

            
946
#[cfg(test)]
947
mod test {
948
    // @@ begin test lint list maintained by maint/add_warning @@
949
    #![allow(clippy::bool_assert_comparison)]
950
    #![allow(clippy::clone_on_copy)]
951
    #![allow(clippy::dbg_macro)]
952
    #![allow(clippy::mixed_attributes_style)]
953
    #![allow(clippy::print_stderr)]
954
    #![allow(clippy::print_stdout)]
955
    #![allow(clippy::single_char_pattern)]
956
    #![allow(clippy::unwrap_used)]
957
    #![allow(clippy::unchecked_time_subtraction)]
958
    #![allow(clippy::useless_vec)]
959
    #![allow(clippy::needless_pass_by_value)]
960
    #![allow(clippy::string_slice)] // See arti#2571
961
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
962
    use std::{
963
        collections::HashSet,
964
        io::Read,
965
        sync::{Arc, Once},
966
    };
967

            
968
    use flate2::read::{DeflateDecoder, GzDecoder};
969
    use rusqlite::Connection;
970
    use strum::IntoEnumIterator;
971
    use tempfile::tempdir;
972
    use tor_basic_utils::test_rng::testing_rng;
973
    use tor_dircommon::config::DirToleranceBuilder;
974
    use tor_llcrypto::pk::rsa::RsaIdentity;
975
    use tor_netdoc::doc::netstatus::{md, plain};
976

            
977
    use crate::testdata2;
978

            
979
    use super::*;
980

            
981
    type Plain = plain::NetworkStatusUnverified;
982
    type Md = md::NetworkStatusUnverified;
983

            
984
    #[test]
985
    fn open_test() {
986
        let db_dir = tempdir().unwrap();
987
        let db_path = db_dir.path().join("db");
988

            
989
        open(&db_path).unwrap();
990
        let conn = Connection::open(&db_path).unwrap();
991

            
992
        // Check if the version was initialized properly.
993
        let version = conn
994
            .query_one(
995
                "SELECT version FROM arti_dirserver_schema_version WHERE rowid = 1",
996
                params![],
997
                |row| row.get::<_, String>(0),
998
            )
999
            .unwrap();
        assert_eq!(version, "1");
        // Set the version to something unknown.
        conn.execute(
            "UPDATE arti_dirserver_schema_version SET version = 42",
            params![],
        )
        .unwrap();
        drop(conn);
        assert_eq!(
            open(&db_path).unwrap_err().to_string(),
            "incompatible schema version: 42"
        );
    }
    #[test]
    fn read_tx_test() {
        let db_dir = tempdir().unwrap();
        let db_path = db_dir.path().join("db");
        let pool = open(&db_path).unwrap();
        // Do a write transaction despite forbidden.
        read_tx(&pool, |tx| {
            tx.execute_batch("DELETE FROM arti_dirserver_schema_version")
                .unwrap();
            let e = tx
                .query_one(
                    sql!("SELECT version FROM arti_dirserver_schema_version"),
                    params![],
                    |row| row.get::<_, String>(0),
                )
                .unwrap_err();
            assert_eq!(e, rusqlite::Error::QueryReturnedNoRows);
        })
        .unwrap();
        // Normal check.
        let version: String = read_tx(&pool, |tx| {
            tx.query_one(
                sql!("SELECT version FROM arti_dirserver_schema_version"),
                params![],
                |row| row.get(0),
            )
            .unwrap()
        })
        .unwrap();
        assert_eq!(version, "1");
    }
    #[test]
    fn rw_tx_test() {
        let db_dir = tempdir().unwrap();
        let db_path = db_dir.path().join("db");
        let pool = open(&db_path).unwrap();
        // Do a write transaction.
        rw_tx(&pool, |tx| {
            tx.execute_batch("DELETE FROM arti_dirserver_schema_version")
                .unwrap();
        })
        .unwrap();
        // Check that it was deleted.
        read_tx(&pool, |tx| {
            let e = tx
                .query_one(
                    sql!("SELECT version FROM arti_dirserver_schema_version"),
                    params![],
                    |row| row.get::<_, String>(0),
                )
                .unwrap_err();
            assert_eq!(e, rusqlite::Error::QueryReturnedNoRows);
        })
        .unwrap();
    }
    /// Tests whether our SQLite busy error handling works in normal situations.
    ///
    /// A normal situations means a situation where a lock is never held for
    /// more than 1000ms.  In our case, we will work with two threads.
    /// t1 will acquire an exclusive lock and inform t2 about it.  t2 waits
    /// until t1 has acquired this lock and then immediately informs t1, that
    /// it will now wait for a lock too.  Now, t1 will immediately terminate,
    /// thereby releasing the lock and leading t2 to eventually acquire it.
    #[test]
    fn rw_tx_busy_timeout_working() {
        let db_dir = tempdir().unwrap();
        let db_path = db_dir.path().join("db");
        let pool = open(db_path).unwrap();
        // t2 will wait on this before it starts doing stuff.
        let t1_acquired_lock = Arc::new(Once::new());
        // t1 will wait on this in order to terminate properly.
        let t2_is_waiting = Arc::new(Once::new());
        let t1 = std::thread::spawn({
            let pool = pool.clone();
            let t1_acquired_lock = t1_acquired_lock.clone();
            let t2_is_waiting = t2_is_waiting.clone();
            move || {
                rw_tx(&pool, move |_tx| {
                    // Inform t2 we have write lock.
                    t1_acquired_lock.call_once(|| ());
                    println!("t1 acquired write lock");
                    // Wait for t2 to start waiting.
                    t2_is_waiting.wait();
                })
                .unwrap();
                println!("t2 released write lock");
            }
        });
        println!("t2 waits for t1 to acquire write lock");
        t1_acquired_lock.wait();
        t2_is_waiting.call_once(|| ());
        rw_tx(&pool, |_| ()).unwrap();
        println!("t2 acquired and released write lock");
        t1.join().unwrap();
    }
    /// Tests whether our SQLite busy error handlings fails as expected.
    ///
    /// We configure SQLite to fail after 1000ms.  This test works with two
    /// threads.  t1 will acquire an exclusive lock on the database and will
    /// inform t2 about it, which itself will wait until t1 has acquired the
    /// lock.  t2 will then immediately try to also obtain an exclusive lock,
    /// which should fail after about 1000ms.  After the failure, t2 informs
    /// t1 that it has failed, causing t1 to terminate.
    #[test]
    fn rw_tx_busy_timeout_busy() {
        let db_dir = tempdir().unwrap();
        let db_path = db_dir.path().join("db");
        let pool = open(db_path).unwrap();
        // t2 will wait on this before it starts doing stuff.
        let t1_acquired_lock = Arc::new(Once::new());
        // t1 will wait on this in order to terminate properly.
        let t2_gave_up = Arc::new(Once::new());
        let t1 = std::thread::spawn({
            let pool = pool.clone();
            let t1_acquired_lock = t1_acquired_lock.clone();
            let t2_gave_up = t2_gave_up.clone();
            move || {
                rw_tx(&pool, move |_tx| {
                    // Inform t2 we have the write lock.
                    t1_acquired_lock.call_once(|| ());
                    println!("t1 acquired write lock");
                    // Wait for t2 to give up before we release (how mean from us).
                    t2_gave_up.wait();
                })
                .unwrap();
                println!("t1 released write lock");
            }
        });
        println!("t2 waits for t1 to acquire write lock");
        t1_acquired_lock.wait();
        let e = rw_tx(&pool, |_| ()).unwrap_err();
        assert_eq!(
            e.to_string(),
            "low-level rusqlite error: database is locked"
        );
        println!("t2 gave up on acquiring write lock");
        t2_gave_up.call_once(|| ());
        t1.join().unwrap();
    }
    #[test]
    fn store_insert_test() {
        let db_dir = tempdir().unwrap();
        let db_path = db_dir.path().join("db");
        open(&db_path).unwrap();
        let mut conn = Connection::open(&db_path).unwrap();
        let tx = conn.transaction().unwrap();
        let docid = store_insert(&tx, "foobar".as_bytes(), ContentEncoding::iter()).unwrap();
        assert_eq!(
            docid,
            "C3AB8FF13720E8AD9047DD39466B3C8974E592C2FA383D4A3960714CAEF0C4F2"
        );
        let res = tx
            .query_one(
                sql!(
                    "
                    SELECT content
                    FROM store
                    WHERE docid = 'C3AB8FF13720E8AD9047DD39466B3C8974E592C2FA383D4A3960714CAEF0C4F2'
                    "
                ),
                params![],
                |row| row.get::<_, Vec<u8>>(0),
            )
            .unwrap();
        assert_eq!(res, "foobar".as_bytes());
        let mut stmt = tx.prepare_cached(sql!(
            "
            SELECT algorithm
            FROM compressed_document
            WHERE identity_docid = 'C3AB8FF13720E8AD9047DD39466B3C8974E592C2FA383D4A3960714CAEF0C4F2'
            "
        )).unwrap();
        let algorithms = stmt
            .query_map(params![], |row| row.get::<_, String>(0))
            .unwrap();
        let algorithms = algorithms.map(|x| x.unwrap()).collect::<HashSet<_>>();
        assert_eq!(
            algorithms,
            HashSet::from([
                "deflate".to_string(),
                "gzip".to_string(),
                "x-zstd".to_string(),
                "x-tor-lzma".to_string()
            ])
        );
        // Now insert the same thing a second time again and see whether the
        // ON CONFLICT magic works.
        let docid_second = store_insert(&tx, "foobar".as_bytes(), ContentEncoding::iter()).unwrap();
        assert_eq!(docid, docid_second);
        // Remove a few compressed entries and get them again.
        let n = tx
            .execute(
                sql!(
                    "
                    DELETE FROM
                    compressed_document
                    WHERE algorithm IN ('deflate', 'x-zstd')
                    "
                ),
                params![],
            )
            .unwrap();
        assert_eq!(n, 2);
        let docid_third = store_insert(&tx, "foobar".as_bytes(), ContentEncoding::iter()).unwrap();
        assert_eq!(docid, docid_third);
        let algorithms = stmt
            .query_map(params![], |row| row.get::<_, String>(0))
            .unwrap();
        let algorithms = algorithms.map(|x| x.unwrap()).collect::<HashSet<_>>();
        assert_eq!(
            algorithms,
            HashSet::from([
                "deflate".to_string(),
                "gzip".to_string(),
                "x-zstd".to_string(),
                "x-tor-lzma".to_string()
            ])
        );
    }
    #[test]
    fn compress_test() {
        /// Asserts that `res` contains `encoding`.
        fn contains(encoding: ContentEncoding, res: &[(ContentEncoding, Vec<u8>)]) {
            assert!(res.iter().any(|x| x.0 == encoding));
        }
        const INPUT: &[u8] = "foobar".as_bytes();
        // Check whether everything was encoded.
        let res = ContentEncoding::iter()
            .map(|encoding| (encoding, compress(INPUT, encoding).unwrap()))
            .collect::<Vec<_>>();
        assert_eq!(res.len(), 5);
        contains(ContentEncoding::Identity, &res);
        contains(ContentEncoding::Deflate, &res);
        contains(ContentEncoding::Gzip, &res);
        contains(ContentEncoding::XTorLzma, &res);
        contains(ContentEncoding::XZstd, &res);
        // Check if we can decode it.
        for (encoding, compressed) in res {
            let mut decompressed = Vec::new();
            match encoding {
                ContentEncoding::Identity => decompressed = compressed,
                ContentEncoding::Deflate => {
                    DeflateDecoder::new(Cursor::new(compressed))
                        .read_to_end(&mut decompressed)
                        .unwrap();
                }
                ContentEncoding::Gzip => {
                    GzDecoder::new(Cursor::new(compressed))
                        .read_to_end(&mut decompressed)
                        .unwrap();
                }
                ContentEncoding::XTorLzma => {
                    lzma_rs::lzma_decompress(&mut Cursor::new(compressed), &mut decompressed)
                        .unwrap();
                }
                ContentEncoding::XZstd => {
                    decompressed = zstd::decode_all(Cursor::new(compressed)).unwrap();
                }
            }
            assert_eq!(decompressed, INPUT);
        }
    }
    /// Tests whether consensuses are queried properly from the database given
    /// a pre-defined data.
    ///
    /// It also tests various constraints and edge-cases, including the use of
    /// tolerances.
    #[test]
    fn recent_consensus() {
        let pool = testdata2::test_db();
        let no_tolerance = DirToleranceBuilder::default()
            .pre_valid_tolerance(Duration::ZERO)
            .post_valid_tolerance(Duration::ZERO)
            .build()
            .unwrap();
        let liberal_tolerance = DirToleranceBuilder::default()
            .pre_valid_tolerance(Duration::from_secs(60 * 60)) // 1h before
            .post_valid_tolerance(Duration::from_secs(60 * 60)) // 1h after
            .build()
            .unwrap();
        let docid = Sha256::digest(testdata2::current_consensus_ns().1.as_bytes());
        let lifetime = testdata2::current_consensus_ns().0.preamble.lifetime;
        let unsigned_sha3_256 = testdata2::consensus_sha3(testdata2::current_consensus_ns().1);
        read_tx(&pool, move |tx| {
            // Get None by being way before valid-after.
            assert!(
                ConsensusMeta::<Plain>::query(
                    tx,
                    &no_tolerance,
                    Some((lifetime.valid_after.0 - Duration::from_secs(60 * 60 * 24 * 365)).into())
                )
                .unwrap()
                .is_empty()
            );
            // Get None by being way behind valid-until.
            assert!(
                ConsensusMeta::<Plain>::query(
                    tx,
                    &no_tolerance,
                    Some((lifetime.valid_until.0 + Duration::from_secs(60 * 60 * 24 * 365)).into()),
                )
                .unwrap()
                .is_empty()
            );
            // Get None by being minimally before valid-after.
            assert!(
                ConsensusMeta::<Plain>::query(
                    tx,
                    &no_tolerance,
                    Some((lifetime.valid_after.0 - Duration::from_secs(1)).into()),
                )
                .unwrap()
                .is_empty()
            );
            // Get None by being minimally behind valid-until.
            assert!(
                ConsensusMeta::<Plain>::query(
                    tx,
                    &no_tolerance,
                    Some((lifetime.valid_until.0 + Duration::from_secs(1)).into()),
                )
                .unwrap()
                .is_empty()
            );
            // Get a valid consensus by being in the interval (or None).
            let res1 = ConsensusMeta::<Plain>::query(
                tx,
                &no_tolerance,
                Some(lifetime.valid_after.0.into()),
            )
            .unwrap()[0];
            let res2 = ConsensusMeta::<Plain>::query(
                tx,
                &no_tolerance,
                Some(lifetime.valid_until.0.into()),
            )
            .unwrap()[0];
            let res3 = ConsensusMeta::<Plain>::query(
                tx,
                &no_tolerance,
                Some(testdata2::valid_system_time().into()),
            )
            .unwrap()[0];
            let res4 = ConsensusMeta::<Plain>::query(tx, &no_tolerance, None).unwrap()[0];
            assert_eq!(
                res1,
                ConsensusMeta {
                    docid,
                    unsigned_sha3_256,
                    valid_after: lifetime.valid_after.0.into(),
                    fresh_until: lifetime.fresh_until.0.into(),
                    valid_until: lifetime.valid_until.0.into(),
                    flavor: Default::default(),
                }
            );
            assert_eq!(res1, res2);
            assert_eq!(res2, res3);
            assert_eq!(res3, res4);
            // Get a valid consensus using a liberal dir tolerance.
            let res1 = ConsensusMeta::<Plain>::query(
                tx,
                &liberal_tolerance,
                Some((lifetime.valid_after.0 - Duration::from_secs(60 * 30)).into()),
            )
            .unwrap()[0];
            let res2 = ConsensusMeta::<Plain>::query(
                tx,
                &liberal_tolerance,
                Some((lifetime.valid_until.0 + Duration::from_secs(60 * 30)).into()),
            )
            .unwrap()[0];
            assert_eq!(
                res1,
                ConsensusMeta {
                    docid,
                    unsigned_sha3_256,
                    valid_after: lifetime.valid_after.0.into(),
                    fresh_until: lifetime.fresh_until.0.into(),
                    valid_until: lifetime.valid_until.0.into(),
                    flavor: Default::default(),
                }
            );
            assert_eq!(res1, res2);
            // TODO DIRMIRROR: Test retrieval of multiple consensuses, which
            // requires the test database to contain more than one.
        })
        .unwrap();
    }
    /// Tests whether the timeout computation lies within the proper interval.
    ///
    /// Because this involves randomness, it performs the test several thousand
    /// times.  This should be okay performance wise, as it takes about ~250ms
    /// with a debug build on my machine.
    #[test]
    fn sync_timeout() {
        // We repeat the tests a few thousand times to go over many random values.
        let docid = Sha256::digest(testdata2::current_consensus_ns().1.as_bytes());
        let lifetime = testdata2::current_consensus_ns().0.preamble.lifetime;
        let unsigned_sha3_256 = testdata2::consensus_sha3(testdata2::current_consensus_ns().1);
        let cons = ConsensusMeta::<Plain> {
            docid,
            unsigned_sha3_256,
            valid_after: lifetime.valid_after.0.into(),
            fresh_until: lifetime.fresh_until.0.into(),
            valid_until: lifetime.valid_until.0.into(),
            flavor: Default::default(),
        };
        for _ in 0..10000 {
            let when = cons.lifetime(&mut testing_rng());
            assert!(when >= lifetime.fresh_until.0.into());
            // Computes the half between fresh_until and valid_until.
            assert!(
                when <= (lifetime.fresh_until.0
                    + (lifetime
                        .valid_until
                        .0
                        .duration_since(lifetime.fresh_until.0)
                        .unwrap()
                        / 2))
                    .into()
            );
        }
    }
    /// Tests whether authority certificates are properly queried from the database.
    #[test]
    fn get_auth_cert() {
        let pool = testdata2::test_db();
        // Empty.
        let (found, missing) = read_tx(&pool, |tx| {
            AuthCertMeta::query(
                tx,
                &[],
                &DirTolerance::default(),
                testdata2::valid_system_time().into(),
            )
        })
        .unwrap()
        .unwrap();
        assert!(found.is_empty());
        assert!(missing.is_empty());
        // Find one and two missing ones.
        let (found, missing) = read_tx(&pool, |tx| {
            AuthCertMeta::query(
                tx,
                &[
                    // Found one.
                    AuthCertKeyIds {
                        id_fingerprint: *testdata2::current_auth_certs()[0].0.id_fingerprint(),
                        sk_fingerprint: testdata2::current_auth_certs()[0]
                            .0
                            .signing_key()
                            .to_rsa_identity(),
                    },
                    // Missing.
                    AuthCertKeyIds {
                        id_fingerprint: RsaIdentity::from_hex(
                            "0000000000000000000000000000000000000000",
                        )
                        .unwrap(),
                        sk_fingerprint: RsaIdentity::from_hex(
                            "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
                        )
                        .unwrap(),
                    },
                    // Missing.
                    AuthCertKeyIds {
                        id_fingerprint: RsaIdentity::from_hex(
                            "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
                        )
                        .unwrap(),
                        sk_fingerprint: RsaIdentity::from_hex(
                            "0000000000000000000000000000000000000000",
                        )
                        .unwrap(),
                    },
                ],
                &DirTolerance::default(),
                testdata2::valid_system_time().into(),
            )
        })
        .unwrap()
        .unwrap();
        assert_eq!(
            found,
            vec![AuthCertMeta {
                docid: DocumentId::digest(testdata2::current_auth_certs()[0].1.as_bytes()),
                kp_auth_id_rsa_sha1: Sha1::from(
                    testdata2::current_auth_certs()[0]
                        .0
                        .id_fingerprint()
                        .to_bytes()
                ),
                kp_auth_sign_rsa_sha1: Sha1::from(
                    testdata2::current_auth_certs()[0]
                        .0
                        .signing_key()
                        .to_rsa_identity()
                        .to_bytes()
                ),
                dir_key_published: testdata2::current_auth_certs()[0].0.published().into(),
                dir_key_expires: testdata2::current_auth_certs()[0].0.expires().into(),
            }]
        );
        assert_eq!(
            missing,
            vec![
                AuthCertKeyIds {
                    id_fingerprint: RsaIdentity::from_hex(
                        "0000000000000000000000000000000000000000",
                    )
                    .unwrap(),
                    sk_fingerprint: RsaIdentity::from_hex(
                        "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
                    )
                    .unwrap(),
                },
                AuthCertKeyIds {
                    id_fingerprint: RsaIdentity::from_hex(
                        "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
                    )
                    .unwrap(),
                    sk_fingerprint: RsaIdentity::from_hex(
                        "0000000000000000000000000000000000000000",
                    )
                    .unwrap(),
                }
            ]
        );
    }
    /// Tests whether the missing router descriptor queue is computed properly.
    ///
    /// For this, we remove existing router descriptors from the database and
    /// see whether they are determined as missing properly.
    #[test]
    fn missing_server_descriptors() {
        let pool = testdata2::test_db();
        let meta = read_tx(&pool, |tx| {
            ConsensusMeta::<Plain>::query(
                tx,
                &DirTolerance::default(),
                Some(testdata2::valid_system_time().into()),
            )
        })
        .unwrap()
        .unwrap()[0];
        // Ensure that the returned consensus matches the one from testdata2.
        assert_eq!(
            meta.docid,
            DocumentId::digest(testdata2::current_consensus_ns().1.as_bytes())
        );
        // Delete a single router descriptor, so we can determine a missing one
        // using it.
        let removed_descriptor = *testdata2::current_consensus_ns().0.routers[0].doc_digest();
        let removed_descriptor = Sha1::from(removed_descriptor);
        pool.get()
            .unwrap()
            .execute(
                sql!(
                    "
                    DELETE FROM router_descriptor
                    WHERE unsigned_sha1 = ?1
                    "
                ),
                params![removed_descriptor],
            )
            .unwrap();
        // Only one should be returned.
        let missing_servers = read_tx(&pool, |tx| meta.missing_servers(tx))
            .unwrap()
            .unwrap();
        assert_eq!(missing_servers, HashSet::from([removed_descriptor]));
        // If we delete all router descriptors we have, we should get all.
        rw_tx(&pool, |tx| {
            tx.execute(sql!("DELETE FROM router_descriptor"), params![])
        })
        .unwrap()
        .unwrap();
        // Now all should be returned; we verify this by checking that the
        // result is present in all_descriptors, which is a superset.
        let missing_servers = read_tx(&pool, |tx| meta.missing_servers(tx))
            .unwrap()
            .unwrap();
        // This is a superset of missing_servers because it includes router
        // descriptors that are not a part of the current consensus.
        let all_descriptors = testdata2::current_router_descs()
            .iter()
            .map(|x| Sha1::from(x.1.hashes.sha1.unwrap()))
            .collect::<HashSet<_>>();
        assert!(
            missing_servers
                .iter()
                .all(|sha1| all_descriptors.contains(sha1))
        );
    }
    /// Tests whether the missing extra-info documents are computed properly.
    // TODO DIRMIRROR: Expand on this once we have proper extra-info support.
    #[test]
    fn missing_extra_infos() {
        let pool = testdata2::test_db();
        let meta = read_tx(&pool, |tx| {
            ConsensusMeta::<Plain>::query(
                tx,
                &DirTolerance::default(),
                Some(testdata2::valid_system_time().into()),
            )
        })
        .unwrap()
        .unwrap()[0];
        // Ensure that the returned consensus matches the one from testdata2.
        assert_eq!(
            meta.docid,
            DocumentId::digest(testdata2::current_consensus_ns().1.as_bytes())
        );
        // We should have no missing extra-infos.
        let missing_extras = read_tx(&pool, |tx| meta.missing_extras(tx))
            .unwrap()
            .unwrap();
        assert!(missing_extras.is_empty());
        // TODO DIRMIRROR: Once we have support for extra-info's, add a test
        // for this here.  Right now, testing this is pretty useless as we
        // cannot add it nicely to the testdata2 module if there isn't even
        // an ExtraInfo struct from tor-netdoc.
    }
    /// Tests whether the missing micro descriptor queue is computed properly.
    ///
    /// For this, we remove existing micro descriptors from the database and
    /// see whether they are determined as missing properly.
    #[test]
    fn missing_micro_descriptors() {
        let pool = testdata2::test_db();
        let meta = read_tx(&pool, |tx| {
            ConsensusMeta::<Md>::query(
                tx,
                &DirTolerance::default(),
                Some(testdata2::valid_system_time().into()),
            )
        })
        .unwrap()
        .unwrap()[0];
        // Ensure that the returned consensus matches the one from testdata2.
        assert_eq!(
            meta.docid,
            DocumentId::digest(testdata2::current_consensus_md().1.as_bytes())
        );
        // Delete a single router descriptor, so we can determine a missing one
        // using it.
        let removed_descriptor = *testdata2::current_consensus_md().0.routers[0].doc_digest();
        let removed_descriptor = Sha256::from(removed_descriptor);
        pool.get()
            .unwrap()
            .execute(
                sql!(
                    "
                    DELETE FROM router_descriptor
                    WHERE unsigned_sha2 = ?1
                    "
                ),
                params![removed_descriptor],
            )
            .unwrap();
        // Only one should be returned.
        let missing_micros = read_tx(&pool, |tx| meta.missing_micros(tx))
            .unwrap()
            .unwrap();
        assert_eq!(missing_micros, HashSet::from([removed_descriptor]));
        // If we delete all micro descriptors we have, we should get all.
        rw_tx(&pool, |tx| {
            tx.execute(sql!("DELETE FROM router_descriptor"), params![])
        })
        .unwrap()
        .unwrap();
        // Now all should be returned; we verify this by checking that the
        // result is present in all_descriptors, which is a superset.
        let missing_micros = read_tx(&pool, |tx| meta.missing_micros(tx))
            .unwrap()
            .unwrap();
        // This is a superset of missing_micros because it includes micro
        // descriptors that are not a part of the current consensus.
        let all_descriptors = testdata2::current_micro_descs()
            .iter()
            .map(|x| Sha256::digest(x.1.as_bytes()))
            .collect::<HashSet<_>>();
        assert!(
            missing_micros
                .iter()
                .all(|sha2| all_descriptors.contains(sha2))
        );
    }
}