Skip to content

Commit 34daa77

Browse files
committed
Add generic repository support
Implement demo cipher repository
1 parent 9502e4e commit 34daa77

File tree

18 files changed

+493
-3
lines changed

18 files changed

+493
-3
lines changed

Cargo.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bitwarden-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ wasm = [
2828
] # WASM support
2929

3030
[dependencies]
31+
async-trait = ">=0.1.80, <0.2"
3132
base64 = ">=0.22.1, <0.23"
3233
bitwarden-api-api = { workspace = true }
3334
bitwarden-api-identity = { workspace = true }

crates/bitwarden-core/src/client/client.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::client::flags::Flags;
99
use crate::client::{
1010
client_settings::ClientSettings,
1111
internal::{ApiConfigurations, Tokens},
12+
repository::RepositoryMap,
1213
};
1314

1415
/// The main struct to interact with the Bitwarden SDK.
@@ -86,6 +87,8 @@ impl Client {
8687
})),
8788
external_client,
8889
key_store: KeyStore::default(),
90+
#[cfg(feature = "internal")]
91+
repository_map: RwLock::new(RepositoryMap::default()),
8992
}),
9093
}
9194
}

crates/bitwarden-core/src/client/internal.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use crate::{
1919
#[cfg(feature = "internal")]
2020
use crate::{
2121
client::encryption_settings::EncryptionSettingsError,
22+
client::repository::{Repository, RepositoryMap},
2223
client::{flags::Flags, login_method::UserLoginMethod},
2324
error::NotAuthenticatedError,
2425
};
@@ -60,6 +61,9 @@ pub struct InternalClient {
6061
pub(crate) external_client: reqwest::Client,
6162

6263
pub(super) key_store: KeyStore<KeyIds>,
64+
65+
#[cfg(feature = "internal")]
66+
pub(super) repository_map: RwLock<RepositoryMap>,
6367
}
6468

6569
impl InternalClient {
@@ -223,4 +227,20 @@ impl InternalClient {
223227
) -> Result<(), EncryptionSettingsError> {
224228
EncryptionSettings::set_org_keys(org_keys, &self.key_store)
225229
}
230+
231+
#[cfg(feature = "internal")]
232+
pub fn register_repository<T: 'static + Repository<V>, V: 'static>(&self, store: Arc<T>) {
233+
self.repository_map
234+
.write()
235+
.expect("RwLock is not poisoned")
236+
.insert(store);
237+
}
238+
239+
#[cfg(feature = "internal")]
240+
pub fn get_repository<T: 'static>(&self) -> Option<Arc<dyn Repository<T>>> {
241+
self.repository_map
242+
.read()
243+
.expect("RwLock is not poisoned")
244+
.get()
245+
}
226246
}

crates/bitwarden-core/src/client/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@ pub use client_settings::{ClientSettings, DeviceType};
1818

