diff --git a/Cargo.lock b/Cargo.lock index b2f438354..4f577c437 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3035,6 +3035,7 @@ dependencies = [ "cbindgen", "libc", "taskchampion", + "uuid", ] [[package]] diff --git a/binding-tests/Makefile b/binding-tests/Makefile index 3599821f4..620befab5 100644 --- a/binding-tests/Makefile +++ b/binding-tests/Makefile @@ -3,7 +3,7 @@ INC=-I ../lib LIB=-L ../target/debug RPATH=-Wl,-rpath,../target/debug -TESTS = replica.cpp +TESTS = replica.cpp uuid.cpp .PHONY: all test diff --git a/binding-tests/uuid.cpp b/binding-tests/uuid.cpp new file mode 100644 index 000000000..949d081f0 --- /dev/null +++ b/binding-tests/uuid.cpp @@ -0,0 +1,38 @@ +#include +#include "doctest.h" +#include "taskchampion.h" + +TEST_CASE("creating UUIDs does not crash") { + Uuid u1 = tc_uuid_new_v4(); + Uuid u2 = tc_uuid_nil(); +} + +TEST_CASE("converting UUIDs to string works") { + Uuid u2 = tc_uuid_nil(); + REQUIRE(TC_UUID_STRING_BYTES == 36); + + char u2str[TC_UUID_STRING_BYTES]; + tc_uuid_to_str(u2, u2str); + CHECK(strncmp(u2str, "00000000-0000-0000-0000-000000000000", TC_UUID_STRING_BYTES) == 0); +} + +TEST_CASE("converting UUIDs from string works") { + Uuid u; + char ustr[TC_UUID_STRING_BYTES] = "fdc314b7-f938-4845-b8d1-95716e4eb762"; + CHECK(tc_uuid_from_str(ustr, &u)); + CHECK(u._0[0] == 0xfd); + // .. if these two are correct, probably it worked :) + CHECK(u._0[15] == 0x62); +} + +TEST_CASE("converting invalid UUIDs from string fails as expected") { + Uuid u; + char ustr[TC_UUID_STRING_BYTES] = "not-a-valid-uuid"; + CHECK(!tc_uuid_from_str(ustr, &u)); +} + +TEST_CASE("converting invalid UTF-8 UUIDs from string fails as expected") { + Uuid u; + char ustr[TC_UUID_STRING_BYTES] = "\xf0\x28\x8c\xbc"; + CHECK(!tc_uuid_from_str(ustr, &u)); +} diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 5d9b44fc6..b01d1bcbd 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["cdylib"] [dependencies] libc = "0.2.113" taskchampion = { path = "../taskchampion" } +uuid = { version = "^0.8.2", features = ["serde", "v4"] } anyhow = "1.0" [build-dependencies] diff --git a/lib/build.rs b/lib/build.rs index 8d9db2f1f..74dfe0fc8 100644 --- a/lib/build.rs +++ b/lib/build.rs @@ -10,6 +10,16 @@ fn main() { .with_language(Language::C) .with_config(Config { cpp_compat: true, + export: ExportConfig { + item_types: vec![ + ItemType::Structs, + ItemType::Globals, + ItemType::Functions, + ItemType::Constants, + ItemType::OpaqueItems, + ], + ..Default::default() + }, ..Default::default() }) .generate() diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 78004cf88..98b7c0124 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1 +1,2 @@ pub mod replica; +pub mod uuid; diff --git a/lib/src/uuid.rs b/lib/src/uuid.rs new file mode 100644 index 000000000..5fbbe8d7a --- /dev/null +++ b/lib/src/uuid.rs @@ -0,0 +1,63 @@ +use libc; +use taskchampion::Uuid as TcUuid; + +/// Uuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. +#[repr(C)] +pub struct Uuid([u8; 16]); + +impl From for Uuid { + fn from(uuid: TcUuid) -> Uuid { + // TODO: can we avoid clone here? + Uuid(uuid.as_bytes().clone()) + } +} + +impl From for TcUuid { + fn from(uuid: Uuid) -> TcUuid { + TcUuid::from_bytes(uuid.0) + } +} + +/// Create a new, randomly-generated UUID. +#[no_mangle] +pub extern "C" fn tc_uuid_new_v4() -> Uuid { + TcUuid::new_v4().into() +} + +/// Create a new UUID with the nil value. +#[no_mangle] +pub extern "C" fn tc_uuid_nil() -> Uuid { + TcUuid::nil().into() +} + +/// Length, in bytes, of a C string containing a Uuid. +#[no_mangle] +pub static TC_UUID_STRING_BYTES: usize = ::uuid::adapter::Hyphenated::LENGTH; + +/// Write the string representation of a Uuid into the given buffer, which must be +/// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. +#[no_mangle] +pub extern "C" fn tc_uuid_to_str<'a>(uuid: Uuid, out: *mut libc::c_char) { + debug_assert!(!out.is_null()); + let buf: &'a mut [u8] = unsafe { + std::slice::from_raw_parts_mut(out as *mut u8, ::uuid::adapter::Hyphenated::LENGTH) + }; + let uuid: TcUuid = uuid.into(); + uuid.to_hyphenated().encode_lower(buf); +} + +/// Parse the given value as a UUID. The value must be exactly TC_UUID_STRING_BYTES long. Returns +/// false on failure. +#[no_mangle] +pub extern "C" fn tc_uuid_from_str<'a>(val: *const libc::c_char, out: *mut Uuid) -> bool { + debug_assert!(!val.is_null()); + debug_assert!(!out.is_null()); + let slice = unsafe { std::slice::from_raw_parts(val as *const u8, TC_UUID_STRING_BYTES) }; + if let Ok(s) = std::str::from_utf8(slice) { + if let Ok(u) = TcUuid::parse_str(s) { + unsafe { *out = u.into() }; + return true; + } + } + false +} diff --git a/lib/taskchampion.h b/lib/taskchampion.h index 081dd09cc..e5ad1de99 100644 --- a/lib/taskchampion.h +++ b/lib/taskchampion.h @@ -8,8 +8,15 @@ /// for querying and modifying that data. struct Replica; +/// Uuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. +struct Uuid { + uint8_t _0[16]; +}; + extern "C" { +extern const uintptr_t TC_UUID_STRING_BYTES; + /// Create a new Replica. /// /// If path is NULL, then an in-memory replica is created. Otherwise, path is the path to the @@ -34,4 +41,18 @@ const char *tc_replica_error(Replica *rep); /// Free a Replica. void tc_replica_free(Replica *rep); +/// Create a new, randomly-generated UUID. +Uuid tc_uuid_new_v4(); + +/// Create a new UUID with the nil value. +Uuid tc_uuid_nil(); + +/// Write the string representation of a Uuid into the given buffer, which must be +/// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. +void tc_uuid_to_str(Uuid uuid, char *out); + +/// Parse the given value as a UUID. The value must be exactly TC_UUID_STRING_BYTES long. Returns +/// false on failure. +bool tc_uuid_from_str(const char *val, Uuid *out); + } // extern "C"