Skip to main content

bitwarden_state/
repository.rs

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