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