Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions bindings/mobile/src/mls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,73 @@ pub async fn create_client(
}))
}

#[derive(uniffi::Enum, Debug, Clone, Copy)]
pub enum FfiIntegrityCheckLevel {
Quick,
Full,
}

impl From<FfiIntegrityCheckLevel> for xmtp_db::prelude::IntegrityCheckLevel {
fn from(level: FfiIntegrityCheckLevel) -> Self {
match level {
FfiIntegrityCheckLevel::Quick => Self::Quick,
FfiIntegrityCheckLevel::Full => Self::Full,
}
}
}

#[derive(uniffi::Record)]
pub struct FfiIntegrityCheckOutcome {
/// "ok" | "corrupt" | "unreadable" | "saltMissing" | "locked" | "failed"
pub outcome: String,
/// Row-level findings (corrupt) or the error/reason string (other
/// non-ok outcomes). Empty when ok.
pub findings: Vec<String>,
}

impl From<xmtp_db::prelude::IntegrityCheckResult> for FfiIntegrityCheckOutcome {
fn from(r: xmtp_db::prelude::IntegrityCheckResult) -> Self {
use xmtp_db::prelude::IntegrityCheckResult::*;
let (outcome, findings) = match r {
Ok => ("ok", vec![]),
Corrupt { findings } => ("corrupt", findings),
Unreadable { reason } => ("unreadable", vec![reason]),
SaltMissing => ("saltMissing", vec![]),
Locked => ("locked", vec![]),
Failed { error } => ("failed", vec![error]),
};
FfiIntegrityCheckOutcome {
outcome: outcome.into(),
findings,
}
}
}

/// Read-only integrity check of a database file by path, without a client.
/// Runs on a blocking thread. For encrypted databases pass the same 32-byte
/// encryption key used to create the client.
#[uniffi::export(async_runtime = "tokio", default(encryption_key = None, level = None))]
pub async fn check_database_integrity(
db_path: String,
encryption_key: Option<Vec<u8>>,
level: Option<FfiIntegrityCheckLevel>,
) -> Result<FfiIntegrityCheckOutcome, FfiError> {
// Same 32-byte validation and error message as `create_client`'s key
// conversion.
let key = encryption_key
.map(|k| -> Result<EncryptionKey, String> {
k.try_into()
.map_err(|_| "Malformed 32 byte encryption key".to_string())
})
.transpose()?;
let level = level.map(Into::into).unwrap_or_default();
let result = tokio::task::spawn_blocking(move || {
xmtp_db::prelude::check_database_integrity(&db_path, key.as_ref(), level)
})
.await?;
Ok(result.into())
}

#[allow(unused)]
#[uniffi::export(async_runtime = "tokio")]
#[tracing::instrument(level = "debug", skip_all)]
Expand Down Expand Up @@ -770,6 +837,21 @@ impl FfiXmtpClient {
Ok(self.inner_client.reconnect_db()?)
}

/// Read-only integrity check of this client's database. Uses a
/// dedicated read-only connection on a blocking thread, so it blocks
/// neither the async runtime nor this client's DB operations.
#[tracing::instrument(skip_all)]
pub async fn db_integrity_check(
&self,
level: Option<FfiIntegrityCheckLevel>,
) -> Result<FfiIntegrityCheckOutcome, FfiError> {
let client = self.inner_client.clone();
let level = level.map(Into::into).unwrap_or_default();
let result =
tokio::task::spawn_blocking(move || client.db_integrity_check(level)).await??;
Ok(result.into())
}

/// Cleanly shut down this client: cancel in-flight workers and detached
/// streams, then release the DB connection. Idempotent — a second call
/// resolves to `Ok`.
Expand Down
86 changes: 86 additions & 0 deletions bindings/mobile/src/mls/tests/integrity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//! Tests for the read-only database integrity check: the client method
//! (`FfiXmtpClient::db_integrity_check`) and the free function
//! (`check_database_integrity`).

use crate::{DbOptions, check_database_integrity};
use xmtp_db::EncryptedMessageStore;

use super::*;

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_db_integrity_check_ok() {
let client = new_test_client().await;
let outcome = client.db_integrity_check(None).await.unwrap();
assert_eq!(outcome.outcome, "ok");
assert!(outcome.findings.is_empty());
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_check_database_integrity_free_fn_ok() {
let ffi_inbox_owner = FfiWalletInboxOwner::new();
let ident = ffi_inbox_owner.identifier();
let nonce = 1;
let inbox_id = ident.inbox_id(nonce).unwrap();
let db_path = tmp_path();
let key: Vec<u8> = EncryptedMessageStore::<()>::generate_enc_key().into();

let client = create_client(
connect_to_backend_test().await,
DbOptions::new(Some(db_path.clone()), Some(key.clone()), None, None, None),
&inbox_id,
ident,
nonce,
None,
None,
None,
None,
None,
None,
)
.await
.unwrap();
register_client_with_wallet(&ffi_inbox_owner, &client).await;
// Release the DB connection before checking the file out from under a
// still-open client.
client.shutdown().await.unwrap();

let outcome = check_database_integrity(db_path, Some(key), None)
.await
.unwrap();
assert_eq!(outcome.outcome, "ok");
assert!(outcome.findings.is_empty());
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_check_database_integrity_free_fn_wrong_key_is_unreadable() {
let ffi_inbox_owner = FfiWalletInboxOwner::new();
let ident = ffi_inbox_owner.identifier();
let nonce = 1;
let inbox_id = ident.inbox_id(nonce).unwrap();
let db_path = tmp_path();
let key: Vec<u8> = EncryptedMessageStore::<()>::generate_enc_key().into();

let client = create_client(
connect_to_backend_test().await,
DbOptions::new(Some(db_path.clone()), Some(key), None, None, None),
&inbox_id,
ident,
nonce,
None,
None,
None,
None,
None,
None,
)
.await
.unwrap();
register_client_with_wallet(&ffi_inbox_owner, &client).await;
client.shutdown().await.unwrap();

let wrong_key: Vec<u8> = EncryptedMessageStore::<()>::generate_enc_key().into();
let outcome = check_database_integrity(db_path, Some(wrong_key), None)
.await
.unwrap();
assert_eq!(outcome.outcome, "unreadable");
}
1 change: 1 addition & 0 deletions bindings/mobile/src/mls/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ mod content_types;
mod dms;
mod group_management;
mod identity;
mod integrity;
mod lifecycle;
mod networking;
mod static_methods;
Expand Down
Loading