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    ///
81    /// `FromStr` is required so the debug-capabilities surface can address items
82    /// by a string key; every current key type (String and the `uuid_newtype!`
83    /// ids) already implements it, so this bound is satisfied everywhere.
84    type Key: ToString + std::str::FromStr + Send + Sync + 'static;
85
86    /// Returns the `TypeId` of the type implementing this trait.
87    fn type_id() -> TypeId {
88        TypeId::of::<Self>()
89    }
90
91    /// Returns metadata about the repository item type.
92    fn data() -> RepositoryItemData {
93        RepositoryItemData::new::<Self>()
94    }
95}
96
97/// This struct holds metadata about a registered repository item type.
98#[allow(dead_code)]
99#[derive(Debug, Clone, Copy)]
100pub struct RepositoryItemData {
101    type_id: TypeId,
102    name: &'static str,
103    /// Dev-only: monomorphized get/set/list shims for this type's repository,
104    /// captured here because `new::<T>` is the point where `T` is still known.
105    #[cfg(feature = "debug-capabilities")]
106    pub(crate) debug: crate::debug::DebugRepo,
107}
108
109impl RepositoryItemData {
110    /// Create a new `RepositoryItemData` from a type that implements `RepositoryItem`.
111    pub fn new<T: RepositoryItem>() -> Self {
112        Self {
113            type_id: TypeId::of::<T>(),
114            name: T::NAME,
115            #[cfg(feature = "debug-capabilities")]
116            debug: crate::debug::DebugRepo::for_type::<T>(),
117        }
118    }
119
120    /// Get the `TypeId` of the registered type.
121    pub fn type_id(&self) -> TypeId {
122        self.type_id
123    }
124    /// Get the name of the registered type.
125    /// This name is guaranteed to be a valid identifier.
126    pub fn name(&self) -> &'static str {
127        self.name
128    }
129}
130
131/// Validate that the provided name will be a valid identifier at compile time.
132/// This is intentionally limited to ensure compatibility with current and future storage backends.
133/// For example, SQLite tables must not begin with a number or contain special characters.
134/// Valid characters are a-z, A-Z, and underscore (_).
135pub const fn validate_registry_name(name: &str) -> bool {
136    let bytes = name.as_bytes();
137    let mut i = 0;
138    while i < bytes.len() {
139        let byte = bytes[i];
140        // Check if character is alphabetic (a-z, A-Z) or underscore
141        if !((byte >= b'a' && byte <= b'z') || (byte >= b'A' && byte <= b'Z') || byte == b'_') {
142            return false;
143        }
144        i += 1;
145    }
146    true
147}
148
149/// Represents a set of migrations for multiple repositories in a database migration process.
150#[derive(Debug, Clone)]
151pub struct RepositoryMigrations {
152    pub(crate) steps: Vec<RepositoryMigrationStep>,
153    // This is used only by indexedDB
154    #[allow(dead_code)]
155    pub(crate) version: u32,
156}
157
158/// Represents a single step for a repository in a database migration process.
159#[derive(Debug, Clone, Copy)]
160pub enum RepositoryMigrationStep {
161    /// Add a new repository.
162    Add(RepositoryItemData),
163    /// Remove an existing repository.
164    Remove(RepositoryItemData),
165}
166
167impl RepositoryMigrations {
168    /// Create a new `RepositoryMigrations` with the given steps. The version is derived from the
169    /// number of steps.
170    pub fn new(steps: Vec<RepositoryMigrationStep>) -> Self {
171        Self {
172            version: steps.len() as u32,
173            steps,
174        }
175    }
176
177    /// Converts the migration steps into a list of unique repository item data.
178    pub fn into_repository_items(self) -> Vec<RepositoryItemData> {
179        let mut map = std::collections::HashMap::new();
180        for step in self.steps {
181            match step {
182                RepositoryMigrationStep::Add(data) => {
183                    map.insert(data.type_id, data);
184                }
185                RepositoryMigrationStep::Remove(data) => {
186                    map.remove(&data.type_id);
187                }
188            }
189        }
190        map.into_values().collect()
191    }
192}
193
194/// Register a type for use in a repository. The type must only be registered once in the crate
195/// where it's defined. The provided name must be unique and not be changed.
196#[macro_export]
197macro_rules! register_repository_item {
198    ($keyty:ty => $ty:ty, $name:literal) => {
199        const _: () = {
200            impl $crate::repository::___internal::Internal for $ty {}
201            impl $crate::repository::RepositoryItem for $ty {
202                const NAME: &'static str = $name;
203                type Key = $keyty;
204            }
205            assert!(
206                $crate::repository::validate_registry_name($name),
207                concat!(
208                    "Repository name '",
209                    $name,
210                    "' must contain only alphabetic characters and underscores"
211                )
212            )
213        };
214    };
215}
216
217/// This code is not meant to be used directly, users of this crate should use the
218/// [crate::register_repository_item] macro to register their types.
219#[doc(hidden)]
220pub mod ___internal {
221
222    // This trait is in an internal module to try to forbid users from implementing `RepositoryItem`
223    // directly.
224    pub trait Internal {}
225}
226pub(crate) use ___internal::Internal;
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_validate_name() {
234        assert!(validate_registry_name("valid"));
235        assert!(validate_registry_name("Valid_Name"));
236        assert!(!validate_registry_name("Invalid-Name"));
237        assert!(!validate_registry_name("Invalid Name"));
238        assert!(!validate_registry_name("Invalid.Name"));
239        assert!(!validate_registry_name("Invalid123"));
240    }
241}