1919
#[cfg(feature = "internal")]
2020
pub mod test_accounts;
21+
22+
#[cfg(feature = "internal")]
23+
pub mod repository;
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
use std::{
2+
any::{Any, TypeId},
3+
collections::HashMap,
4+
sync::Arc,
5+
};
6+
7+
#[derive(thiserror::Error, Debug)]
8+
pub enum RepositoryError {
9+
#[error("Internal error: {0}")]
10+
Internal(String),
11+
}
12+
13+
#[async_trait::async_trait]
14+
pub trait Repository<T>: Send + Sync {
15+
async fn get(&self, key: String) -> Result<Option<T>, RepositoryError>;
16+
async fn list(&self) -> Result<Vec<T>, RepositoryError>;
17+
async fn set(&self, key: String, value: T) -> Result<(), RepositoryError>;
18+
async fn remove(&self, key: String) -> Result<(), RepositoryError>;
19+
}
20+
21+
#[derive(Default)]
22+
pub struct RepositoryMap {
23+
stores: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
24+
}
25+
26+
impl std::fmt::Debug for RepositoryMap {
27+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28+
f.debug_struct("RepositoryMap")
29+
.field("stores", &self.stores.keys())
30+
.finish()
31+
}
32+
}
33+
34+
impl RepositoryMap {
35+
pub fn new() -> Self {
36+
RepositoryMap {
37+
stores: HashMap::new(),
38+
}
39+
}
40+
41+
pub fn insert<T: 'static>(&mut self, value: Arc<dyn Repository<T>>) {
42+
self.stores.insert(TypeId::of::<T>(), Box::new(value));
43+
}
44+
45+
pub fn get<T: 'static>(&self) -> Option<Arc<dyn Repository<T>>> {
46+
self.stores
47+
.get(&TypeId::of::<T>())
48+
.and_then(|boxed| boxed.downcast_ref::<Arc<dyn Repository<T>>>())
49+
.map(Arc::clone)
50+
}
51+
}
52+
53+
#[cfg(test)]
54+
mod tests {
55+
use super::*;
56+
57+
macro_rules! impl_repository {
58+
($name:ident, $ty:ty) => {
59+
#[async_trait::async_trait]
60+
impl Repository<$ty> for $name {
61+
async fn get(&self, _key: String) -> Result<Option<$ty>, RepositoryError> {
62+
Ok(Some(self.0.clone()))
63+
}
64+
async fn list(&self) -> Result<Vec<$ty>, RepositoryError> {
65+
unimplemented!()
66+
}
67+
async fn set(&self, _key: String, _value: $ty) -> Result<(), RepositoryError> {
68+
unimplemented!()
69+
}
70+
async fn remove(&self, _key: String) -> Result<(), RepositoryError> {
71+
unimplemented!()
72+
}
73+
}
74+
};
75+
}
76+
77+
#[derive(PartialEq, Eq, Debug)]
78+
struct TestA(usize);
79+
#[derive(PartialEq, Eq, Debug)]
80+
struct TestB(String);
81+
#[derive(PartialEq, Eq, Debug)]
82+
struct TestC(Vec<u8>);
83+
84+
impl_repository!(TestA, usize);
85+
impl_repository!(TestB, String);
86+
impl_repository!(TestC, Vec<u8>);
87+
88+
#[tokio::test]
89+
async fn test_repository_map() {
90+
let a = Arc::new(TestA(145832));
91+
let b = Arc::new(TestB("test".to_string()));
92+
let c = Arc::new(TestC(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]));
93+
94+
let mut map = RepositoryMap::new();
95+
96+
async fn get<T: 'static>(map: &RepositoryMap) -> Option<T> {
97+
map.get::<T>().unwrap().get(String::new()).await.unwrap()
98+
}
99+
100+
assert!(map.get::<usize>().is_none());
101+
assert!(map.get::<String>().is_none());
102+
assert!(map.get::<Vec<u8>>().is_none());
103+
104+
map.insert(a.clone());
105+
assert_eq!(get(&map).await, Some(a.0));
106+
assert!(map.get::<String>().is_none());
107+
assert!(map.get::<Vec<u8>>().is_none());
108+
109+
map.insert(b.clone());
110+
assert_eq!(get(&map).await, Some(a.0));
111+
assert_eq!(get(&map).await, Some(b.0.clone()));
112+
assert!(map.get::<Vec<u8>>().is_none());
113+
114+
map.insert(c.clone());
115+
assert_eq!(get(&map).await, Some(a.0));
116+
assert_eq!(get(&map).await, Some(b.0.clone()));
117+
assert_eq!(get(&map).await, Some(c.0.clone()));
118+
}
119+
}

crates/bitwarden-uniffi/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ chrono = { workspace = true, features = ["std"] }
3232
env_logger = "0.11.1"
3333
log = { workspace = true }
3434
schemars = { workspace = true, optional = true }
35+
serde_json = { workspace = true }
3536
thiserror = { workspace = true }
3637
uniffi = { workspace = true }
3738
uuid = { workspace = true }

crates/bitwarden-uniffi/src/platform/mod.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
use bitwarden_core::platform::FingerprintRequest;
1+
use std::sync::Arc;
2+
3+
use bitwarden_core::{platform::FingerprintRequest, Client};
24
use bitwarden_fido::ClientFido2Ext;
5+
use bitwarden_vault::Cipher;
6+
use repository::UniffiRepositoryBridge;
37

48
use crate::error::{Error, Result};
59

610
mod fido2;
11+
mod repository;
712

