Skip to main content

nodedb_lite/storage/pagedb_storage/open/
native.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Native constructors: open an on-disk store, and the opt-in discard path.
4
5use std::path::Path;
6use std::sync::Arc;
7
8use pagedb::vfs::DefaultVfs;
9use pagedb::{Db, RealmId};
10
11use crate::error::LiteError;
12use crate::storage::corruption::CorruptionPolicy;
13use crate::storage::encryption::Encryption;
14use crate::storage::pagedb_storage::errors::is_corruption;
15use crate::storage::pagedb_storage::types::{PagedbStorage, lite_open_options};
16
17impl PagedbStorage<DefaultVfs> {
18    /// Open or create a database at `path` using the platform-native async VFS.
19    ///
20    /// A store that cannot be read is reported, not repaired: this is
21    /// [`CorruptionPolicy::FailClosed`]. Use
22    /// [`open_with_policy`](Self::open_with_policy) to choose otherwise.
23    ///
24    /// `encryption` controls how the 32-byte pagedb page-encryption key is
25    /// obtained:
26    ///
27    /// - [`Encryption::Plaintext`] — no encryption; the all-zero key is used.
28    ///   Must be chosen consciously.
29    /// - [`Encryption::Passphrase`] — derives the key via Argon2id using a
30    ///   random 16-byte salt. The salt is persisted in a plaintext sidecar file
31    ///   at `<path>.salt` (created on first open, mode 0o600 on Unix) so that
32    ///   the same passphrase reproduces the same key on every reopen.
33    /// - [`Encryption::RawKey`] — uses the supplied 32-byte key directly; the
34    ///   caller is responsible for key management and no sidecar is written.
35    pub async fn open(path: impl AsRef<Path>, encryption: Encryption) -> Result<Self, LiteError> {
36        Self::open_with_policy(path, encryption, CorruptionPolicy::FailClosed).await
37    }
38
39    /// Open or create a database at `path`, choosing what happens if the store
40    /// on disk cannot be read.
41    ///
42    /// Under [`CorruptionPolicy::FailClosed`] the corruption is returned and
43    /// the store is left untouched. Under
44    /// [`CorruptionPolicy::DiscardStoreAndRecreate`] the damaged store is
45    /// renamed aside and a fresh, empty database takes its place — see that
46    /// variant's documentation for what the caller is agreeing to.
47    pub async fn open_with_policy(
48        path: impl AsRef<Path>,
49        encryption: Encryption,
50        policy: CorruptionPolicy,
51    ) -> Result<Self, LiteError> {
52        let path = path.as_ref();
53        let kek = crate::storage::encryption::resolve_kek_native(&encryption, path)?;
54        let snapshot_descriptor = Some(
55            super::super::snapshot::metadata::SnapshotDescriptor::from_native(&encryption, path)?,
56        );
57        let realm = RealmId::new([0u8; 16]);
58
59        let vfs = pagedb::vfs::open_default(path).map_err(LiteError::from)?;
60
61        match Db::open(vfs, kek, 4096, realm, lite_open_options()).await {
62            Ok(db) => Ok(Self {
63                db: Arc::new(db),
64                snapshot_descriptor,
65            }),
66            Err(e) if is_corruption(&e) && path.exists() => {
67                if !policy.may_discard() {
68                    tracing::error!(
69                        path = %path.display(),
70                        error = %e,
71                        "pagedb open detected corruption — refusing to open. The store has \
72                         been left exactly as it was found."
73                    );
74                    return Err(LiteError::from(e));
75                }
76                tracing::error!(
77                    path = %path.display(),
78                    error = %e,
79                    "pagedb open detected corruption — the caller opted into discarding the \
80                     store (rename aside, recreate fresh). The new database is empty."
81                );
82                Self::discard_and_recreate(path, &encryption).await
83            }
84            Err(e) => Err(LiteError::from(e)),
85        }
86    }
87
88    /// Rename a damaged database aside and recreate a fresh one at `path`.
89    ///
90    /// The damaged store is renamed to `{path}.corrupt.{unix_secs}` (never
91    /// deleted, so the bytes remain available for offline forensics) and a
92    /// fresh database is created at `path` using the same `encryption`.
93    ///
94    /// Only reachable when the caller selected
95    /// [`CorruptionPolicy::DiscardStoreAndRecreate`]; the open paths call it
96    /// after checking, so the destructive step has exactly one gate in front of
97    /// it and exactly one implementation behind it.
98    pub(crate) async fn discard_and_recreate(
99        path: &Path,
100        encryption: &Encryption,
101    ) -> Result<Self, LiteError> {
102        let kek = crate::storage::encryption::resolve_kek_native(encryption, path)?;
103        let snapshot_descriptor = Some(
104            super::super::snapshot::metadata::SnapshotDescriptor::from_native(encryption, path)?,
105        );
106        let realm = RealmId::new([0u8; 16]);
107
108        let timestamp = crate::runtime::now_secs();
109        let corrupt_path = path.with_extension(format!("corrupt.{timestamp}"));
110
111        tracing::error!(
112            path = %path.display(),
113            corrupt_backup = %corrupt_path.display(),
114            "renaming corrupted pagedb store to backup and creating a fresh database"
115        );
116
117        if let Err(rename_err) = std::fs::rename(path, &corrupt_path) {
118            tracing::error!(error = %rename_err, "failed to rename corrupted pagedb directory");
119            return Err(LiteError::Storage {
120                detail: format!("pagedb corrupted and rename failed: rename={rename_err}"),
121            });
122        }
123
124        let vfs = pagedb::vfs::open_default(path).map_err(LiteError::from)?;
125        let db = Db::open(vfs, kek, 4096, realm, lite_open_options())
126            .await
127            .map_err(|e2| LiteError::Storage {
128                detail: format!(
129                    "pagedb corrupted, backup saved to {}, fresh create failed: {e2}",
130                    corrupt_path.display()
131                ),
132            })?;
133        Ok(Self {
134            db: Arc::new(db),
135            snapshot_descriptor,
136        })
137    }
138}