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}