diff --git a/integration-tests/.gitignore b/integration-tests/.gitignore index e525de89a..140a35ffe 100644 --- a/integration-tests/.gitignore +++ b/integration-tests/.gitignore @@ -1 +1,2 @@ test-db +test-sync-server diff --git a/integration-tests/src/bindings_tests/replica.c b/integration-tests/src/bindings_tests/replica.c index 423d8dc25..bd15adfa9 100644 --- a/integration-tests/src/bindings_tests/replica.c +++ b/integration-tests/src/bindings_tests/replica.c @@ -1,4 +1,5 @@ #include +#include #include #include "unity.h" #include "taskchampion.h" @@ -153,6 +154,48 @@ static void test_replica_task_creation(void) { tc_replica_free(rep); } +// When tc_replica_undo is passed NULL for undone_out, it still succeeds +static void test_replica_sync_local(void) { + TCReplica *rep = tc_replica_new_in_memory(); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + mkdir("test-sync-server", 0755); // ignore error, if dir already exists + + TCString *err = NULL; + TCServer *server = tc_server_new_local(tc_string_borrow("test-sync-server"), &err); + TEST_ASSERT_NOT_NULL(server); + TEST_ASSERT_NULL(err); + + int rv = tc_replica_sync(rep, server, false); + TEST_ASSERT_EQUAL(TC_RESULT_OK, rv); + TEST_ASSERT_NULL(tc_replica_error(rep)); + + tc_server_free(server); + tc_replica_free(rep); + + // test error handling + server = tc_server_new_local(tc_string_borrow("/no/such/directory"), &err); + TEST_ASSERT_NULL(server); + TEST_ASSERT_NOT_NULL(err); + tc_string_free(err); +} + +// When tc_replica_undo is passed NULL for undone_out, it still succeeds +static void test_replica_remote_server(void) { + TCString *err = NULL; + TCServer *server = tc_server_new_remote( + tc_string_borrow("tc.freecinc.com"), + tc_uuid_new_v4(), + tc_string_borrow("\xf0\x28\x8c\x28"), // NOTE: not utf-8 + &err); + TEST_ASSERT_NOT_NULL(server); + TEST_ASSERT_NULL(err); + + // can't actually do anything with this server! + + tc_server_free(server); +} + // a replica with tasks in it returns an appropriate list of tasks and list of uuids static void test_replica_all_tasks(void) { TCReplica *rep = tc_replica_new_in_memory(); @@ -274,6 +317,8 @@ int replica_tests(void) { RUN_TEST(test_replica_working_set); RUN_TEST(test_replica_undo_empty_null_undone_out); RUN_TEST(test_replica_task_creation); + RUN_TEST(test_replica_sync_local); + RUN_TEST(test_replica_remote_server); RUN_TEST(test_replica_all_tasks); RUN_TEST(test_replica_task_import); RUN_TEST(test_replica_get_task_not_found); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 39fab8534..614d57f7b 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -14,6 +14,7 @@ pub mod atomic; pub mod kv; pub mod replica; pub mod result; +pub mod server; pub mod status; pub mod string; pub mod task; @@ -26,6 +27,7 @@ pub(crate) mod types { pub(crate) use crate::kv::{TCKVList, TCKV}; pub(crate) use crate::replica::TCReplica; pub(crate) use crate::result::TCResult; + pub(crate) use crate::server::TCServer; pub(crate) use crate::status::TCStatus; pub(crate) use crate::string::{TCString, TCStringList}; pub(crate) use crate::task::{TCTask, TCTaskList}; diff --git a/lib/src/replica.rs b/lib/src/replica.rs index 314f8d375..423810645 100644 --- a/lib/src/replica.rs +++ b/lib/src/replica.rs @@ -100,8 +100,9 @@ pub unsafe extern "C" fn tc_replica_new_in_memory() -> *mut TCReplica { unsafe { TCReplica::from(Replica::new(storage)).return_ptr() } } -/// Create a new TCReplica with an on-disk database having the given filename. On error, a string -/// is written to the `error_out` parameter (if it is not NULL) and NULL is returned. +/// Create a new TCReplica with an on-disk database having the given filename. On error, a string +/// is written to the error_out parameter (if it is not NULL) and NULL is returned. The caller +/// must free this string. #[no_mangle] pub unsafe extern "C" fn tc_replica_new_on_disk( path: *mut TCString, @@ -258,7 +259,31 @@ pub unsafe extern "C" fn tc_replica_import_task_with_uuid( ) } -// TODO: tc_replica_sync +/// Synchronize this replica with a server. +/// +/// The `server` argument remains owned by the caller, and must be freed explicitly. +#[no_mangle] +pub unsafe extern "C" fn tc_replica_sync( + rep: *mut TCReplica, + server: *mut TCServer, + avoid_snapshots: bool, +) -> TCResult { + wrap( + rep, + |rep| { + debug_assert!(!server.is_null()); + // SAFETY: + // - server is not NULL + // - *server is a valid TCServer (promised by caller) + // - server is valid for the lifetime of tc_replica_sync (not threadsafe) + // - server will not be accessed simultaneously (not threadsafe) + let server = unsafe { TCServer::from_ptr_arg_ref_mut(server) }; + rep.sync(server.as_mut(), avoid_snapshots)?; + Ok(TCResult::Ok) + }, + TCResult::Error, + ) +} /// Undo local operations until the most recent UndoPoint. /// diff --git a/lib/src/server.rs b/lib/src/server.rs new file mode 100644 index 000000000..d50650ec9 --- /dev/null +++ b/lib/src/server.rs @@ -0,0 +1,139 @@ +use crate::traits::*; +use crate::types::*; +use crate::util::err_to_tcstring; +use taskchampion::{Server, ServerConfig}; + +/// TCServer represents an interface to a sync server. Aside from new and free, a server +/// has no C-accessible API, but is designed to be passed to `tc_replica_sync`. +/// +/// ## Safety +/// +/// TCServer are not threadsafe, and must not be used with multiple replicas simultaneously. +pub struct TCServer(Box); + +impl PassByPointer for TCServer {} + +impl From> for TCServer { + fn from(server: Box) -> TCServer { + TCServer(server) + } +} + +impl AsMut> for TCServer { + fn as_mut(&mut self) -> &mut Box { + &mut self.0 + } +} + +/// Utility function to allow using `?` notation to return an error value. +fn wrap(f: F, error_out: *mut *mut TCString, err_value: T) -> T +where + F: FnOnce() -> anyhow::Result, +{ + match f() { + Ok(v) => v, + Err(e) => { + if !error_out.is_null() { + // SAFETY: + // - error_out is not NULL (checked) + // - ..and points to a valid pointer (promised by caller) + // - caller will free this string (promised by caller) + unsafe { + *error_out = err_to_tcstring(e).return_ptr(); + } + } + err_value + } + } +} + +/// Create a new TCServer that operates locally (on-disk). See the TaskChampion docs for the +/// description of the arguments. +/// +/// On error, a string is written to the error_out parameter (if it is not NULL) and NULL is +/// returned. The caller must free this string. +/// +/// The server must be freed after it is used - tc_replica_sync does not automatically free it. +#[no_mangle] +pub unsafe extern "C" fn tc_server_new_local( + server_dir: *mut TCString, + error_out: *mut *mut TCString, +) -> *mut TCServer { + wrap( + || { + // SAFETY: see TCString docstring + let server_dir = unsafe { TCString::take_from_ptr_arg(server_dir) }; + let server_config = ServerConfig::Local { + server_dir: server_dir.to_path_buf(), + }; + let server = server_config.into_server()?; + // SAFETY: caller promises to free this server. + Ok(unsafe { TCServer::return_ptr(server.into()) }) + }, + error_out, + std::ptr::null_mut(), + ) +} + +/// Create a new TCServer that connects to a remote server. See the TaskChampion docs for the +/// description of the arguments. +/// +/// On error, a string is written to the error_out parameter (if it is not NULL) and NULL is +/// returned. The caller must free this string. +/// +/// The server must be freed after it is used - tc_replica_sync does not automatically free it. +#[no_mangle] +pub unsafe extern "C" fn tc_server_new_remote( + origin: *mut TCString, + client_key: TCUuid, + encryption_secret: *mut TCString, + error_out: *mut *mut TCString, +) -> *mut TCServer { + wrap( + || { + debug_assert!(!origin.is_null()); + debug_assert!(!encryption_secret.is_null()); + // SAFETY: + // - origin is not NULL + // - origin is valid (promised by caller) + // - origin ownership is transferred to this function + let origin = unsafe { TCString::take_from_ptr_arg(origin) }.into_string()?; + + // SAFETY: + // - client_key is a valid Uuid (any 8-byte sequence counts) + + let client_key = unsafe { TCUuid::val_from_arg(client_key) }; + // SAFETY: + // - encryption_secret is not NULL + // - encryption_secret is valid (promised by caller) + // - encryption_secret ownership is transferred to this function + let encryption_secret = unsafe { TCString::take_from_ptr_arg(encryption_secret) } + .as_bytes() + .to_vec(); + + let server_config = ServerConfig::Remote { + origin, + client_key, + encryption_secret, + }; + let server = server_config.into_server()?; + // SAFETY: caller promises to free this server. + Ok(unsafe { TCServer::return_ptr(server.into()) }) + }, + error_out, + std::ptr::null_mut(), + ) +} + +/// Free a server. The server may not be used after this function returns and must not be freed +/// more than once. +#[no_mangle] +pub unsafe extern "C" fn tc_server_free(server: *mut TCServer) { + debug_assert!(!server.is_null()); + // SAFETY: + // - server is not NULL + // - server came from tc_server_new_.., which used return_ptr + // - server will not be used after (promised by caller) + let server = unsafe { TCServer::take_from_ptr_arg(server) }; + drop(server); +} diff --git a/lib/src/string.rs b/lib/src/string.rs index 758f1d6e0..f21f05c95 100644 --- a/lib/src/string.rs +++ b/lib/src/string.rs @@ -82,7 +82,7 @@ impl<'a> TCString<'a> { } } - fn as_bytes(&self) -> &[u8] { + pub(crate) fn as_bytes(&self) -> &[u8] { match self { TCString::CString(cstring) => cstring.as_bytes(), TCString::CStr(cstr) => cstr.to_bytes(), diff --git a/lib/taskchampion.h b/lib/taskchampion.h index d5f787a88..41edeedff 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -63,6 +63,16 @@ typedef enum TCStatus { */ typedef struct TCReplica TCReplica; +/** + * TCServer represents an interface to a sync server. Aside from new and free, a server + * has no C-accessible API, but is designed to be passed to `tc_replica_sync`. + * + * ## Safety + * + * TCServer are not threadsafe, and must not be used with multiple replicas simultaneously. + */ +typedef struct TCServer TCServer; + /** * TCString supports passing strings into and out of the TaskChampion API. * @@ -339,8 +349,9 @@ void tc_kv_list_free(struct TCKVList *tckvs); struct TCReplica *tc_replica_new_in_memory(void); /** - * Create a new TCReplica with an on-disk database having the given filename. On error, a string - * is written to the `error_out` parameter (if it is not NULL) and NULL is returned. + * Create a new TCReplica with an on-disk database having the given filename. On error, a string + * is written to the error_out parameter (if it is not NULL) and NULL is returned. The caller + * must free this string. */ struct TCReplica *tc_replica_new_on_disk(struct TCString *path, struct TCString **error_out); @@ -389,6 +400,13 @@ struct TCTask *tc_replica_new_task(struct TCReplica *rep, */ struct TCTask *tc_replica_import_task_with_uuid(struct TCReplica *rep, struct TCUuid tcuuid); +/** + * Synchronize this replica with a server. + * + * The `server` argument remains owned by the caller, and must be freed explicitly. + */ +TCResult tc_replica_sync(struct TCReplica *rep, struct TCServer *server, bool avoid_snapshots); + /** * Undo local operations until the most recent UndoPoint. * @@ -425,6 +443,37 @@ struct TCString *tc_replica_error(struct TCReplica *rep); */ void tc_replica_free(struct TCReplica *rep); +/** + * Create a new TCServer that operates locally (on-disk). See the TaskChampion docs for the + * description of the arguments. + * + * On error, a string is written to the error_out parameter (if it is not NULL) and NULL is + * returned. The caller must free this string. + * + * The server must be freed after it is used - tc_replica_sync does not automatically free it. + */ +struct TCServer *tc_server_new_local(struct TCString *server_dir, struct TCString **error_out); + +/** + * Create a new TCServer that connects to a remote server. See the TaskChampion docs for the + * description of the arguments. + * + * On error, a string is written to the error_out parameter (if it is not NULL) and NULL is + * returned. The caller must free this string. + * + * The server must be freed after it is used - tc_replica_sync does not automatically free it. + */ +struct TCServer *tc_server_new_remote(struct TCString *origin, + struct TCUuid client_key, + struct TCString *encryption_secret, + struct TCString **error_out); + +/** + * Free a server. The server may not be used after this function returns and must not be freed + * more than once. + */ +void tc_server_free(struct TCServer *server); + /** * Create a new TCString referencing the given C string. The C string must remain valid and * unchanged until after the TCString is freed. It's typically easiest to ensure this by using a