Skip to content

Commit

Permalink
Add client_config_default function for easy client configuration (#111
Browse files Browse the repository at this point in the history
)

* [uniffi] Merge `FFICallbackError` into `Error`

Part of the goal of mls-rs-uniffi is to present a small FFI API.
Currently, it does not seem to be necessary to use a separate error
type here.

* [uniffi] Move `GroupStateStorage` wrapper to client

This type is specific to our client type so it can be nicely grouped
with it in the client module.

* [uniffi] Add a `client_config_default` function

This function returns a client config with some simple defaults. Right
now, the default is in-memory storage.

The new `GroupStateStorageAdapter` struct allows us to use any mls-rs
group state storage, so we could easily surface the Sqlite storage as
well now.
  • Loading branch information
mgeisler authored Mar 14, 2024
1 parent 8e74976 commit 23189d0
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 72 deletions.
74 changes: 55 additions & 19 deletions mls-rs-uniffi/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,43 +1,79 @@
use std::{fmt::Debug, sync::Arc};
use std::fmt::Debug;
use std::sync::Arc;

use mls_rs::{
client_builder::{self, WithGroupStateStorage},
identity::basic,
storage_provider::in_memory::InMemoryGroupStateStorage,
};
use mls_rs_core::error::IntoAnyError;
use mls_rs_crypto_openssl::OpensslCryptoProvider;

use self::group_state::{GroupStateStorage, GroupStateStorageWrapper};
use self::group_state::{GroupStateStorage, GroupStateStorageAdapter};
use crate::Error;

pub mod group_state;

