Skip to main content

coven_foundation/files/
settings.rs

1//! The store's id and this device's id in one atomic settings file.
2
3use std::path::Path;
4
5use serde::{Deserialize, Serialize};
6
7use crate::files::atomic_file::{self, FileError};
8use crate::id_source::{DeviceId, StoreId};
9
10pub(crate) const SETTINGS_FILE: &str = "settings.json";
11
12/// The settings written when creating, restoring or joining a store (E1).
13/// Storage settings belong to coven-storage's own named file.
14#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct StoreSettings {
17    /// The store's identity, shared by all of its devices.
18    pub id: StoreId,
19    /// This install's identity; a restored copy receives a fresh one (ยง19.3).
20    pub device_id: DeviceId,
21}
22
23/// Missing, damaged or unreadable store settings remain distinct errors.
24#[derive(Debug, thiserror::Error)]
25pub enum SettingsError {
26    /// The store has no settings file.
27    #[error("settings are missing for store {0}")]
28    Missing(StoreId),
29    /// The settings do not contain the declared data.
30    #[error("invalid store settings: {0}")]
31    Corrupt(#[source] Box<dyn std::error::Error + Send + Sync>),
32    /// A settings file belongs to a different directory's store.
33    #[error("settings identify store {actual}, directory identifies {expected}")]
34    WrongStore {
35        /// The id naming the directory.
36        expected: StoreId,
37        /// The id in the settings file.
38        actual: StoreId,
39    },
40    /// The settings path is not an ordinary file, or is a symbolic link.
41    #[error("settings are not a regular file for store {0}")]
42    NotRegularFile(StoreId),
43    /// Reading or writing settings failed at the filesystem boundary.
44    #[error("store settings: {0}")]
45    File(FileError),
46    /// Reading or writing settings panicked: a bug, which the caller
47    /// reports (E15).
48    #[error(transparent)]
49    Bug(#[from] crate::task::Bug),
50}
51
52impl From<FileError> for SettingsError {
53    fn from(error: FileError) -> Self {
54        match error {
55            FileError::Bug(bug) => Self::Bug(bug),
56            error => Self::File(error),
57        }
58    }
59}
60
61impl crate::task::CarriesBug for SettingsError {
62    fn bug(&self) -> Option<&crate::task::Bug> {
63        match self {
64            Self::Bug(bug) => Some(bug),
65            Self::File(error) => error.bug(),
66            Self::Missing(_)
67            | Self::Corrupt(_)
68            | Self::WrongStore { .. }
69            | Self::NotRegularFile(_) => None,
70        }
71    }
72}
73
74pub(crate) fn read(directory: &Path, id: StoreId) -> Result<StoreSettings, SettingsError> {
75    let path = directory.join(SETTINGS_FILE);
76    match std::fs::symlink_metadata(&path) {
77        Ok(metadata) if !metadata.is_file() => return Err(SettingsError::NotRegularFile(id)),
78        Ok(_) => {}
79        Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
80            return Err(SettingsError::Missing(id))
81        }
82        Err(source) => return Err(FileError::at("inspect settings file", &path, source).into()),
83    }
84    let bytes = atomic_file::read_optional(&path)?.ok_or(SettingsError::Missing(id))?;
85    let settings: StoreSettings = serde_json::from_slice(&bytes)
86        .map_err(|source| SettingsError::Corrupt(Box::new(source)))?;
87    if settings.id != id {
88        return Err(SettingsError::WrongStore { expected: id, actual: settings.id });
89    }
90    Ok(settings)
91}
92
93pub(crate) fn write(directory: &Path, settings: &StoreSettings) -> Result<(), SettingsError> {
94    let bytes =
95        serde_json::to_vec(settings).map_err(|source| SettingsError::Corrupt(Box::new(source)))?;
96    atomic_file::replace(&directory.join(SETTINGS_FILE), &bytes)?;
97    Ok(())
98}
99
100#[cfg(test)]
101#[path = "settings_tests.rs"]
102mod tests;