Skip to main content

bitwarden_state/
repository.rs

1use std::{any::TypeId, sync::Arc};
2
3use crate::{persist::Persist, registry::StateRegistryError};
4
5/// An error resulting from operations on a repository.
6#[derive(thiserror::Error, Debug)]
7pub enum RepositoryError {
8    /// An internal unspecified error.
9    #[error("Internal error: {0}")]
10    Internal(String),
11
12    /// A serialization or deserialization error.
13    #[error(transparent)]
14    Serde(#[from] serde_json::Error),
15
16    /// An internal database error.
17    #[error(transparent)]
18    Database(#[from] crate::sdk_managed::DatabaseError),
19
20    /// State registry error.
21    #[error(transparent)]
22    StateRegistry(#[from] StateRegistryError),
23}
24
25/// Extension trait for `Option<Arc<dyn Repository<V>>>` to concisely require that a repository
26/// is available.
27pub trait RepositoryOption<V: RepositoryItem> {
28    /// Returns a reference to the repository, or a
29    /// [`StateRegistryError::DatabaseNotInitialized`] error if it is `None`.
30    fn require(&self) -> Result<&Arc<dyn Repository<V>>, RepositoryError>;
31}
32
33impl<V: RepositoryItem> RepositoryOption<V> for Option<Arc<dyn Repository<V>>> {
34    fn require(&self) -> Result<&Arc<dyn Repository<V>>, RepositoryError> {
35        self.as_ref()
36            .ok_or(StateRegistryError::DatabaseNotInitialized.into())
37    }
38}
39
40/// This trait represents a generic repository interface, capable of storing and retrieving
41/// items using a key-value API.
42#[async_trait::async_trait]
43pub trait Repository<V: RepositoryItem>: Send + Sync {
44    /// Retrieves an item from the repository by its key.
45    async fn get(&self, key: V::Key) -> Result<Option<V>, RepositoryError>;
46    /// Lists all items in the repository.
47    async fn list(&self) -> Result<Vec<V>, RepositoryError>;
48    /// Sets an item in the repository with the specified key.
49    async fn set(&self, key: V::Key, value: V) -> Result<(), RepositoryError>;
50    /// Sets multiple items in the repository.
51    async fn set_bulk(&self, values: Vec<(V::Key, V)>) -> Result<(), RepositoryError>;
52    /// Removes an item from the repository by its key.
53    async fn remove(&self, key: V::Key) -> Result<(), RepositoryError>;
54    /// Removes multiple items from the repository by their keys.
55    async fn remove_bulk(&self, keys: Vec<V::Key>) -> Result<(), RepositoryError>;
56    /// Removes all items from the repository.
57    async fn remove_all(&self) -> Result<(), RepositoryError>;
58
59    /// Replaces all items in the repository with the provided values. This is a convenience method
60    /// that first removes all existing items and then sets the new items in bulk.
61    ///
62    /// In the future we may want to explore using revision dates to optimize this operation.
63    async fn replace_all(&self, values: Vec<(V::Key, V)>) -> Result<(), RepositoryError> {
64        self.remove_all().await?;
65        self.set_bulk(values).await
66    }
67}
68
69/// This trait is used to mark types that can be stored in a repository.
70/// It should not be implemented manually; instead, users should
71/// use the [crate::register_repository_item] macro to register their item types.
72///
73/// The [`Persist`] bound is what makes an item storable by SDK-managed repositories. It is
74/// satisfied automatically by any type that is serializable and thread-safe.
75pub trait RepositoryItem: Internal + Persist {
76    /// The name of the type implementing this trait.
77    const NAME: &'static str;
78
79    /// The type used as a key in the Repository
80    type Key: ToString + Send + Sync + 'static;
81
82    /// Returns the `TypeId` of the type implementing this trait.
83    fn type_id() -> TypeId {
84        TypeId::of::<Self>()
85    }
86
87    /// Returns metadata about the repository item type.
88    fn data() -> RepositoryItemData {
89        RepositoryItemData::new::<Self>()
90    }
91}
92
93/// This struct holds metadata about a registered repository item type.
94#[allow(dead_code)]
95#[derive(Debug, Clone, Copy)]
96pub struct RepositoryItemData {
97    type_id: TypeId,
98    name: &'static str,
99}
100
101impl RepositoryItemData {
102    /// Create a new `RepositoryItemData` from a type that implements `RepositoryItem`.
103    pub fn new<T: RepositoryItem>() -> Self {
104        Self {
105            type_id: TypeId::of::<T>(),
106            name: T::NAME,
107        }
108    }
109
110    /// Get the `TypeId` of the registered type.
111    pub fn type_id(&self) -> TypeId {
112        self.type_id
113    }
114    /// Get the name of the registered type.
115    /// This name is guaranteed to be a valid identifier.
116    pub fn name(&self) -> &'static str {
117        self.name
118    }
119}
120
121/// Validate that the provided name will be a valid identifier at compile time.
122/// This is intentionally limited to ensure compatibility with current and future storage backends.
123/// For example, SQLite tables must not begin with a number or contain special characters.
124/// Valid characters are a-z, A-Z, and underscore (_).
125pub const fn validate_registry_name(name: &str) -> bool {
126    let bytes = name.as_bytes();
127    let mut i = 0;
128    while i < bytes.len() {
129        let byte = bytes[i];
130        // Check if character is alphabetic (a-z, A-Z) or underscore
131        if !((byte >= b'a' && byte <= b'z') || (byte >= b'A' && byte <= b'Z') || byte == b'_') {
132            return false;
133        }
134        i += 1;
135    }
136    true
137}
138
139/// Represents a set of migrations for multiple repositories in a database migration process.
140#[derive(Debug, Clone)]
141pub struct RepositoryMigrations {
142    pub(crate) steps: Vec<RepositoryMigrationStep>,
143    // This is used only by indexedDB
144    #[allow(dead_code)]
145    pub(crate) version: u32,
146}
147
148/// Represents a single step for a repository in a database migration process.
149#[derive(Debug, Clone, Copy)]
150pub enum RepositoryMigrationStep {
151    /// Add a new repository.
152    Add(RepositoryItemData),
153    /// Remove an existing repository.
154    Remove(RepositoryItemData),
155}
156
157impl RepositoryMigrations {
158    /// Create a new `RepositoryMigrations` with the given steps. The version is derived from the
159    /// number of steps.
160    pub fn new(steps: Vec<RepositoryMigrationStep>) -> Self {
161        Self {
162            version: steps.len() as u32,
163            steps,
164        }
165    }
166
167    /// Converts the migration steps into a list of unique repository item data.
168    pub fn into_repository_items(self) -> Vec<RepositoryItemData> {
169        let mut map = std::collections::HashMap::new();
170        for step in self.steps {
171            match step {
172                RepositoryMigrationStep::Add(data) => {
173                    map.insert(data.type_id, data);
174                }
175                RepositoryMigrationStep::Remove(data) => {
176                    map.remove(&data.type_id);
177                }
178            }
179        }
180        map.into_values().collect()
181    }
182}
183
184/// Register a type for use in a repository. The type must only be registered once in the crate
185/// where it's defined. The provided name must be unique and not be changed.
186#[macro_export]
187macro_rules! register_repository_item {
188    ($keyty:ty => $ty:ty, $name:literal) => {
189        const _: () = {
190            impl $crate::repository::___internal::Internal for $ty {}
191            impl $crate::repository::RepositoryItem for $ty {
192                const NAME: &'static str = $name;
193                type Key = $keyty;
194            }
195            assert!(
196                $crate::repository::validate_registry_name($name),
197                concat!(
198                    "Repository name '",
199                    $name,
200                    "' must contain only alphabetic characters and underscores"
201                )
202            )
203        };
204    };
205}
206
207/// This code is not meant to be used directly, users of this crate should use the
208/// [crate::register_repository_item] macro to register their types.
209#[doc(hidden)]
210pub mod ___internal {
211
212    // This trait is in an internal module to try to forbid users from implementing `RepositoryItem`
213    // directly.
214    pub trait Internal {}
215}
216pub(crate) use ___internal::Internal;
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn test_validate_name() {
224        assert!(validate_registry_name("valid"));
225        assert!(validate_registry_name("Valid_Name"));
226        assert!(!validate_registry_name("Invalid-Name"));
227        assert!(!validate_registry_name("Invalid Name"));
228        assert!(!validate_registry_name("Invalid.Name"));
229        assert!(!validate_registry_name("Invalid123"));
230    }
231}