Skip to main content

coven/
bootstrap.rs

1//! New-device composition roots. Sync owns admission and loading; foundation
2//! owns unpublished directories and crypto owns final key custody (§13, E10).
3
4use super::*;
5#[cfg(test)]
6use coven_crypto::custody::Keychain;
7use coven_crypto::custody::{InMemoryCustody, PersonEntries, StoreKeychain};
8use coven_database::Database;
9use coven_format::codes::{InviteCode, RestoreCode};
10use coven_foundation::files::StoreDirAccess;
11use coven_foundation::files::{
12    BootstrapDirectoryError, BootstrapStore, ReplacedBy, ReplacementStore,
13};
14use coven_storage::{
15    ConnectionCredentials, InviteStorage, RestoreStorage, StorageCredentials, StorageFailure,
16};
17use coven_sync::{HeldIntervals, Parks, SequenceSync, SyncOften};
18use std::{sync::Arc, time::Duration};
19use tokio::sync::watch;
20
21#[path = "bootstrap_authentication.rs"]
22mod authentication;
23use authentication::{account_sign_in, join_sign_in, provider_auth, SignInCustody};
24use coven_sync::SessionSignIn;
25
26#[path = "bootstrap_commit.rs"]
27mod commit;
28
29#[path = "bootstrap_join.rs"]
30mod join;
31
32#[path = "bootstrap_admitted.rs"]
33mod admitted;
34
35#[path = "bootstrap_replace.rs"]
36mod replace;
37pub(super) use replace::{restore_device, resume_replacement};
38
39#[path = "bootstrap_reset.rs"]
40mod reset;
41pub(super) use reset::reset_device;
42
43#[path = "bootstrap_restore.rs"]
44mod restore;
45#[cfg(test)]
46use restore::keychain_code;
47pub use restore::{restore_from_code, restore_from_saved_code};
48pub(super) use restore::{resume_restore, unfinished_restores};
49
50#[path = "restore_credentials.rs"]
51mod credentials;
52use credentials::restore_connection;
53
54#[path = "bootstrap_unfinished.rs"]
55mod unfinished;
56use unfinished::{begin_installation, kept, Kept};
57
58#[path = "bootstrap_end.rs"]
59mod end;
60use end::{record_ending, remove_unfinished};
61
62#[path = "bootstrap_error.rs"]
63mod error;
64pub use error::BootstrapError;
65
66#[path = "bootstrap_status.rs"]
67mod status;
68pub use status::BootstrapStatus;
69use status::{loading, open_installation};
70
71/// Accept provider access, retain one signed request across restarts, and wait
72/// for approval. Returns the open store, or None when declined or expired.
73/// `joiner` is the new person with the name they typed, or a recovery, as the
74/// invite's `InviteInfo::kind` says; the other kind fails with
75/// `CodeError::WrongJoiner` before anything changes. Dropping the future
76/// before the join finds its admission preserves the unfinished join for a
77/// subsequent call with the same invite, device name and person, or
78/// `resume_join`. On Dropbox and
79/// OneDrive the builder's sign-in is kept on the device beside the saved code
80/// until the join ends, and a later call without one uses it (§13.2).
81/// `control` cancels the join or asks for a check for approval at once.
82/// Cancelling before the request may have been sent removes the unpublished
83/// installation; after, the call ends with `BootstrapError::JoinRequestSent`
84/// and keeps it, as dropping does. Once the join finds its admission it
85/// publishes the store, a cancel notwithstanding, then loads it, reporting
86/// `Loading`; a failure while it loads returns `BootstrapError::Published`
87/// with the open handle (§13.2). A store whose unfinished installation
88/// holds a restore's record is refused with `StoreLockError::BootstrapPending`
89/// before anything changes (§13.4).
90pub async fn join_with_invite(
91    builder: CovenBuilder,
92    code: &str,
93    joiner: Joiner,
94    device_name: &str,
95    on_status: impl Fn(BootstrapStatus),
96    control: &JoinControl,
97) -> Result<Option<CovenHandle>, BootstrapError> {
98    // Whether a cancel may still remove the join is known only once its
99    // saved request is read, so the call checks after preparing it.
100    let cancel = control.cancel.subscribe();
101    Box::pin(bootstrap_device(
102        builder,
103        BootstrapRequest::Join {
104            code: coven_sync::read_invite_code(code)?,
105            terms: RequestTerms::Given { device_name: device_name.into(), joiner },
106            held: control.often.subscribe(),
107            parks: control.parks.clone(),
108        },
109        on_status,
110        &cancel,
111    ))
112    .await
113}
114
115/// The joins under way on this device, ordered by store id: each unfinished
116/// first installation with a saved invite code, as `decode_code_info` reads
117/// it (`CovenBuilder::unfinished_joins`).
118pub(super) async fn unfinished_joins(
119    builder: &CovenBuilder,
120) -> Result<Vec<InviteInfo>, BootstrapError> {
121    let keychain = builder.keychain()?;
122    let mut joins = Vec::new();
123    for store in builder
124        .directories
125        .unfinished_first_installations()
126        .await
127        .map_err(BootstrapDirectoryError::from)?
128    {
129        // An unfinished restore saves no code; an installation that keeps
130        // nothing is reclaimed.
131        let entries = StoreKeychain::new(keychain.clone(), store);
132        let Kept::Join(text) = kept(builder, &entries, store).await? else {
133            continue;
134        };
135        let info = match coven_sync::decode_code_info(text.as_str())? {
136            CodeInfo::Invite(info) => info,
137            CodeInfo::Restore(_) => {
138                return Err(CodeError::WrongKind {
139                    expected: CodeKind::Invite,
140                    actual: CodeKind::Restore,
141                }
142                .into())
143            }
144        };
145        CodeError::require_store(store, info.metadata.store_id)?;
146        joins.push(info);
147    }
148    Ok(joins)
149}
150
151/// Continue an unfinished join from its saved code and the device name and
152/// person its saved request carries (`CovenBuilder::resume_join`).
153pub(super) async fn resume_join(
154    builder: CovenBuilder,
155    store: StoreId,
156    on_status: impl Fn(BootstrapStatus),
157    control: &JoinControl,
158) -> Result<Option<CovenHandle>, BootstrapError> {
159    let code = saved_invite_code(&StoreKeychain::new(builder.keychain()?, store), store)?
160        .ok_or(BootstrapError::NoUnfinishedJoin(store))?;
161    let cancel = control.cancel.subscribe();
162    Box::pin(bootstrap_device(
163        builder,
164        BootstrapRequest::Join {
165            code,
166            terms: RequestTerms::Saved,
167            held: control.often.subscribe(),
168            parks: control.parks.clone(),
169        },
170        on_status,
171        &cancel,
172    ))
173    .await
174}
175
176/// The invite code a join under way saved for `store`.
177fn saved_invite_code(
178    keychain: &StoreKeychain,
179    store: StoreId,
180) -> Result<Option<InviteCode>, BootstrapError> {
181    let Some(text) = keychain.invite_code()? else {
182        return Ok(None);
183    };
184    let code = coven_sync::read_invite_code(text.as_str())?;
185    CodeError::require_store(store, code.store)?;
186    Ok(Some(code))
187}
188
189/// The app's hold on a running join: cancel it, or ask it to check for
190/// approval often while the app shows the wait.
191pub struct JoinControl {
192    cancel: watch::Sender<bool>,
193    often: SyncOften,
194    /// Where the join's wait parks, for tests.
195    parks: Parks,
196}
197
198impl JoinControl {
199    /// A control no join has used yet.
200    pub fn new() -> Self {
201        Self { cancel: watch::channel(false).0, often: SyncOften::new(), parks: Parks::new() }
202    }
203
204    /// Cancel the join this control was passed to.
205    pub fn cancel(&self) {
206        self.cancel.send_replace(true);
207    }
208
209    /// Have the join check for approval at most `interval` after its last
210    /// check ended, until the guard drops; a check already that overdue runs
211    /// at once. With several guards held, the shortest interval wins. Checks
212    /// that find the request waiting still count toward the backoff, and a
213    /// provider cooldown still holds every check (§13.2).
214    pub fn check_often(&self, interval: Duration) -> SyncOftenGuard {
215        self.often.hold(interval)
216    }
217
218    /// Wait until the join's wait parks in a poll begun after this call,
219    /// having seen all the caller did before.
220    #[cfg(any(test, feature = "test-utils"))]
221    pub fn parked_from_now(&self) -> impl std::future::Future<Output = ()> + Send + 'static {
222        self.parks.parked_from_now()
223    }
224}
225
226enum BootstrapRequest {
227    Restore {
228        code: RestoreCode,
229        data: ConnectionCredentials,
230        /// Where an OAuth connection refreshes: the session, or the store's
231        /// device credentials a recorded restore kept its sign-in in.
232        sign_in: SignInCustody,
233        name: String,
234    },
235    Join {
236        code: InviteCode,
237        terms: RequestTerms,
238        /// The intervals the app asks the join to check at.
239        held: HeldIntervals,
240        parks: Parks,
241    },
242}
243
244/// The device name and person a join's request carries.
245enum RequestTerms {
246    /// What the app passed, which a saved request must repeat.
247    Given { device_name: String, joiner: Joiner },
248    /// What the saved request carries: the join resumes from it.
249    Saved,
250}
251
252/// Its state runs to tens of KiB, so the public calls await it boxed and the
253/// futures apps hold stay small (application_api_tests bounds them).
254async fn bootstrap_device(
255    mut builder: CovenBuilder,
256    request: BootstrapRequest,
257    status: impl Fn(BootstrapStatus),
258    cancel: &watch::Receiver<bool>,
259) -> Result<Option<CovenHandle>, BootstrapError> {
260    if matches!(request, BootstrapRequest::Restore { .. }) {
261        check_cancel(cancel)?;
262    }
263    let keychain = builder.keychain()?;
264    let id = match &request {
265        BootstrapRequest::Restore { code, .. } => code.store,
266        BootstrapRequest::Join { code, .. } => code.store,
267    };
268    let device_entries = StoreKeychain::new(keychain.clone(), id);
269    let clock = builder.clock.clone();
270    let started = clock.elapsed();
271    #[cfg(test)]
272    let recorder = builder.loading_recorder.clone();
273    // A deletion of the store a cut call left recorded still holds its id
274    // (§19.3): it is finished first.
275    if builder.directories.deletion_recorded(id).await.map_err(BootstrapDirectoryError::from)? {
276        let directory = builder.directories.store_dir(&id);
277        let keys = StoreKeychain::new(keychain.clone(), id);
278        crate::coven::finish_deletion::<BootstrapError>(&directory, keys).await?;
279    }
280    let pending = begin_installation(&builder, id, &device_entries).await?;
281    // The store's one unfinished installation lists one call. A restore on
282    // a waiting join's installation, or a join on a recorded restore's,
283    // would remove the other's listing when it ends without the store, with
284    // its request or registration possibly in storage (§13.4).
285    let other = match &request {
286        BootstrapRequest::Restore { .. } => device_entries.invite_code()?.is_some(),
287        BootstrapRequest::Join { .. } => device_entries.restore_record()?.is_some(),
288    };
289    if other {
290        return Err(BootstrapDirectoryError::from(StoreLockError::BootstrapPending(id)).into());
291    }
292    // A join that decided to end and stopped part way through deleting
293    // what it kept finishes that, without connecting: the sign-in it would
294    // connect with may be gone already (§13.4).
295    if let BootstrapRequest::Join { code, .. } = &request {
296        if coven_sync::ended_for(&pending.directory(), code)? {
297            return remove_unfinished(&device_entries, &pending, Ok(None));
298        }
299    }
300    let result =
301        prepare_and_load(&mut builder, Installing::New(&pending), &request, None, &status, cancel)
302            .await;
303    match result {
304        Ok(Some(Loaded {
305            code,
306            location,
307            provider,
308            credentials,
309            sign_in,
310            ring,
311            database,
312            storage,
313            state,
314            waits,
315            invitee,
316        })) => {
317            let requests = storage.requests();
318            status(BootstrapStatus::Committing);
319            // The saved code's name, read before the final steps, which no
320            // await separates; so is the status the published store opens
321            // with (E5). A join publishes before it loads, with the name its
322            // invite carries (§13.2).
323            let name = match &request {
324                BootstrapRequest::Join { code, .. } => {
325                    commit::joined_name(&database, code, invitee).await
326                }
327                BootstrapRequest::Restore { .. } => commit::loaded_name(&database).await,
328            };
329            let name = match name {
330                Ok(name) => crate::storage::record_opened(
331                    &database,
332                    &clock,
333                    coven_database::RecordedLocation::Connected(provider),
334                    Some(clock.now()),
335                )
336                .await
337                .map(|()| name)
338                .map_err(BootstrapError::from),
339                Err(error) => Err(error),
340            };
341            let result = match name.and_then(|name| {
342                // A loaded restore registered its device: a cancel can't take
343                // it back, and ends the call (§13.1). A join that found its
344                // admission publishes, cancelled or not (§13.2).
345                if matches!(request, BootstrapRequest::Restore { .. })
346                    && check_cancel(cancel).is_err()
347                {
348                    return Err(BootstrapError::RestoreRegistered);
349                }
350                builder.authentication.take();
351                let credentials = match &sign_in {
352                    // A join's kept sign-in is already this store's device
353                    // credentials: the handle refreshes through the same
354                    // custody as the connection it continues.
355                    SignInCustody::Device { credentials, .. } => Some(credentials.clone()),
356                    _ => None,
357                };
358                let owners = builder.owners(
359                    pending.directory(),
360                    Arc::new(StoreKeychain::new(keychain, id)),
361                    crate::builder::Continuing::Loaded { state, storage, credentials },
362                )?;
363                Ok((owners, name))
364            }) {
365                // No cancellable await separates final custody, publication
366                // and the handle.
367                Ok((owners, name)) => {
368                    let publishing = clock.elapsed();
369                    let published = commit::publish_bootstrap(
370                        &pending,
371                        code,
372                        location,
373                        name,
374                        credentials,
375                        sign_in,
376                        ring,
377                        owners,
378                        database.clone(),
379                        waits,
380                    )
381                    .await;
382                    tracing::info!(
383                        published = published.is_ok(),
384                        time = ?(clock.elapsed() - publishing),
385                        "committed keys and credentials and published the store"
386                    );
387                    tracing::info!(
388                        restore = matches!(request, BootstrapRequest::Restore { .. }),
389                        published = published.is_ok(),
390                        time = ?(clock.elapsed() - started),
391                        %requests,
392                        "ended the new installation's bootstrap"
393                    );
394                    published
395                }
396                Err(error) => Err(error),
397            };
398            // A published store no longer needs what its join or restore
399            // kept to finish it.
400            let result = match &request {
401                BootstrapRequest::Join { .. } => {
402                    forget_kept(result, || device_entries.delete_invite_code())
403                }
404                BootstrapRequest::Restore { .. } => {
405                    forget_kept(result, || device_entries.delete_restore_record())
406                }
407            };
408            match result {
409                Ok(handle) => match &request {
410                    BootstrapRequest::Join { .. } => {
411                        let (progress, watch) = watch::channel(None);
412                        #[cfg(test)]
413                        if let Some(recorder) = &recorder {
414                            recorder.watch(watch.clone());
415                        }
416                        admitted::load_joined(handle, &status, &clock, progress, watch)
417                            .await
418                            .map(Some)
419                    }
420                    BootstrapRequest::Restore { .. } => Ok(Some(handle)),
421                },
422                Err(error @ BootstrapError::Published { .. }) => Err(error),
423                // A loaded join or restore is past cancelling: it stays for
424                // the next call.
425                Err(error) => combine(Err(error), close_database(&database).await),
426            }
427        }
428        // Only a join ends without a store: declined or expired.
429        Ok(None) => match record_ending(&request, &pending) {
430            Ok(()) => remove_unfinished(&device_entries, &pending, Ok(None)),
431            Err(error) => Err(error),
432        },
433        Err(error) => match &request {
434            BootstrapRequest::Restore { .. } => {
435                restore::ended(&builder, &pending, &device_entries, error).await
436            }
437            BootstrapRequest::Join { .. } if matches!(error, BootstrapError::Cancelled) => {
438                match record_ending(&request, &pending) {
439                    Ok(()) => remove_unfinished(&device_entries, &pending, Err(error)),
440                    Err(recording) => combine(Err(error), Err(recording)),
441                }
442            }
443            // Before its admission nothing ends a join but its outcome: it
444            // stays listed for the next call (§13.2).
445            BootstrapRequest::Join { .. } => Err(error),
446        },
447    }
448}
449
450/// A published join no longer needs its saved code, nor a published restore
451/// its record: `delete` deletes it. Failing to is reported with the open
452/// handle, as other failures after publication are.
453fn forget_kept(
454    result: Result<CovenHandle, BootstrapError>,
455    delete: impl FnOnce() -> Result<(), KeyError>,
456) -> Result<CovenHandle, BootstrapError> {
457    let (handle, published) = match result {
458        Ok(handle) => (handle, None),
459        Err(BootstrapError::Published { handle, source }) => (handle, Some(*source)),
460        Err(error) => return Err(error),
461    };
462    let deleted = delete().map_err(BootstrapError::from);
463    match (published.map_or(Ok(()), Err), deleted) {
464        (Ok(()), Ok(())) => Ok(handle),
465        (failed, deleted) => Err(BootstrapError::Published {
466            handle,
467            source: Box::new(combine(failed, deleted).expect_err("a failure")),
468        }),
469    }
470}
471
472/// Where a new installation loads: a new device's unpublished store, or the
473/// replacement of this device's installation (§19.3).
474#[derive(Clone, Copy)]
475enum Installing<'a> {
476    New(&'a BootstrapStore),
477    Replacing(&'a ReplacementStore),
478}
479
480impl Installing<'_> {
481    fn directory(&self) -> StoreDir {
482        match self {
483            Self::New(pending) => pending.directory(),
484            Self::Replacing(replacement) => replacement.directory(),
485        }
486    }
487}
488
489enum Admission {
490    Restoring,
491    Joining(join::Joining),
492}
493
494impl Admission {
495    /// Once a join's request may have been sent, cancelling can't take it
496    /// back: approval could land after its keys were thrown away, leaving a
497    /// member no device can act as (§13.2). The join stays unfinished.
498    fn cancellable<T>(&self, result: Result<T, BootstrapError>) -> Result<T, BootstrapError> {
499        match (result, self) {
500            (Err(BootstrapError::Cancelled), Self::Joining(joining)) if joining.sent() => {
501                Err(BootstrapError::JoinRequestSent)
502            }
503            (result, _) => result,
504        }
505    }
506
507    /// Whether a join may have accepted the share without sending its
508    /// request.
509    fn accepted(&self) -> bool {
510        matches!(self, Self::Joining(joining) if joining.accepted())
511    }
512
513    /// A join cancelled before sending takes back the acceptance it recorded
514    /// before it is removed. An undo that fails fails the cancel, and the
515    /// join stays listed for another (§13.2).
516    async fn leave_cancelled<T>(
517        &self,
518        sync: &SequenceSync,
519        result: Result<T, BootstrapError>,
520    ) -> Result<T, BootstrapError> {
521        match (result, self) {
522            (Err(BootstrapError::Cancelled), Self::Joining(joining)) if joining.accepted() => {
523                combine(Err(BootstrapError::Cancelled), joining.leave(sync).await)
524            }
525            (result, _) => result,
526        }
527    }
528}
529
530/// What a new installation does once what it reads from storage is read.
531enum Finishing {
532    /// A join that found its admission, which registered its device,
533    /// publishes the store, recording `labelled`, the location as the
534    /// joining account names it; the store loads after (§13.2).
535    Publish { labelled: StorageConfig },
536    /// A restore or replacement, loaded, registers its device unless an
537    /// interrupted call's registration is `registered` already, and records
538    /// `access`, the access it reaches storage with (§13.1, §19.3).
539    Register { access: coven_format::MemberAccess, registered: bool, labelled: StorageConfig },
540}
541
542/// A loaded store awaiting publication, with what reaches its storage.
543struct Loaded {
544    code: RestoreCode,
545    /// The location as loading's connection labelled it, which the commit
546    /// records (§20.2).
547    location: StorageConfig,
548    /// The provider of the location loaded from.
549    provider: CloudProvider,
550    credentials: StorageCredentials,
551    sign_in: SignInCustody,
552    ring: StoreKeyring,
553    database: Database,
554    storage: Arc<coven_storage::StorageConnection>,
555    /// The sequence state the handle continues, which gates the connection.
556    state: Arc<coven_sync::SequenceState>,
557    waits: Arc<coven_sync::RetryWaits>,
558    /// The new person's name a join's request carried, for the store it
559    /// publishes to keep; none for a restore and for a recovery.
560    invitee: Option<String>,
561}
562
563/// Load the store into `pending` and register its device. A join loads
564/// nothing here: once it finds its admission, which registered its device,
565/// it returns the store to publish, which loads after (§13.2). A replacement
566/// runs the app's `prepare` step on its loaded installation, then keeps what
567/// finishing needs and records itself, before it registers (§19.3).
568async fn prepare_and_load(
569    builder: &mut CovenBuilder,
570    pending: Installing<'_>,
571    request: &BootstrapRequest,
572    prepare: Option<replace::Prepare>,
573    status: &impl Fn(BootstrapStatus),
574    cancel: &watch::Receiver<bool>,
575) -> Result<Option<Loaded>, BootstrapError> {
576    let directory = pending.directory();
577    let (member, mut data, mut sign_in, device_name, mut admission) = match request {
578        BootstrapRequest::Restore { code, data, sign_in, name } => (
579            MemberIdentity::new(code.member, code.member_keys.clone()),
580            data.clone(),
581            sign_in.clone(),
582            name.clone(),
583            Admission::Restoring,
584        ),
585        BootstrapRequest::Join { code, terms, held, parks } => {
586            let (joining, data, sign_in) = join::prepare_request(
587                builder,
588                &directory,
589                code,
590                terms,
591                held.clone(),
592                parks.clone(),
593            )?;
594            (
595                joining.identity(),
596                data,
597                sign_in,
598                joining.device_name().to_owned(),
599                Admission::Joining(joining),
600            )
601        }
602    };
603    // A join that may have accepted the share connects even when cancelled
604    // already, so its cancel can take the acceptance back (§13.2).
605    if !admission.accepted() {
606        admission.cancellable(check_cancel(cancel))?;
607    }
608    // A restore records itself before it registers: a new device's (§13.1),
609    // and a replacement of this device's installation (§19.3).
610    let recording = match (request, pending) {
611        (BootstrapRequest::Restore { code, name, .. }, Installing::New(_)) => {
612            Some(restore::Recording::new(builder, code.store, name)?)
613        }
614        (BootstrapRequest::Restore { code, name, .. }, Installing::Replacing(_)) => {
615            Some(restore::Recording::replacement(builder, code.store, name)?)
616        }
617        _ => None,
618    };
619    let mut prepare = prepare;
620    let provider = data.location.provider();
621    let settings = directory.settings().map_err(CovenError::from)?;
622    let state = Arc::new(coven_sync::SequenceState::new());
623    let ring = Arc::new(KeySession::open(Arc::new(InMemoryCustody::<StoreKeyring>::empty()))?);
624    let identity = Arc::new(KeySession::open(Arc::new(InMemoryCustody::new(member.clone())))?);
625    // A replacement registers the fresh id as the old one's replacement.
626    let replacement = match pending {
627        Installing::Replacing(replacement) => Some(replacement),
628        Installing::New(_) => None,
629    };
630    // The database opens before connecting: its persisted waits and provider
631    // cooldown hold the joiner's polls and every request across restarts (§6.7).
632    let clock = builder.clock.clone();
633    let database = open_installation(builder.database(directory.clone())?, status).await?;
634    status(BootstrapStatus::Connecting);
635    let connected = async {
636        let connecting = clock.elapsed();
637        let waits = Arc::new(
638            coven_sync::RetryWaits::open(database.clone(), builder.clock.clone())
639                .await
640                .map_err(CovenError::from)?,
641        );
642        data.location = crate::authentication::label_location(
643            builder.connector().as_ref(),
644            data.location.clone(),
645            provider_auth(&data, &sign_in)?,
646            waits.clone(),
647        )
648        .await
649        .map_err(SyncError::from)?;
650        let storage = builder
651            .connector()
652            .connect(
653                data.location.clone(),
654                provider_auth(&data, &sign_in)?,
655                settings.device_id,
656                state.clone(),
657                waits.clone(),
658                builder.transfers.clone(),
659            )
660            .await
661            .map_err(SyncError::from)?;
662        if matches!(admission, Admission::Restoring) {
663            storage.check_location_claim(settings.id).await.map_err(SyncError::from)?;
664        }
665        tracing::info!(
666            ?provider,
667            time = ?(clock.elapsed() - connecting),
668            requests = %storage.requests(),
669            "connected to storage"
670        );
671        // The loading owner's transfers end with loading; the handle's start
672        // with the handle.
673        let files = coven_sync::Files::new(
674            coven_database::FileDatabase::new(database.clone()),
675            directory.clone(),
676            None,
677            state.clone(),
678            waits.clone(),
679            builder.ids.clone(),
680            builder.transfers.clone(),
681            clock.clone(),
682            coven_foundation::network_watch::NetworkWatch::unwatched().view(),
683        );
684        let sync = SequenceSync::open(
685            database.clone(),
686            storage.clone(),
687            directory.clone(),
688            settings.device_id,
689            ring.clone(),
690            identity,
691            Arc::new(PersonEntries::new(builder.keychain()?, settings.id)),
692            state.clone(),
693            builder.clock.clone(),
694            builder.ids.clone(),
695            waits.clone(),
696            files.clone(),
697        )
698        .await;
699        match sync {
700            Ok(sync) => Ok::<_, BootstrapError>((waits, storage, files, Arc::new(sync))),
701            Err(error) => {
702                files.close().await;
703                Err(error.into())
704            }
705        }
706    }
707    .await;
708    let (waits, storage, files, sync) = match connected {
709        Ok(connected) => connected,
710        Err(error) => {
711            return combine(sign_in.refused(provider, Err(error)), close_database(&database).await)
712        }
713    };
714    #[cfg(test)]
715    if let Some(recorder) = &builder.loading_recorder {
716        recorder.watch_progress(sync.subscribe_progress());
717    }
718    // The location as the joining account names it, read once the share is
719    // accepted (§20.2): the store a join publishes records it, and no storage
720    // request comes between finding the admission and publishing (§13.2).
721    let mut accepted_label = None;
722    let loading = async {
723        if let Admission::Joining(joining) = &mut admission {
724            if !joining.accept(&sync, status).await? {
725                return Ok(None);
726            }
727            let labelled = storage.labelled_config().await.map_err(SyncError::from)?;
728            accepted_label = Some(labelled.clone());
729            if !joining.admit(&sync, &database, status).await? {
730                return Ok(None);
731            }
732            // Its admission found, which registered its device, the join
733            // publishes the store, which loads after (§13.2): nothing is
734            // left to register, and no access to record.
735            return Ok(Some(Finishing::Publish { labelled }));
736        }
737        // Reading forward opens this member's copies and loads the floor's
738        // snapshots and the slots after them; an interrupted bootstrap's
739        // registration is already applied.
740        let unlocking = (clock.elapsed(), storage.requests());
741        loading(&sync, sync.unlock(), status).await?;
742        tracing::info!(
743            time = ?(clock.elapsed() - unlocking.0),
744            requests = %storage.requests().since(&unlocking.1),
745            "read the store forward with this member's keys"
746        );
747        // The folder's label as this member's account names it (§20.2).
748        let labelled = storage.labelled_config().await.map_err(SyncError::from)?;
749        let registered = database
750            .store_state()
751            .await?
752            .devices
753            .get(&settings.device_id)
754            .is_some_and(|device| device.member == member.member());
755        // A restore checks the access it reaches storage with before it
756        // registers, and records it after, as setup does, so the member's
757        // recorded access is the one in use (§13.1).
758        status(BootstrapStatus::Registering);
759        let access = crate::storage::member_access(&data.credentials, &storage)
760            .await
761            .map_err(SyncError::from)?;
762        sync.check_access(&access).await?;
763        Ok::<_, BootstrapError>(Some(Finishing::Register { access, registered, labelled }))
764    };
765    let mut cancellation = cancel.clone();
766    let loaded = tokio::select! {
767        biased;
768        _ = cancelled(&mut cancellation) => Err(BootstrapError::Cancelled),
769        result = loading => result,
770    };
771    // A join whose keys opened in this call has found its admission, which
772    // registered its device: it publishes the store, cancelled or not, since
773    // nothing is left for a cancel to take back (§13.2).
774    let loaded = match (loaded, &admission, accepted_label) {
775        (Err(BootstrapError::Cancelled), Admission::Joining(_), Some(labelled))
776            if ring.read()?.is_some() =>
777        {
778            Ok(Some(Finishing::Publish { labelled }))
779        }
780        (loaded, ..) => loaded,
781    };
782    let result = match loaded {
783        // A join that found its admission has nothing left to write.
784        Ok(Some(Finishing::Publish { labelled })) => {
785            data.location = labelled;
786            Ok(true)
787        }
788        Ok(Some(Finishing::Register { access, registered, labelled })) => {
789            data.location = labelled;
790            // The work that writes to storage. A cancel stops it only between
791            // its steps: one under way, a registration on its way above all,
792            // runs to its end first (§11.6).
793            let registering = async {
794                if !registered {
795                    check_cancel(cancel)?;
796                    match (replacement, &recording) {
797                        (Some(replacement), Some(recording)) => {
798                            sign_in = replace::record_replacement(
799                                replacement,
800                                recording,
801                                &database,
802                                prepare.take(),
803                                (&member, settings.id, &data),
804                                sign_in.clone(),
805                            )
806                            .await?;
807                        }
808                        (None, Some(recording)) => {
809                            sign_in =
810                                recording.record(&member, settings.id, &data, sign_in.clone())?;
811                        }
812                        _ => {}
813                    }
814                    let replaces =
815                        replacement.map(|replacement| replacement.device_reset().old_device);
816                    let registering = (clock.elapsed(), storage.requests());
817                    sync.register_device(device_name, replaces).await?;
818                    tracing::info!(
819                        time = ?(clock.elapsed() - registering.0),
820                        requests = %storage.requests().since(&registering.1),
821                        "registered this device"
822                    );
823                }
824                let state = database.store_state().await?;
825                if let Some(change) =
826                    crate::storage::access_change(&state, member.member(), access)?
827                {
828                    check_cancel(cancel)?;
829                    sync.administer(change).await?;
830                }
831                // The notice commits before any handle is published (E5).
832                if let Some(replacement) = replacement {
833                    check_cancel(cancel)?;
834                    #[cfg(test)]
835                    if builder.fail_device_reset_notice {
836                        database.test_fail_device_reset().await.map_err(CovenError::from)?;
837                    }
838                    reset::carry_discarded_provider_work(&sync, &database, replacement).await?;
839                }
840                Ok::<_, BootstrapError>(true)
841            };
842            tokio::pin!(registering);
843            let ended = tokio::select! {
844                biased;
845                _ = cancelled(&mut cancellation) => None,
846                result = &mut registering => Some(result),
847            };
848            match ended {
849                Some(result) => result,
850                None => match registering.await {
851                    Err(error) if crate::task::CarriesBug::bug(&error).is_some() => Err(error),
852                    _ => Err(BootstrapError::Cancelled),
853                },
854            }
855        }
856        Ok(None) => Ok(false),
857        Err(error) => Err(error),
858    };
859    let result = admission.leave_cancelled(&sync, admission.cancellable(result)).await;
860    let result = sign_in.refused(provider, result);
861    files.close().await;
862    drop(sync);
863    let result = result.and_then(|loaded| {
864        if !loaded {
865            return Ok(None);
866        }
867        let ring = ring.read()?.ok_or(SyncError::from(StorageFailure::MemberKeysMissing))?;
868        Ok(Some((
869            coven_sync::location_code(settings.id, &member, data.location.clone())?,
870            data.location,
871            data.credentials,
872            ring,
873        )))
874    });
875    let invitee = match &admission {
876        Admission::Joining(joining) => joining.invitee(),
877        Admission::Restoring => None,
878    };
879    match result {
880        Ok(Some((code, location, credentials, ring))) => Ok(Some(Loaded {
881            code,
882            location,
883            provider,
884            credentials,
885            sign_in,
886            ring,
887            database,
888            storage,
889            state,
890            waits,
891            invitee,
892        })),
893        result => {
894            let closed = close_database(&database).await;
895            combine(result.map(|_| None), closed)
896        }
897    }
898}
899
900fn check_cancel(cancel: &watch::Receiver<bool>) -> Result<(), BootstrapError> {
901    if *cancel.borrow() {
902        Err(BootstrapError::Cancelled)
903    } else {
904        Ok(())
905    }
906}
907
908async fn cancelled(cancel: &mut watch::Receiver<bool>) {
909    loop {
910        if *cancel.borrow_and_update() {
911            return;
912        }
913        if cancel.changed().await.is_err() {
914            std::future::pending::<()>().await;
915        }
916    }
917}
918
919/// Close the installation's database; its failure is the bootstrap's.
920async fn close_database(database: &Database) -> Result<(), BootstrapError> {
921    database.close().await.map_err(CovenError::from).map_err(BootstrapError::from)
922}
923
924fn combine<T>(
925    result: Result<T, BootstrapError>,
926    cleanup: Result<(), BootstrapError>,
927) -> Result<T, BootstrapError> {
928    match (result, cleanup) {
929        (result, Ok(())) => result,
930        (Ok(_), Err(error)) => Err(error),
931        (Err(operation), Err(cleanup)) => Err(BootstrapError::Cleanup {
932            operation: Box::new(operation),
933            cleanup: Box::new(cleanup),
934        }),
935    }
936}
937
938/// Every entry under `path` with its contents, sorted, so a test can show
939/// that a refused replacement changed nothing.
940#[cfg(test)]
941fn files_under(path: &std::path::Path) -> Vec<(std::path::PathBuf, Option<Vec<u8>>)> {
942    let mut found = Vec::new();
943    for entry in std::fs::read_dir(path).unwrap() {
944        let entry = entry.unwrap();
945        let relative = std::path::PathBuf::from(entry.file_name());
946        if entry.file_type().unwrap().is_dir() {
947            found.extend(
948                files_under(&entry.path())
949                    .into_iter()
950                    .map(|(inner, bytes)| (relative.join(inner), bytes)),
951            );
952            found.push((relative, None));
953        } else {
954            found.push((relative, Some(std::fs::read(entry.path()).unwrap())));
955        }
956    }
957    found.sort();
958    found
959}
960
961#[cfg(test)]
962#[path = "bootstrap_tests.rs"]
963mod tests;