Skip to main content

coven/
builder.rs

1//! Compose database, custody, operation and file owners at the store boundary.
2
3use crate::*;
4use coven_crypto::custody::{
5    Custody, CustodyChoice, InMemoryCustody, KeyMaterial, KeySession, Keychain, KeyringCustody,
6    PassphraseCustody, PersonEntries, StoreCustody, StoreKeychain,
7};
8use coven_database::{DatabaseBuilder, OpenStep};
9use coven_foundation::files::StoreDirAccess;
10use coven_foundation::files::StoreFile;
11use coven_foundation::progress::Progress;
12#[cfg(test)]
13use coven_foundation::progress::RecordingClock;
14use coven_storage::{
15    providers::{OAuthExchange, OAuthFlow, StorageConnector},
16    Storage,
17};
18use std::sync::Arc;
19
20/// The choices for creating or opening a store (E1).
21pub struct CovenBuilder {
22    tables: Option<Vec<SyncedTable>>,
23    namespaces: Vec<coven_database::Namespace>,
24    migrations: Option<Vec<Migration>>,
25    directories: coven_foundation::files::StoreDirectories,
26    keychain: Arc<Keychain>,
27    ids: IdSourceRef,
28    clock: ClockRef,
29    connector: Option<Arc<dyn StorageConnector>>,
30    transfers: Arc<coven_storage::TransferBudget>,
31    /// Where the store's watch hears about the device's network (§6.7).
32    network: coven_foundation::network_watch::NetworkSource,
33    oauth: Option<OAuthClients>,
34    presenter: Option<Arc<dyn OAuthPresenter>>,
35    /// The latest sign-in, held for this session until bootstrap or setup
36    /// publishes it, or a join keeps it on the device beside its saved code.
37    authentication: Option<Arc<coven_sync::SessionSignIn>>,
38    keys: SessionSource<StoreKeyring>,
39    identity: SessionSource<MemberIdentity>,
40    sqlite: SqliteSettings,
41    sql_functions: coven_database::SqlFunctions,
42    /// Stop a replacement right after its swap, as if the process ended there.
43    #[cfg(test)]
44    interrupt_after_swap: bool,
45    /// Fail the insert of the device-reset notice a reset commits, as a full
46    /// disk would.
47    #[cfg(test)]
48    fail_device_reset_notice: bool,
49    /// Have SQLite refuse to close the store this builder opens.
50    #[cfg(test)]
51    refuse_database_close: bool,
52    /// Stop a replacement right after its fresh device registered, before
53    /// its swap, as if the process ended there.
54    #[cfg(test)]
55    interrupt_after_registering: bool,
56    /// Stop the next commit of a new installation's custody right after it
57    /// writes the device id, as if the process ended there.
58    #[cfg(test)]
59    interrupt_commit: bool,
60    /// Records every value a bootstrap through this builder reports as it
61    /// loads; also the builder's clock.
62    #[cfg(test)]
63    loading_recorder: Option<Arc<RecordingClock<coven_database::LoadStep>>>,
64}
65
66/// What a store's owners continue from as its handle is made.
67enum Continuing {
68    /// The store's own files: a new sequence state, no connection yet, and
69    /// device credentials over this installation's keychain.
70    Fresh,
71    /// A bootstrap that loaded the store from storage: the sequence state
72    /// and connection it loaded through, and, for a join, the device
73    /// credentials its sign-in is kept in (§13.2).
74    Loaded {
75        state: Arc<coven_sync::SequenceState>,
76        storage: Arc<coven_storage::StorageConnection>,
77        credentials: Option<Arc<coven_sync::DeviceCredentials>>,
78    },
79}
80
81/// What a builder makes a key kind's session from: the configured choice, or
82/// the unlocked session a reset or restore transfers into the replacement.
83enum SessionSource<T: KeyMaterial> {
84    Configured(CustodyChoice<T>),
85    Session(Arc<KeySession<T>>),
86}
87
88impl CovenBuilder {
89    pub(crate) fn new(
90        directories: coven_foundation::files::StoreDirectories,
91        keychain: Arc<Keychain>,
92        network: coven_foundation::network_watch::NetworkSource,
93    ) -> Self {
94        let ids: IdSourceRef = Arc::new(UuidIds);
95        let clock: ClockRef = Arc::new(SystemClock);
96        Self {
97            tables: None,
98            namespaces: Vec::new(),
99            migrations: None,
100            directories,
101            keychain,
102            ids,
103            clock,
104            connector: None,
105            oauth: None,
106            #[cfg(not(any(target_os = "ios", target_os = "android")))]
107            presenter: Some(Arc::new(DesktopOAuthPresenter)),
108            #[cfg(any(target_os = "ios", target_os = "android"))]
109            presenter: None,
110            authentication: None,
111            transfers: Arc::new(coven_storage::TransferBudget::new(TransferLimits::default())),
112            network,
113            keys: SessionSource::Configured(CustodyChoice::Keyring),
114            identity: SessionSource::Configured(CustodyChoice::Keyring),
115            sqlite: SqliteSettings::default(),
116            sql_functions: coven_database::SqlFunctions::default(),
117            #[cfg(test)]
118            interrupt_after_swap: false,
119            #[cfg(test)]
120            fail_device_reset_notice: false,
121            #[cfg(test)]
122            refuse_database_close: false,
123            #[cfg(test)]
124            interrupt_after_registering: false,
125            #[cfg(test)]
126            interrupt_commit: false,
127            #[cfg(test)]
128            loading_recorder: None,
129        }
130    }
131
132    /// The tables that sync (E2). Required.
133    pub fn synced_tables(mut self, tables: Vec<SyncedTable>) -> Self {
134        self.tables = Some(tables);
135        self
136    }
137    /// The file namespaces, each with its kind and cache budget (E1). A file
138    /// is staged into one of them.
139    pub fn namespaces(mut self, namespaces: Vec<coven_database::Namespace>) -> Self {
140        self.namespaces = namespaces;
141        self
142    }
143    /// The app's schema migrations, numbered from 1 with no gaps (E13).
144    /// Required.
145    pub fn migrations(mut self, migrations: Vec<Migration>) -> Self {
146        self.migrations = Some(migrations);
147        self
148    }
149    /// The wall clock that timestamps use (§7.2). Defaults to the system clock.
150    pub fn clock(mut self, clock: ClockRef) -> Self {
151        self.clock = clock;
152        self
153    }
154    /// The source of new ids: store, circle, member, key, invite and file ids,
155    /// and this install's device id (Appendix F.2). Defaults to `UuidIds`.
156    pub fn id_source(mut self, ids: IdSourceRef) -> Self {
157        self.ids = ids;
158        self
159    }
160    /// Replace provider construction in tests while exercising setup, credential
161    /// custody, reconnect, restore and recovery through the production graph.
162    /// `coven_storage::test_utils::MemoryStorage` implements this connector.
163    #[cfg(any(test, feature = "test-utils"))]
164    pub fn storage_connector(mut self, connector: Arc<dyn StorageConnector>) -> Self {
165        self.connector = Some(connector);
166        self
167    }
168    fn connector(&mut self) -> Arc<dyn StorageConnector> {
169        self.connector
170            .get_or_insert_with(|| {
171                Arc::new(coven_storage::providers::ProviderConnector::new(
172                    self.clock.clone(),
173                    self.ids.clone(),
174                ))
175            })
176            .clone()
177    }
178
179    /// Configure this app's own sign-in clients. Coven supplies no client ids.
180    pub fn oauth_clients(mut self, clients: OAuthClients) -> Self {
181        self.oauth = Some(clients);
182        self
183    }
184    /// Present sign-in through the app's platform sheet. Desktop defaults to
185    /// `DesktopOAuthPresenter`; iOS and Android require the app's presenter.
186    pub fn oauth_presenter(mut self, presenter: Arc<dyn OAuthPresenter>) -> Self {
187        self.presenter = Some(presenter);
188        self
189    }
190
191    /// Sign in and keep tokens in session custody for restore, join or opening.
192    /// Replaces the prior sign-in only on success. Dropping the future cancels it.
193    pub async fn authenticate(&mut self, access: OAuthAccess) -> Result<(), OAuthError> {
194        let exchange = self.oauth_exchange();
195        let flow = Self::oauth_flow(exchange.clone(), self.presenter.clone())
196            .ok_or(OAuthError::Unavailable(access.provider()))?;
197        let tokens = flow.authenticate(access).await?;
198        self.authentication = Some(coven_sync::SessionSignIn::new(tokens, exchange));
199        Ok(())
200    }
201
202    /// The app's sign-in clients, sending their token requests with this
203    /// builder's clock and a client of their own.
204    fn oauth_exchange(&self) -> Option<OAuthExchange> {
205        Some(OAuthExchange::new(self.oauth.clone()?, self.clock.clone()))
206    }
207
208    /// Whether the app configured sign-in: its clients and its presenter.
209    fn signs_in(&self) -> bool {
210        self.oauth.is_some() && self.presenter.is_some()
211    }
212
213    fn oauth_flow(
214        exchange: Option<OAuthExchange>,
215        presenter: Option<Arc<dyn OAuthPresenter>>,
216    ) -> Option<OAuthFlow> {
217        Some(OAuthFlow::new(exchange?, presenter?))
218    }
219
220    /// Limit file uploads running together; defaults to one.
221    pub fn max_concurrent_uploads(self, n: std::num::NonZeroUsize) -> Self {
222        let mut limits = self.transfers.limits();
223        limits.uploads = n;
224        self.transfers.set_limits(limits);
225        self
226    }
227    /// Limit file downloads running together; defaults to one. The
228    /// sequence's own reads don't count against it.
229    pub fn max_concurrent_downloads(self, n: std::num::NonZeroUsize) -> Self {
230        let mut limits = self.transfers.limits();
231        limits.downloads = n;
232        self.transfers.set_limits(limits);
233        self
234    }
235
236    /// Where this device keeps the store keys: the OS keychain by default, a
237    /// file sealed with a passphrase, memory for this session only, or the
238    /// app's own `Custody<StoreKeyring>`.
239    pub fn key_custody(mut self, custody: CustodyChoice<StoreKeyring>) -> Self {
240        self.keys = SessionSource::Configured(custody);
241        self
242    }
243    /// Where this device keeps its member's keys, with the same four choices
244    /// and the app's own `Custody<MemberIdentity>` as the last.
245    pub fn identity_custody(mut self, custody: CustodyChoice<MemberIdentity>) -> Self {
246        self.identity = SessionSource::Configured(custody);
247        self
248    }
249
250    /// SQLite settings for every connection a handle opens to the store's
251    /// database (§4). Unset ones keep SQLite's defaults, except that
252    /// `checkpoint_fullfsync` is on for macOS and iOS; the page size and
253    /// auto-vacuum mode apply only when the database is created.
254    pub fn sqlite_settings(mut self, settings: SqliteSettings) -> Self {
255        self.sqlite = settings;
256        self
257    }
258
259    /// Apple's protection class for the store's files: when they can be read
260    /// and written against whether the device is locked (E1). Creating,
261    /// restoring, joining or replacing a store gives each new installation's
262    /// directory the class before anything is made there. A writable open,
263    /// a resumed restore or join, and a replacement give it to every file
264    /// already there, unless they were last given this class. On a volume
265    /// that keeps no classes each of them fails before any file changes.
266    /// Unset, coven leaves every class as it is, and new files take their
267    /// folder's.
268    #[cfg(target_vendor = "apple")]
269    pub fn file_protection(mut self, protection: FileProtection) -> Self {
270        self.directories = self.directories.file_protection(protection);
271        self
272    }
273
274    /// A collation the app's SQL names with `COLLATE name` (§4), ordering
275    /// text by `compare` at `version`, on every connection a handle opens to
276    /// the store's database. The order is part of the store's schema:
277    /// `compare` must be deterministic, the same on every device and in every
278    /// app version that registers this `version`, and a total order. Every
279    /// change to the order takes a new version, and a writable open then
280    /// rebuilds the indexes in it. A name that is SQLite's own (`BINARY`,
281    /// `NOCASE`, `RTRIM`) or given twice fails the open, and a panic in
282    /// `compare` ends the process.
283    pub fn collation<F>(mut self, name: impl Into<String>, version: u32, compare: F) -> Self
284    where
285        F: Fn(&str, &str) -> std::cmp::Ordering + Send + Sync + 'static,
286    {
287        self.sql_functions =
288            std::mem::take(&mut self.sql_functions).collation(name, version, compare);
289        self
290    }
291
292    /// A scalar function the app's SQL calls as `name(...)` with exactly
293    /// `arguments` arguments (§4), on every connection a handle opens to the
294    /// store's database. `compute` reads the arguments and returns the
295    /// result, or an error that fails the statement calling it.
296    /// A deterministic function can appear in indexes, CHECKs and generated
297    /// columns, and is then part of the store's schema: at its version it
298    /// must give the same result for the same arguments on every device and
299    /// in every app version, and takes at least one argument. Every change
300    /// to its results takes a new version, and a writable open then rebuilds
301    /// the indexes that call it. A name SQLite or coven
302    /// already has, one with coven's prefix `coven_` or `_coven_`, or one
303    /// given twice fails the open, and a panic in `compute` ends the process.
304    pub fn function<F, T>(
305        mut self,
306        name: impl Into<String>,
307        arguments: u8,
308        determinism: Determinism,
309        compute: F,
310    ) -> Self
311    where
312        F: Fn(&FunctionArguments<'_>) -> SqliteResult<T> + Send + Sync + 'static,
313        T: Into<types::Value>,
314    {
315        self.sql_functions =
316            std::mem::take(&mut self.sql_functions).scalar(name, arguments, determinism, compute);
317        self
318    }
319
320    /// Create and open a store, initializing its member in the chosen custody
321    /// before publication. Cancellation does not interrupt creation or rollback.
322    /// Errors carry no handle; a published store is closed before returning.
323    pub async fn create_store(self, name: &str) -> Result<CovenHandle, StoreCreationError> {
324        let name = name.to_owned();
325        let bugs = self.directories.bugs().clone();
326        crate::task::spawn_completing(
327            "create the store",
328            bugs,
329            creation::create(self, name, coven_foundation::files::BootstrapStore::publish),
330        )
331        .await?
332    }
333
334    /// Opens the store for reading and writing, taking the store's lock.
335    /// Opening always migrates coven's tables before the app's schema, then
336    /// unlocks both custody sessions before resuming unfinished operations and
337    /// committed file work. Empty custody is allowed; an unlock failure prevents
338    /// opening. No sync loop starts, and local database calls need no key material.
339    /// `on_progress` is called on this task with each step the open runs and
340    /// how much of it is done, at most a few times a second (§4).
341    pub async fn open(
342        self,
343        store: StoreId,
344        on_progress: impl Fn(Progress<OpenStep>),
345    ) -> CovenResult<CovenHandle> {
346        // Its work rides on the heap, as every bootstrap's does, so an app's
347        // future holding this call stays small.
348        Box::pin(async move {
349            let opening = self.open_graph(store, Opening::ForUse).await?;
350            let progress = opening.database.subscribe_progress();
351            progress.forward(opening.open(), on_progress).await
352        })
353        .await
354    }
355
356    /// Finishes this device's recorded deletion of `store`, which
357    /// `CovenHandle::delete_store` began and couldn't finish: it opens the
358    /// store as `open` does, then goes on as the deletion would, with the
359    /// choice for unsent work it recorded. Before the device's removal is
360    /// journaled, a refusal or failure removes the record and leaves the
361    /// store as it was; from the journal on, a failure is
362    /// `StoreDeletionError::Unfinished` and the deletion stays recorded
363    /// (§11.6, E1). A store with no deletion recorded fails with
364    /// `StoreDeletionError::NoUnfinishedDeletion`.
365    pub async fn resume_deletion(self, store: StoreId) -> Result<(), StoreDeletionError> {
366        Box::pin(async move {
367            let directory = self.directories.store_dir(&store);
368            let bugs = directory.bugs().clone();
369            let unsent =
370                crate::task::blocking_completing("read the deletion record", bugs, move || {
371                    directory.unfinished_deletion()
372                })
373                .await??
374                .ok_or(StoreDeletionError::NoUnfinishedDeletion(store))?;
375            let opening = self
376                .open_graph(store, Opening::ToDelete)
377                .await
378                .map_err(StoreDeletionError::Open)?;
379            let handle = opening.open().await.map_err(StoreDeletionError::Open)?;
380            handle.leave(unsent).await
381        })
382        .await
383    }
384
385    /// Replace this installation with a new device loaded from storage
386    /// (§19.3), whenever the app chooses: after sync found another copy using
387    /// this device's id, a damaged database, or a device entry `open` found
388    /// missing. Uses saved settings, credentials and member keys; nothing
389    /// unsent is salvaged. The new device loads beside the old copy, which
390    /// moves aside only once loading is complete; saved member keys that
391    /// history has replaced fail with `MemberKeysReplaced` and change nothing.
392    /// Just before the fresh device registers, the reset records itself and
393    /// only goes forward: the store refuses opens with
394    /// `StoreLockError::ReplacementUnfinished` until this call, or
395    /// `resume_replacement`, finishes it. An interrupted call resumes with the
396    /// same fresh id. A replacement `restore_device` began is refused with
397    /// `StoreLockError::ReplacementUnfinished`, changing nothing. The returned
398    /// handle's `device_reset()` names the old and new ids. `on_status` hears
399    /// how far the new device has come, as the other bootstrap calls report
400    /// it.
401    pub async fn reset_device(
402        self,
403        store: StoreId,
404        device_name: &str,
405        on_status: impl Fn(BootstrapStatus),
406    ) -> Result<CovenHandle, BootstrapError> {
407        Box::pin(bootstrap::reset_device(self, store, device_name, on_status)).await
408    }
409
410    /// Restore this device's installation of `store` from the person's
411    /// current restore code, for a device whose member keys were replaced
412    /// (§13.1). The code must name this store, and its pair must be current,
413    /// checked against history before the store or keychain changes. The new
414    /// device loads beside the old copy, which stays openable until the
415    /// restore records itself, just before its fresh device registers; from
416    /// then on the store refuses opens and the restore only goes forward,
417    /// and once registered it swaps in and the old copy is removed. Cancelling
418    /// past the record waits for the registration, then fails with
419    /// `RestoreRegistered`. An interrupted call is finished by retrying it, or
420    /// by `resume_replacement`; a replacement `reset_device` began is refused
421    /// with `StoreLockError::ReplacementUnfinished`, changing nothing. A
422    /// device whose custody and applied history already hold the code's pair
423    /// just opens. S3 requires the separately held key, as
424    /// `restore_from_code` does. `prepare` is the app's step on the new
425    /// installation: one bulk write (`CovenHandle::write_bulk`) of its
426    /// device-local tables, run after loading and before the record, so what
427    /// it writes, such as preferences read from the old copy before the call,
428    /// takes the old copy's place with the new installation or not at all. A
429    /// call that finds the replacement recorded, or the pair already
430    /// installed, doesn't run it.
431    pub async fn restore_device(
432        self,
433        store: StoreId,
434        code: &str,
435        s3_credentials: Option<S3Credentials>,
436        device_name: &str,
437        prepare: impl FnOnce(BulkSqlContext<'_>) -> CovenResult<()> + Send + 'static,
438        on_status: impl Fn(BootstrapStatus),
439        cancel: &tokio::sync::watch::Receiver<bool>,
440    ) -> Result<CovenHandle, BootstrapError> {
441        Box::pin(bootstrap::restore_device(
442            self,
443            store,
444            code,
445            s3_credentials,
446            device_name,
447            Box::new(prepare),
448            on_status,
449            cancel,
450        ))
451        .await
452    }
453
454    /// Finishes this device's replacement of `store` past its record, as the
455    /// call that began it would: `reset_device` or `restore_device`, from
456    /// what the replacement kept on the device, with neither the code nor a
457    /// new sign-in (§19.3). On Dropbox and OneDrive it connects through the
458    /// kept sign-in, or a sign-in this builder holds once it grants the
459    /// location's access. A store with no replacement under way fails with
460    /// `BootstrapError::NoUnfinishedReplacement`.
461    pub async fn resume_replacement(
462        self,
463        store: StoreId,
464        on_status: impl Fn(BootstrapStatus),
465        cancel: &tokio::sync::watch::Receiver<bool>,
466    ) -> Result<CovenHandle, BootstrapError> {
467        Box::pin(bootstrap::resume_replacement(self, store, on_status, cancel)).await
468    }
469
470    /// The joins under way on this device, ordered by store id (§13.2). A
471    /// saved code coven can't read fails the call; it is never skipped. An
472    /// unfinished installation that keeps neither a saved code nor a
473    /// restore's record, and that no call holds, is deleted: a crash left it
474    /// before its call wrote to storage (§13.4).
475    pub async fn unfinished_joins(&self) -> Result<Vec<InviteInfo>, BootstrapError> {
476        bootstrap::unfinished_joins(self).await
477    }
478
479    /// Continues an unfinished join from its saved code and device name, as
480    /// `join_with_invite` does. On Dropbox and OneDrive it connects through
481    /// the sign-in the join keeps on the device, or through a sign-in this
482    /// builder holds, which replaces the kept one once it is shown to grant
483    /// the location's access. With neither, or with a kept sign-in the
484    /// provider refuses even after its one refresh, it fails with
485    /// `OAuthError::Reauthorize` and the join stays unfinished (§13.2).
486    /// Returns `None` when the request was declined or the invite expired,
487    /// after deleting the kept sign-in and the saved code. A store with no
488    /// saved code fails with `BootstrapError::NoUnfinishedJoin`.
489    pub async fn resume_join(
490        self,
491        store: StoreId,
492        on_status: impl Fn(BootstrapStatus),
493        control: &JoinControl,
494    ) -> Result<Option<CovenHandle>, BootstrapError> {
495        Box::pin(bootstrap::resume_join(self, store, on_status, control)).await
496    }
497
498    /// The restores under way on this device, ordered by store id: each
499    /// restore that recorded itself before registering the device and has
500    /// not ended, as `decode_code_info` reads its code (§13.1). A record coven
501    /// can't read fails the call; it is never skipped. An unfinished
502    /// installation that keeps neither a record nor a join's saved code, and
503    /// that no call holds, is deleted, as `unfinished_joins` does (§13.4).
504    pub async fn unfinished_restores(&self) -> Result<Vec<CodeMetadata>, BootstrapError> {
505        bootstrap::unfinished_restores(self).await
506    }
507
508    /// Continues an unfinished restore from its record, as `restore_from_code`
509    /// does: the code and device name it recorded, and the S3 key or sign-in
510    /// it kept on the device. On Dropbox and OneDrive a sign-in this builder
511    /// holds replaces the kept one once it is shown to grant the location's
512    /// access; with neither, or with a kept sign-in the provider refuses even
513    /// after its one refresh, it fails with `OAuthError::Reauthorize` and the
514    /// restore stays unfinished. Cancelling ends it as `restore_from_code`'s
515    /// cancel does. A store with no record fails with
516    /// `BootstrapError::NoUnfinishedRestore`.
517    pub async fn resume_restore(
518        self,
519        store: StoreId,
520        on_status: impl Fn(BootstrapStatus),
521        cancel: &tokio::sync::watch::Receiver<bool>,
522    ) -> Result<CovenHandle, BootstrapError> {
523        Box::pin(bootstrap::resume_restore(self, store, on_status, cancel)).await
524    }
525
526    fn database(&self, directory: StoreDir) -> CovenResult<DatabaseBuilder> {
527        let tables = self
528            .tables
529            .clone()
530            .ok_or(CovenError::MissingConfiguration { field: "synced_tables" })?;
531        let migrations = self
532            .migrations
533            .clone()
534            .ok_or(CovenError::MissingConfiguration { field: "migrations" })?;
535        let database = DatabaseBuilder::new(directory)
536            .migration_operation(coven_sync::SequenceSync::migration_operation)
537            .synced_tables(tables)
538            .namespaces(self.namespaces.clone())
539            .migrations(migrations)
540            .clock(self.clock.clone())
541            .id_source(self.ids.clone())
542            .sqlite_settings(self.sqlite.clone())
543            .sql_functions(self.sql_functions.clone());
544        #[cfg(test)]
545        let database = if self.refuse_database_close { database.refuse_close() } else { database };
546        Ok(database)
547    }
548
549    fn keychain(&self) -> Result<Arc<Keychain>, KeyError> {
550        Ok(self.keychain.clone())
551    }
552
553    async fn open_graph(self, store: StoreId, opening: Opening) -> CovenResult<OpeningStore> {
554        let directory = self.directories.store_dir(&store);
555        let database = self.database(directory.clone())?;
556        let lock = directory.lock_for_open().await?;
557        let closing = lock.closing_guard();
558        let bugs = directory.bugs().clone();
559        let result =
560            crate::task::blocking_completing("open the store's custody", bugs, move || {
561                let mut builder = self;
562                let settings = lock.settings()?;
563                let keychain = Arc::new(StoreKeychain::new(builder.keychain()?, settings.id));
564                // An interrupted replacement resumes only through the call that
565                // began it (§19.3).
566                refuse_unfinished_replacement(&directory, settings.id)?;
567                // A store this device is leaving opens only to finish that.
568                if opening == Opening::ForUse {
569                    refuse_unfinished_deletion(&directory, settings.id)?;
570                }
571                // A copy restored without its non-backed-up custody id, or a
572                // keychain that no longer names it, changes nothing (§19.3).
573                if keychain.device_id()? != Some(settings.device_id) {
574                    let identity = Self::make_session(
575                        std::mem::replace(
576                            &mut builder.identity,
577                            SessionSource::Configured(CustodyChoice::InMemory),
578                        ),
579                        &directory,
580                        settings.id,
581                        keychain,
582                    );
583                    let store = settings.id;
584                    // Without member keys a connected store can only be restored
585                    // with its code; reset_device reads them from custody.
586                    let connected = coven_storage::recorded_config(&directory)
587                        .map_err(|error| SettingsError::Corrupt(Box::new(error)))?
588                        .is_some();
589                    return Err(match identity?.read()? {
590                        None if connected => CovenError::MemberKeysMissing { store },
591                        _ => CovenError::DeviceKeyMissing { store },
592                    });
593                }
594                // Before the database opens, so its WAL and shared-memory files
595                // are made with the class (E1).
596                coven_foundation::files::settle_files(&directory, &lock).map_err(|error| {
597                    match error {
598                        coven_foundation::files::FileError::DeviceLocked { path } => {
599                            CovenError::DeviceLocked { path }
600                        }
601                        error => DbError::from(error).into(),
602                    }
603                })?;
604                // A join that published and stopped before deleting its code (§13.4).
605                if !directory.is_unpublished().map_err(DbError::from)? {
606                    keychain.delete_invite_code()?;
607                }
608                Ok(OpeningStore { database, lock, builder, directory, keychain })
609            })
610            .await
611            .unwrap_or_else(|shutdown| Err(shutdown.into()));
612        closing.disarm();
613        result
614    }
615
616    fn owners(
617        mut self,
618        directory: StoreDir,
619        keychain: Arc<StoreKeychain>,
620        continuing: Continuing,
621    ) -> CovenResult<OpeningOwners> {
622        let clients = self.oauth_exchange();
623        let oauth = Self::oauth_flow(clients.clone(), self.presenter.clone());
624        let settings = directory.settings()?;
625        let person = Arc::new(PersonEntries::new(self.keychain()?, settings.id));
626        let (state, storage, credentials) = match continuing {
627            Continuing::Fresh => (Arc::new(coven_sync::SequenceState::new()), None, None),
628            Continuing::Loaded { state, storage, credentials } => {
629                (state, Some(storage), credentials)
630            }
631        };
632        // A restore keeps no sign-in in device credentials: the absence is
633        // designed, not a default over a failure.
634        let credentials = credentials.unwrap_or_else(|| {
635            Arc::new(coven_sync::DeviceCredentials::new(
636                keychain.clone(),
637                person.clone(),
638                directory.clone(),
639                clients.clone(),
640            ))
641        });
642        let connector = self.connector();
643        let keys = Self::make_session(self.keys, &directory, settings.id, keychain.clone())?;
644        let identity =
645            Self::make_session(self.identity, &directory, settings.id, keychain.clone())?;
646        let network = coven_foundation::network_watch::NetworkWatch::start(
647            &self.network,
648            self.directories.bugs(),
649        )
650        .map_err(CovenError::NetworkWatch)?;
651        Ok(OpeningOwners {
652            directory,
653            custody: StoreCustody::new(keys.clone(), identity.clone(), keychain.clone()),
654            keychain,
655            person,
656            credentials,
657            connector,
658            oauth,
659            clients,
660            authentication: self.authentication,
661            transfers: self.transfers,
662            network,
663            device: settings.device_id,
664            keys,
665            identity,
666            state,
667            clock: self.clock,
668            ids: self.ids,
669            storage,
670            #[cfg(test)]
671            interrupt_commit: self.interrupt_commit,
672        })
673    }
674
675    fn make_session<T: KeyMaterial>(
676        chosen: SessionSource<T>,
677        directory: &StoreDir,
678        id: StoreId,
679        keychain: Arc<StoreKeychain>,
680    ) -> Result<Arc<KeySession<T>>, KeyError> {
681        let custody = match chosen {
682            SessionSource::Session(session) => return Ok(session),
683            SessionSource::Configured(custody) => custody,
684        };
685        let custody: Arc<dyn Custody<T>> = match custody {
686            CustodyChoice::Keyring => Arc::new(KeyringCustody::new(keychain)),
687            CustodyChoice::Passphrase(secret) => {
688                Arc::new(PassphraseCustody::new(secret, directory.owned_file(T::FILE), id))
689            }
690            CustodyChoice::InMemory => Arc::new(InMemoryCustody::empty()),
691            CustodyChoice::Custom(custody) => custody,
692        };
693        Ok(Arc::new(KeySession::open(custody)?))
694    }
695
696    /// Make the next replacement stop right after its swap, undoing nothing.
697    #[cfg(test)]
698    pub(crate) fn interrupt_after_swap(mut self) -> Self {
699        self.interrupt_after_swap = true;
700        self
701    }
702
703    /// Have SQLite refuse to close the store this builder opens.
704    #[cfg(test)]
705    pub(crate) fn refuse_database_close(mut self) -> Self {
706        self.refuse_database_close = true;
707        self
708    }
709
710    /// Have the platform refuse this store's network watch with `error`.
711    #[cfg(test)]
712    pub(crate) fn refuse_network_watch(mut self, error: NetworkWatchError) -> Self {
713        self.network = coven_foundation::network_watch::NetworkSource::Refused(error);
714        self
715    }
716
717    /// Make the insert of the next reset's device-reset notice fail.
718    #[cfg(test)]
719    pub(crate) fn fail_device_reset_notice(mut self) -> Self {
720        self.fail_device_reset_notice = true;
721        self
722    }
723
724    /// Make the next replacement stop right after its fresh device
725    /// registered, before its swap, undoing nothing.
726    #[cfg(test)]
727    pub(crate) fn interrupt_after_registering(mut self) -> Self {
728        self.interrupt_after_registering = true;
729        self
730    }
731
732    /// Make the next commit of a new installation's custody stop right after
733    /// it writes the device id, undoing nothing.
734    #[cfg(test)]
735    pub(crate) fn interrupt_commit(mut self) -> Self {
736        self.interrupt_commit = true;
737        self
738    }
739
740    /// Keep time by `recorder`, which records every value a bootstrap
741    /// through this builder reports as it loads, values a host reading the
742    /// loading watch could miss.
743    #[cfg(test)]
744    pub(crate) fn record_loading(
745        mut self,
746        recorder: Arc<RecordingClock<coven_database::LoadStep>>,
747    ) -> Self {
748        self.clock = recorder.clone();
749        self.loading_recorder = Some(recorder);
750        self
751    }
752}
753
754/// Retains the graph while its asynchronous database opening is in progress.
755struct OpeningStore {
756    database: DatabaseBuilder,
757    lock: coven_foundation::files::StoreLock,
758    builder: CovenBuilder,
759    directory: StoreDir,
760    keychain: Arc<StoreKeychain>,
761}
762
763struct OpeningOwners {
764    transfers: Arc<coven_storage::TransferBudget>,
765    /// The store's watch of the device's network, from opening to closing.
766    network: coven_foundation::network_watch::NetworkWatch,
767    keychain: Arc<StoreKeychain>,
768    /// The person's entries, deleted when they leave the store (§14.1).
769    person: Arc<PersonEntries>,
770    credentials: Arc<coven_sync::DeviceCredentials>,
771    connector: Arc<dyn StorageConnector>,
772    oauth: Option<OAuthFlow>,
773    clients: Option<OAuthExchange>,
774    authentication: Option<Arc<coven_sync::SessionSignIn>>,
775    device: DeviceId,
776    directory: StoreDir,
777    custody: StoreCustody,
778    keys: Arc<KeySession<StoreKeyring>>,
779    identity: Arc<KeySession<MemberIdentity>>,
780    /// This installation's sequence state, shared by the handle's transfers.
781    state: Arc<coven_sync::SequenceState>,
782    clock: ClockRef,
783    ids: IdSourceRef,
784    /// A loaded installation's connection, made through `state`.
785    storage: Option<Arc<coven_storage::StorageConnection>>,
786    /// Stop the commit right after it writes the device id.
787    #[cfg(test)]
788    interrupt_commit: bool,
789}
790
791impl OpeningStore {
792    async fn open(self) -> CovenResult<CovenHandle> {
793        let database = self.database.open_locked(self.lock).await?;
794        let state = database.store_state().await?;
795        if let Some(identity) = &state.store {
796            check_store_settings(identity, &self.directory)?;
797        }
798        let waits = Arc::new(
799            coven_sync::RetryWaits::open(database.clone(), self.builder.clock.clone()).await?,
800        );
801        let bugs = database.bugs().clone();
802        let owners = crate::task::blocking_completing("open the store's owners", bugs, move || {
803            let owners = self.builder.owners(self.directory, self.keychain, Continuing::Fresh)?;
804            // The saved restore code's name catches up with this store's
805            // when the store's is newer, and an unreadable entry is replaced
806            // with this device's current code (§13.1). A store never set up
807            // has no saved code.
808            if let Some(identity) = &state.store {
809                coven_sync::follow_name(
810                    &owners.person,
811                    &coven_sync::SavedName::of(identity),
812                    || {
813                        let location = coven_storage::recorded_config(&owners.directory)
814                            .map_err(|error| CovenError::from(SyncError::from(error)))?;
815                        coven_sync::replacement_code(
816                            &state,
817                            owners.identity.read()?.as_ref(),
818                            location.as_ref(),
819                        )
820                        .map_err(CovenError::from)
821                    },
822                )?;
823                // Where each write chooses the cloud backup, as Block Store's
824                // does, the entries are stored again, so a screen lock added
825                // since they were written lets them reach it (§13.1).
826                owners.person.store_again()?;
827            }
828            Ok::<_, CovenError>(owners)
829        })
830        .await??;
831        let location = owners.credentials.location().map_err(CovenError::from)?;
832        let configured = location.is_connected();
833        // A setup a crash cut is undone, or left for the first connection
834        // to finish, before the handle exists (§11.6).
835        let finishing =
836            Box::pin(crate::storage::settle_opened(&database, &owners.keychain, &owners.keys))
837                .await
838                .map_err(CovenError::from)?;
839        // Boxed, so `open`'s future stays as small as an app's stack needs
840        // (`application_api_tests.rs`).
841        Box::pin(crate::storage::record_opened(&database, &owners.clock, location, None))
842            .await
843            .map_err(CovenError::from)?;
844        Ok(owners.into_handle(database, waits, configured, finishing))
845    }
846}
847
848/// Why the store opens.
849#[derive(Clone, Copy, PartialEq, Eq)]
850enum Opening {
851    /// For the app's use.
852    ForUse,
853    /// To finish this device's recorded deletion of it.
854    ToDelete,
855}
856
857/// A store this device is deleting and leaving opens only to finish that
858/// (E1, §11.6).
859pub(crate) fn refuse_unfinished_deletion(
860    directory: &impl StoreDirAccess,
861    store: StoreId,
862) -> CovenResult<()> {
863    match directory.unfinished_deletion().map_err(DbError::from)? {
864        Some(_) => Err(StoreLockError::DeletionUnfinished(store).into()),
865        None => Ok(()),
866    }
867}
868
869/// An interrupted replacement resumes only through the call that began it
870/// (§19.3).
871fn refuse_unfinished_replacement(
872    directory: &impl StoreDirAccess,
873    store: StoreId,
874) -> CovenResult<()> {
875    match directory.unfinished_replacement()? {
876        Some(record) => Err(StoreLockError::ReplacementUnfinished { store, by: record.by }.into()),
877        None => Ok(()),
878    }
879}
880
881/// The store's identity names the store its directory's settings were
882/// written for.
883fn check_store_settings(
884    identity: &coven_database::StoreIdentity,
885    directory: &impl StoreDirAccess,
886) -> CovenResult<()> {
887    Ok(identity.check_settings(&directory.settings()?)?)
888}
889
890/// The device-custody credentials of `store`'s installation.
891pub(crate) fn device_credentials(
892    keychain: Arc<Keychain>,
893    store: StoreId,
894    directory: StoreDir,
895    oauth: Option<OAuthExchange>,
896) -> Arc<coven_sync::DeviceCredentials> {
897    Arc::new(coven_sync::DeviceCredentials::new(
898        Arc::new(StoreKeychain::new(keychain.clone(), store)),
899        Arc::new(PersonEntries::new(keychain, store)),
900        directory,
901        oauth,
902    ))
903}
904
905impl OpeningOwners {
906    /// Construction after publication performs no fallible work and cannot suspend.
907    fn into_handle(
908        self,
909        database: coven_database::Database,
910        waits: Arc<coven_sync::RetryWaits>,
911        configured: bool,
912        setup_finishing: bool,
913    ) -> CovenHandle {
914        let files = coven_sync::Files::new(
915            coven_database::FileDatabase::new(database.clone()),
916            self.directory.clone(),
917            self.storage.clone().map(|storage| storage as Arc<dyn coven_storage::Storage>),
918            self.state.clone(),
919            waits.clone(),
920            self.ids.clone(),
921            self.transfers.clone(),
922            self.clock.clone(),
923            self.network.view(),
924        );
925        let network =
926            coven_sync::NetworkFollow::start(self.network, waits.clone(), database.clone());
927        let steps = Arc::new(coven_sync::OperationSteps::new(
928            self.device,
929            self.identity.clone(),
930            self.ids.clone(),
931            self.clock.clone(),
932            self.directory.clone(),
933            self.credentials.clone(),
934            waits.clone(),
935            self.state.clone(),
936        ));
937        let codes = coven_sync::RestoreCodes::new(
938            self.directory.clone(),
939            database.clone(),
940            self.identity.clone(),
941            self.credentials.clone(),
942        );
943        let storage = crate::storage::StorageConnections::new(
944            database.clone(),
945            files.clone(),
946            self.identity,
947            self.person,
948            self.credentials,
949            codes.clone(),
950            self.state.clone(),
951            self.directory.clone(),
952            waits.clone(),
953            self.transfers,
954            self.keys,
955            self.connector,
956            self.oauth,
957            self.clients,
958            self.authentication,
959            self.storage,
960            configured,
961            self.keychain.clone(),
962            setup_finishing,
963            self.device,
964            self.ids,
965            self.clock,
966        );
967        CovenHandle::new(
968            database,
969            self.custody,
970            files,
971            steps,
972            waits,
973            self.state,
974            codes,
975            Arc::new(network),
976            Arc::new(storage),
977            self.directory,
978            self.keychain,
979            self.device,
980        )
981    }
982}
983
984#[cfg(test)]
985#[path = "builder_tests.rs"]
986mod tests;
987
988#[path = "builder_creation.rs"]
989mod creation;
990
991#[path = "builder_read_only.rs"]
992mod read_only;
993
994#[path = "bootstrap.rs"]
995mod bootstrap;
996pub use bootstrap::{
997    join_with_invite, restore_from_code, restore_from_saved_code, BootstrapError, BootstrapStatus,
998    JoinControl,
999};