Skip to main content

bitwarden_state/
repository.rs

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