813
#[derive(uniffi::Object)]
914
pub struct PlatformClient(pub(crate) bitwarden_core::Client);
@@ -38,4 +43,22 @@ impl PlatformClient {
3843
pub fn fido2(&self) -> fido2::ClientFido2 {
3944
fido2::ClientFido2(self.0.fido2())
4045
}
46+
47+
pub fn repository(&self) -> RepositoryClient {
48+
RepositoryClient(self.0.clone())
49+
}
50+
}
51+
52+
#[derive(uniffi::Object)]
53+
pub struct RepositoryClient(Client);
54+
55+
repository::create_uniffi_repository!(CipherRepository, Cipher);
56+
57+
#[uniffi::export]
58+
impl RepositoryClient {
59+
pub fn register_cipher_repository(&self, store: Arc<dyn CipherRepository>) -> Result<()> {
60+
let store_internal = UniffiRepositoryBridge::new(store);
61+
self.0.internal.register_repository(store_internal);
62+
Ok(())
63+
}
4164
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use std::sync::Arc;
2+
3+
pub struct UniffiRepositoryBridge<T>(pub T);
4+
5+
impl<T: ?Sized> UniffiRepositoryBridge<Arc<T>> {
6+
pub fn new(store: Arc<T>) -> Arc<Self> {
7+
Arc::new(UniffiRepositoryBridge(store))
8+
}
9+
}
10+
11+
impl<T: std::fmt::Debug> std::fmt::Debug for UniffiRepositoryBridge<T> {
12+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13+
self.0.fmt(f)
14+
}
15+
}
16+
17+
#[derive(uniffi::Error, thiserror::Error, Debug)]
18+
#[uniffi(flat_error)]
19+
pub enum RepositoryError {
20+
#[error("Internal error: {0}")]
21+
Internal(String),
22+
}
23+
24+
// Need to implement this From<> impl in order to handle unexpected callback errors. See the
25+
// following page in the Uniffi user guide:
26+
// <https://mozilla.github.io/uniffi-rs/foreign_traits.html#error-handling>
27+
impl From<uniffi::UnexpectedUniFFICallbackError> for RepositoryError {
28+
fn from(e: uniffi::UnexpectedUniFFICallbackError) -> Self {
29+
Self::Internal(e.reason)
30+
}
31+
}
32+
33+
impl From<RepositoryError> for bitwarden_core::client::repository::RepositoryError {
34+
fn from(e: RepositoryError) -> Self {
35+
match e {
36+
RepositoryError::Internal(msg) => Self::Internal(msg),
37+
}
38+
}
39+
}
40+
41+
macro_rules! create_uniffi_repository {
42+
($name:ident, $ty:ty) => {
43+
#[uniffi::export(with_foreign)]
44+
#[async_trait::async_trait]
45+
pub trait $name: Send + Sync {
46+
async fn get(
47+
&self,
48+
id: String,
49+
) -> Result<Option<$ty>, $crate::platform::repository::RepositoryError>;
50+
async fn list(&self)
51+
-> Result<Vec<$ty>, $crate::platform::repository::RepositoryError>;
52+
async fn set(
53+
&self,
54+
id: String,
55+
value: $ty,
56+
) -> Result<(), $crate::platform::repository::RepositoryError>;
57+
async fn remove(
58+
&self,
59+
id: String,
60+
) -> Result<(), $crate::platform::repository::RepositoryError>;
61+
62+
async fn has(
63+
&self,
64+
id: String,
65+
) -> Result<bool, $crate::platform::repository::RepositoryError> {
66+
match self.get(id).await {
67+
Ok(x) => Ok(x.is_some()),
68+
Err(e) => Err(e),
69+
}
70+
}
71+
}
72+
73+
#[async_trait::async_trait]
74+
impl bitwarden_core::client::repository::Repository<$ty>
75+
for $crate::platform::repository::UniffiRepositoryBridge<Arc<dyn $name>>
76+
{
77+
async fn get(
78+
&self,
79+
key: String,
80+
) -> Result<Option<$ty>, bitwarden_core::client::repository::RepositoryError> {
81+
self.0.get(key).await.map_err(Into::into)
82+
}
83+
async fn list(
84+
&self,
85+
) -> Result<Vec<$ty>, bitwarden_core::client::repository::RepositoryError> {
86+
self.0.list().await.map_err(Into::into)
87+
}
88+
async fn set(
89+
&self,
90+
key: String,
91+
value: $ty,
92+
) -> Result<(), bitwarden_core::client::repository::RepositoryError> {
93+
self.0.set(key, value).await.map_err(Into::into)
94+
}
95+
async fn remove(
96+
&self,
97+
key: String,
98+
) -> Result<(), bitwarden_core::client::repository::RepositoryError> {
99+
self.0.remove(key).await.map_err(Into::into)
100+
}
101+
}
102+
};
103+
}
104+
pub(super) use create_uniffi_repository;

crates/bitwarden-uniffi/src/vault/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,8 @@ impl VaultClient {
6060
.generate_totp_cipher_view(view, time)
6161
.map_err(Error::Totp)?)
6262
}
63+
64+
pub async fn print_the_ciphers(&self) -> String {
65+
self.0.print_the_ciphers().await
66+
}
6367
}

0 commit comments

Comments
 (0)