bitwarden_state/
repository.rs

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