#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
#[non_exhaustive]
pub enum FFICallbackError {
#[error("data preparation error")]
DataPreparationError {
#[from]
inner: mls_rs_core::mls_rs_codec::Error,
},
#[error("unexpected callback error")]
UnexpectedCallbackError {
#[from]
inner: uniffi::UnexpectedUniFFICallbackError,
},
#[derive(Debug, Clone)]
pub(crate) struct ClientGroupStorage(Arc<dyn GroupStateStorage>);

impl From<Arc<dyn GroupStateStorage>> for ClientGroupStorage {
fn from(value: Arc<dyn GroupStateStorage>) -> Self {
Self(value)
}
}

impl IntoAnyError for FFICallbackError {}
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
#[cfg_attr(mls_build_async, maybe_async::must_be_async)]
impl mls_rs_core::group::GroupStateStorage for ClientGroupStorage {
type Error = Error;

async fn state(&self, group_id: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
self.0.state(group_id.to_vec())
}

async fn epoch(&self, group_id: &[u8], epoch_id: u64) -> Result<Option<Vec<u8>>, Self::Error> {
self.0.epoch(group_id.to_vec(), epoch_id)
}

async fn write(
&mut self,
state: mls_rs_core::group::GroupState,
inserts: Vec<mls_rs_core::group::EpochRecord>,
updates: Vec<mls_rs_core::group::EpochRecord>,
) -> Result<(), Self::Error> {
self.0.write(
state.into(),
inserts.into_iter().map(Into::into).collect(),
updates.into_iter().map(Into::into).collect(),
)
}

async fn max_epoch_id(&self, group_id: &[u8]) -> Result<Option<u64>, Self::Error> {
self.0.max_epoch_id(group_id.to_vec())
}
}

pub type UniFFIConfig = client_builder::WithIdentityProvider<
basic::BasicIdentityProvider,
client_builder::WithCryptoProvider<
OpensslCryptoProvider,
WithGroupStateStorage<GroupStateStorageWrapper, client_builder::BaseConfig>,
WithGroupStateStorage<ClientGroupStorage, client_builder::BaseConfig>,
>,
>;

#[derive(Debug, Clone, uniffi::Record)]
pub struct ClientConfig {
pub group_state_storage: Arc<dyn GroupStateStorage>,
}

// TODO(mgeisler): turn into an associated function when UniFFI
// supports them: https://github.com/mozilla/uniffi-rs/issues/1074.
/// Create a client config with an in-memory group state storage.
#[uniffi::export]
pub fn client_config_default() -> ClientConfig {
ClientConfig {
group_state_storage: Arc::new(GroupStateStorageAdapter::new(
InMemoryGroupStateStorage::new(),
)),
}
}
111 changes: 67 additions & 44 deletions mls-rs-uniffi/src/config/group_state.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::{fmt::Debug, sync::Arc};
use mls_rs::error::IntoAnyError;
use std::fmt::Debug;
use std::sync::Mutex;

use super::FFICallbackError;
use crate::Error;

// TODO(mulmarta): we'd like to use GroupState and EpochRecord from mls-rs-core
// but this breaks python tests because using 2 crates makes uniffi generate
Expand All @@ -21,80 +23,101 @@ pub struct EpochRecord {
}

impl From<mls_rs_core::group::GroupState> for GroupState {
fn from(value: mls_rs_core::group::GroupState) -> Self {
Self {
id: value.id,
data: value.data,
}
fn from(mls_rs_core::group::GroupState { id, data }: mls_rs_core::group::GroupState) -> Self {
Self { id, data }
}
}

impl From<GroupState> for mls_rs_core::group::GroupState {
fn from(GroupState { id, data }: GroupState) -> Self {
Self { id, data }
}
}

impl From<mls_rs_core::group::EpochRecord> for EpochRecord {
fn from(value: mls_rs_core::group::EpochRecord) -> Self {
Self {
id: value.id,
data: value.data,
}
fn from(mls_rs_core::group::EpochRecord { id, data }: mls_rs_core::group::EpochRecord) -> Self {
Self { id, data }
}
}

impl From<EpochRecord> for mls_rs_core::group::EpochRecord {
fn from(EpochRecord { id, data }: EpochRecord) -> Self {
Self { id, data }
}
}

#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
#[cfg_attr(mls_build_async, maybe_async::must_be_async)]
#[uniffi::export(with_foreign)]
pub trait GroupStateStorage: Send + Sync + Debug {
async fn state(&self, group_id: Vec<u8>) -> Result<Option<Vec<u8>>, FFICallbackError>;
async fn epoch(
&self,
group_id: Vec<u8>,
epoch_id: u64,
) -> Result<Option<Vec<u8>>, FFICallbackError>;
async fn state(&self, group_id: Vec<u8>) -> Result<Option<Vec<u8>>, Error>;
async fn epoch(&self, group_id: Vec<u8>, epoch_id: u64) -> Result<Option<Vec<u8>>, Error>;

async fn write(
&self,
state: GroupState,
epoch_inserts: Vec<EpochRecord>,
epoch_updates: Vec<EpochRecord>,
) -> Result<(), FFICallbackError>;
) -> Result<(), Error>;

async fn max_epoch_id(&self, group_id: Vec<u8>) -> Result<Option<u64>, FFICallbackError>;
async fn max_epoch_id(&self, group_id: Vec<u8>) -> Result<Option<u64>, Error>;
}

#[derive(Debug, Clone)]
pub(crate) struct GroupStateStorageWrapper(Arc<dyn GroupStateStorage>);
/// Adapt a mls-rs `GroupStateStorage` implementation.
///
/// This is used to adapt a mls-rs `GroupStateStorage` implementation
/// to our own `GroupStateStorage` trait. This way we can use any
/// standard mls-rs group state storage from the FFI layer.
#[derive(Debug)]
pub(crate) struct GroupStateStorageAdapter<S>(Mutex<S>);

impl From<Arc<dyn GroupStateStorage>> for GroupStateStorageWrapper {
fn from(value: Arc<dyn GroupStateStorage>) -> Self {
Self(value)
impl<S> GroupStateStorageAdapter<S> {
pub fn new(group_state_storage: S) -> GroupStateStorageAdapter<S> {
Self(Mutex::new(group_state_storage))
}

fn inner(&self) -> std::sync::MutexGuard<'_, S> {
self.0.lock().unwrap()
}
}

#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
#[cfg_attr(mls_build_async, maybe_async::must_be_async)]
impl mls_rs_core::group::GroupStateStorage for GroupStateStorageWrapper {
type Error = FFICallbackError;

async fn state(&self, group_id: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
self.0.state(group_id.to_vec())
impl<S, Err> GroupStateStorage for GroupStateStorageAdapter<S>
where
S: mls_rs::GroupStateStorage<Error = Err> + Debug,
Err: IntoAnyError,
{
async fn state(&self, group_id: Vec<u8>) -> Result<Option<Vec<u8>>, Error> {
self.inner()
.state(&group_id)
.map_err(|err| err.into_any_error().into())
}

async fn epoch(&self, group_id: &[u8], epoch_id: u64) -> Result<Option<Vec<u8>>, Self::Error> {
self.0.epoch(group_id.to_vec(), epoch_id)
async fn epoch(&self, group_id: Vec<u8>, epoch_id: u64) -> Result<Option<Vec<u8>>, Error> {
self.inner()
.epoch(&group_id, epoch_id)
.map_err(|err| err.into_any_error().into())
}

async fn write(
&mut self,
state: mls_rs_core::group::GroupState,
inserts: Vec<mls_rs_core::group::EpochRecord>,
updates: Vec<mls_rs_core::group::EpochRecord>,
) -> Result<(), Self::Error> {
self.0.write(
state.into(),
inserts.into_iter().map(Into::into).collect(),
updates.into_iter().map(Into::into).collect(),
)
&self,
state: GroupState,
epoch_inserts: Vec<EpochRecord>,
epoch_updates: Vec<EpochRecord>,
) -> Result<(), Error> {
self.inner()
.write(
state.into(),
epoch_inserts.into_iter().map(Into::into).collect(),
epoch_updates.into_iter().map(Into::into).collect(),
)
.map_err(|err| err.into_any_error().into())
}

async fn max_epoch_id(&self, group_id: &[u8]) -> Result<Option<u64>, Self::Error> {
self.0.max_epoch_id(group_id.to_vec())
async fn max_epoch_id(&self, group_id: Vec<u8>) -> Result<Option<u64>, Error> {
self.inner()
.max_epoch_id(&group_id)
.map_err(|err| err.into_any_error().into())
}
}
25 changes: 16 additions & 9 deletions mls-rs-uniffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,20 @@ pub enum Error {
#[from]
inner: mls_rs::error::AnyError,
},
#[error("A data encoding error occurred: {inner}")]
MlsCodecError {
#[from]
inner: mls_rs_core::mls_rs_codec::Error,
},
#[error("Unexpected callback error in UniFFI: {inner}")]
UnexpectedCallbackError {
#[from]
inner: uniffi::UnexpectedUniFFICallbackError,
},
}

impl IntoAnyError for Error {}

/// A [`mls_rs::crypto::SignaturePublicKey`] wrapper.
#[derive(Clone, Debug, uniffi::Object)]
pub struct SignaturePublicKey {
Expand Down Expand Up @@ -678,7 +690,6 @@ impl Group {
mod tests {
use super::*;
use crate::config::group_state::{EpochRecord, GroupState, GroupStateStorage};
use crate::config::FFICallbackError;
use std::collections::HashMap;

#[test]
Expand Down Expand Up @@ -708,16 +719,12 @@ mod tests {
}

impl GroupStateStorage for CustomGroupStateStorage {
fn state(&self, group_id: Vec<u8>) -> Result<Option<Vec<u8>>, FFICallbackError> {
fn state(&self, group_id: Vec<u8>) -> Result<Option<Vec<u8>>, Error> {
let groups = self.lock();
Ok(groups.get(&group_id).map(|group| group.state.clone()))
}

fn epoch(
&self,
group_id: Vec<u8>,
epoch_id: u64,
) -> Result<Option<Vec<u8>>, FFICallbackError> {
fn epoch(&self, group_id: Vec<u8>, epoch_id: u64) -> Result<Option<Vec<u8>>, Error> {
let groups = self.lock();
match groups.get(&group_id) {
Some(group) => {
Expand All @@ -735,7 +742,7 @@ mod tests {
state: GroupState,
epoch_inserts: Vec<EpochRecord>,
epoch_updates: Vec<EpochRecord>,
) -> Result<(), FFICallbackError> {
) -> Result<(), Error> {
let mut groups = self.lock();

let group = groups.entry(state.id).or_default();
Expand All @@ -756,7 +763,7 @@ mod tests {
Ok(())
}

fn max_epoch_id(&self, group_id: Vec<u8>) -> Result<Option<u64>, FFICallbackError> {
fn max_epoch_id(&self, group_id: Vec<u8>) -> Result<Option<u64>, Error> {
let groups = self.lock();
Ok(groups
.get(&group_id)
Expand Down
8 changes: 8 additions & 0 deletions mls-rs-uniffi/tests/client_config_default_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from mls_rs_uniffi import Client, CipherSuite, generate_signature_keypair, client_config_default

client_config = client_config_default()
key = generate_signature_keypair(CipherSuite.CURVE25519_AES128)
alice = Client(b'alice', key, client_config)

group = alice.create_group(None)
group.write_to_storage()
1 change: 1 addition & 0 deletions mls-rs-uniffi/tests/scenarios.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ macro_rules! generate_python_tests {
}

generate_python_tests!(generate_signature_keypair, None);
generate_python_tests!(client_config_default_sync, None);

// TODO(mulmarta): it'll break if we use async trait which will be
// supported in the next UniFFI release
Expand Down

0 comments on commit 23189d0

Please sign in to comment.