Skip to main content

coven/
read_handle.rs

1//! The read-only handle: a same-store secondary reader.
2//!
3//! Where [`CovenHandle`](crate::CovenHandle) is the one full handle a host opens to
4//! drive rows, blobs, and sync, [`CovenReadHandle`] is the deliberately narrow
5//! counterpart for a second reader of the *same* store — a separate process (the
6//! macOS File Provider extension) or a second in-process handle — that must read
7//! while another handle holds the writer open.
8//!
9//! It is opened with [`Coven::builder(cfg).open_read_only()`](crate::CovenBuilder::open_read_only)
10//! and exposes reads only: SQL queries against the connection coven owns, and blob
11//! reads (local store, pinned/evictable cache, or a cloud fetch into the cache).
12//! There is no write, sync-manager, migration, or stamp API on it — those are absent
13//! by construction, so a reader cannot mutate the synced state a concurrent writer
14//! owns. Its connection is `SQLITE_OPEN_READONLY`, so even the raw
15//! [`sql_read`](CovenReadHandle::sql_read) closure's `&Connection` refuses DML at
16//! the SQLite layer.
17//!
18//! It takes no store lock: it coexists with the writer that holds the exclusive
19//! `.coven-lock`, and with any number of other read-only opens (a read-only
20//! connection cannot write, so the single-writer lock does not apply to it).
21//! Cross-process reads are safe because the writer opens the db in WAL mode.
22
23use std::sync::Arc;
24
25use crate::blob::cache::BlobCacheError;
26use crate::blob::{BlobRef, RowBlobRef};
27use crate::clock::ClockRef;
28use crate::config::Config;
29use crate::coven::{CovenError, CovenResult};
30use crate::database::Database;
31use crate::encryption::SealError;
32use crate::keys::{DeviceIdentityCustody, MasterKeyCustody, StoreKeys};
33use crate::store_dir::StoreDir;
34use crate::sync::storage::SyncStorage;
35use crate::sync::sync_manager::ConfigProvider;
36
37/// A read-only handle over one coven store, for a same-store secondary reader.
38///
39/// Open it with
40/// [`Coven::builder(cfg).open_read_only()`](crate::CovenBuilder::open_read_only).
41/// Cheap to [`clone`](Clone) — every field is shared (an `Arc` or a `Clone` handle),
42/// so a clone reads the same database and storage as the original.
43///
44/// # What it can do
45///
46/// - **Rows** — read via [`sql_read`](Self::sql_read). The closure receives the
47///   `&Connection` coven owns; because the connection is read-only, any write
48///   statement fails at the SQLite layer.
49/// - **Blobs** — [`read_blob`](Self::read_blob) and
50///   [`open_blob_stream`](Self::open_blob_stream) resolve a blob's locality and serve
51///   it from the local store, the cache, or a cloud fetch into the per-device cache.
52///   [`is_pinned`](Self::is_pinned) reports whether a set is kept offline.
53///
54/// It builds read storage from the current [`Config`] on a cloud miss, exactly as a
55/// home-less full handle does — there is no sync loop to reuse.
56#[derive(Clone)]
57pub struct CovenReadHandle {
58    db: Database,
59    store_dir: StoreDir,
60    config_provider: ConfigProvider,
61    key_service: StoreKeys,
62    key_custody: Arc<dyn MasterKeyCustody>,
63    identity_custody: Arc<dyn DeviceIdentityCustody>,
64    clock: ClockRef,
65    cloudkit_ops: Option<Arc<dyn crate::storage::cloud::cloudkit::CloudKitOps>>,
66}
67
68impl CovenReadHandle {
69    #[allow(clippy::too_many_arguments)]
70    pub(crate) fn new(
71        db: Database,
72        store_dir: StoreDir,
73        config_provider: ConfigProvider,
74        key_service: StoreKeys,
75        key_custody: Arc<dyn MasterKeyCustody>,
76        identity_custody: Arc<dyn DeviceIdentityCustody>,
77        clock: ClockRef,
78        cloudkit_ops: Option<Arc<dyn crate::storage::cloud::cloudkit::CloudKitOps>>,
79    ) -> Self {
80        Self {
81            db,
82            store_dir,
83            config_provider,
84            key_service,
85            key_custody,
86            identity_custody,
87            clock,
88            cloudkit_ops,
89        }
90    }
91
92    fn config(&self) -> Config {
93        (self.config_provider)()
94    }
95
96    /// Run a pure read against the connection coven owns and await the result.
97    ///
98    /// This is the read handle's form of
99    /// [`CovenHandle::sql_read`](crate::CovenHandle::sql_read): the closure receives
100    /// the `&Connection` directly (coven serializes access on its connection
101    /// thread, so this never races the writer's process), and a host closure
102    /// written against `CovenHandle::sql_read` ports unchanged here. The connection
103    /// is `SQLITE_OPEN_READONLY`: a `SELECT`/`PRAGMA` reads normally, and any
104    /// `INSERT`/`UPDATE`/`DELETE`/DDL is refused by SQLite — the read-only
105    /// guarantee is enforced at the connection, not left to the caller.
106    pub async fn sql_read<F, R>(&self, f: F) -> CovenResult<R>
107    where
108        F: FnOnce(&rusqlite::Connection) -> CovenResult<R> + Send + 'static,
109        R: Send + 'static,
110    {
111        let outcome = self
112            .db
113            .call(move |conn| Ok(f(conn)))
114            .await
115            .map_err(CovenError::from)?;
116        outcome
117    }
118
119    /// The read [`SyncStorage`] for a cloud miss, or `None` for a home-less store.
120    ///
121    /// A read-only handle holds no sync manager, so — unlike the full handle — it
122    /// always builds storage from the current [`Config`]: `None` when no provider is
123    /// configured (a home-less store holds only Local blobs, which never reach the
124    /// cloud-miss path), `Some` built from config otherwise. A provider configured
125    /// but unbuildable (missing credentials) surfaces its setup error.
126    async fn blob_storage(
127        &self,
128    ) -> Result<Option<Arc<dyn SyncStorage>>, crate::storage::cloud::setup::StorageSetupError> {
129        let config = self.config();
130        if config.cloud_home.provider.is_none() {
131            return Ok(None);
132        }
133        let storage = crate::storage::cloud::setup::create_sync_storage_with_cloudkit(
134            &config,
135            &self.key_service,
136            self.key_custody.as_ref(),
137            self.identity_custody.as_ref(),
138            None,
139            self.clock.clone(),
140            self.cloudkit_ops.clone(),
141        )
142        .await?;
143        Ok(Some(Arc::new(storage)))
144    }
145
146    /// Capture the exact current blob-bearing row version from this reader's
147    /// database snapshot.
148    pub async fn row_blob_ref(
149        &self,
150        table: &str,
151        row_id: &str,
152    ) -> Result<RowBlobRef, crate::database::DbError> {
153        self.db.row_blob_ref(table, row_id).await
154    }
155
156    /// Read a blob's whole plaintext through coven's locality-aware read: served from
157    /// the user's file (Local user-provided), coven's local store (Local
158    /// host-provided), the pinned/evictable cache on a Remote hit, or fetched from
159    /// the cloud into the cache on a Remote miss. The read counterpart of
160    /// [`CovenHandle::read_blob`](crate::CovenHandle::read_blob).
161    ///
162    /// A cloud fetch writes the fetched bytes into the per-device cache
163    /// (`storage/cache/`) with an atomic temp-then-rename — device scratch, no synced
164    /// state touched — so a File Provider materializing remote content works through a
165    /// read-only handle. The supplied [`RowBlobRef`] already carries the exact stored
166    /// object and authority, so the read performs no database write or cloud listing.
167    pub async fn read_blob(&self, blob: &RowBlobRef) -> Result<Vec<u8>, BlobCacheError> {
168        let storage = self.blob_storage().await?;
169        crate::blob::cache::read_blob(&self.db, &self.store_dir, storage.as_deref(), blob).await
170    }
171
172    /// Serve `len` plaintext bytes of an exact row blob starting at `offset`, for
173    /// streaming or seeking without loading the whole file. The [`RowBlobRef`]
174    /// carries the plaintext length used to bound the range. The ranged sibling of
175    /// [`read_blob`](Self::read_blob); a Remote-miss range read fetches from the cloud
176    /// but writes no cache file (only a whole-file read populates).
177    pub async fn open_blob_stream(
178        &self,
179        blob: &RowBlobRef,
180        offset: u64,
181        len: u64,
182    ) -> Result<Vec<u8>, BlobCacheError> {
183        let storage = self.blob_storage().await?;
184        crate::blob::cache::open_blob_stream(
185            &self.db,
186            &self.store_dir,
187            storage.as_deref(),
188            blob,
189            offset,
190            len,
191        )
192        .await
193    }
194
195    /// Open a payload
196    /// [`CovenHandle::seal_app_data`](crate::CovenHandle::seal_app_data) produced,
197    /// resolving the store's master keyring through this handle's custody. The read
198    /// side of app-data sealing: a secondary reader opens what the writer sealed,
199    /// under whichever generation the payload names.
200    ///
201    /// There is no seal counterpart here — sealing writes new ciphertext, which is
202    /// the writer's job; this handle only reads.
203    ///
204    /// [`SealError::Locked`] if the store is locked; a wrong `aad`, a tampered
205    /// payload, an unreadable version, or a generation this store's keyring lacks
206    /// each surface their own typed error.
207    pub fn open_app_data(&self, sealed: &[u8], aad: &[u8]) -> Result<Vec<u8>, SealError> {
208        crate::handle::app_data_cipher(self.key_custody.as_ref())?.open_app_data(sealed, aad)
209    }
210
211    /// Whether every blob in `blobs` is pinned for offline — present in coven's kept
212    /// cache folder (`storage/pinned/`). An empty set is vacuously pinned. A read; it
213    /// stats the folder, never writes.
214    pub async fn is_pinned(&self, blobs: &[BlobRef]) -> Result<bool, BlobCacheError> {
215        for blob in blobs {
216            if !crate::blob::cache::is_pinned(&self.store_dir, &blob.namespace, &blob.id).await? {
217                return Ok(false);
218            }
219        }
220        Ok(true)
221    }
222}