diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 120000 index e1fd6d8b1..000000000 --- a/.cargo/config.toml +++ /dev/null @@ -1 +0,0 @@ -../config.toml \ No newline at end of file diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 4fbccafd9..0e8621486 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -61,39 +61,3 @@ jobs: with: command: fmt args: --all -- --check - - codegen: - runs-on: ubuntu-latest - name: "codegen" - steps: - - uses: actions/checkout@v4 - - - name: Cache cargo registry - uses: actions/cache@v4 - with: - path: ~/.cargo/registry - key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - - - name: Cache cargo build - uses: actions/cache@v4 - with: - path: target - key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} - - - uses: actions-rs/toolchain@v1 - with: - toolchain: "1.73.0" # MSRV - override: true - - - uses: actions-rs/cargo@v1.0.3 - with: - command: xtask - args: codegen - - - name: check for changes - run: | - if ! git diff; then - echo "Generated code not up-to-date; - run `cargo run --package xtask -- codegen` and commit the result"; - exit 1; - fi diff --git a/.gitmodules b/.gitmodules index cddc791b0..bcd2aa1c5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "src/libshared"] path = src/libshared url = https://github.com/GothenburgBitFactory/libshared.git -[submodule "src/tc/corrosion"] - path = src/tc/corrosion +[submodule "src/taskchampion-cpp/corrosion"] + path = src/taskchampion-cpp/corrosion url = https://github.com/corrosion-rs/corrosion.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a9d0e896..e12fcf675 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,13 +26,13 @@ if (ENABLE_WASM) endif (ENABLE_WASM) message ("-- Looking for git submodules") -if (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src AND EXISTS ${CMAKE_SOURCE_DIR}/src/tc/corrosion) +if (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src AND EXISTS ${CMAKE_SOURCE_DIR}/src/taskchampion-cpp/corrosion) message ("-- Found git submodules") else (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src) message ("-- Cloning git submodules") execute_process (COMMAND git submodule update --init WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) -endif (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src AND EXISTS ${CMAKE_SOURCE_DIR}/src/tc/corrosion) +endif (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src AND EXISTS ${CMAKE_SOURCE_DIR}/src/taskchampion-cpp/corrosion) message ("-- Looking for SHA1 references") if (EXISTS ${CMAKE_SOURCE_DIR}/.git/index) @@ -142,7 +142,7 @@ configure_file ( add_subdirectory (src) add_subdirectory (src/commands) -add_subdirectory (src/tc) +add_subdirectory (src/taskchampion-cpp) add_subdirectory (src/columns) add_subdirectory (doc) add_subdirectory (scripts) diff --git a/Cargo.lock b/Cargo.lock index 80491d09e..100bc047d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,7 +84,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -95,7 +95,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -203,6 +203,16 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -253,6 +263,50 @@ dependencies = [ "typenum", ] +[[package]] +name = "cxx" +version = "1.0.124" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273dcfd3acd4e1e276af13ed2a43eea7001318823e7a726a6b3ed39b4acc0b82" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0c11acd0e63bae27dcd2afced407063312771212b7a823b4fd72d633be30fb" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.124" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839fcd5e43464614ffaa989eaf1c139ef1f0c51672a1ed08023307fa1b909ccd" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.124" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2c1c1776b986979be68bb2285da855f8d8a35851a769fca8740df7c3d07877" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "der" version = "0.7.9" @@ -274,12 +328,6 @@ dependencies = [ "serde", ] -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - [[package]] name = "digest" version = "0.10.7" @@ -290,12 +338,6 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "either" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" - [[package]] name = "encoding_rs" version = "0.8.34" @@ -323,29 +365,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" -[[package]] -name = "ffizz-header" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a1a52e9f00aa4c639059d977e1289a13963117d8e60ccb25e86cca2aab98538" -dependencies = [ - "ffizz-macros", - "itertools", - "linkme", -] - -[[package]] -name = "ffizz-macros" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af208cea557bab3ec4f05fb0c26460f1c61fdb204f3738f471b6b1ecd58d7a04" -dependencies = [ - "itertools", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "flate2" version = "1.0.30" @@ -400,7 +419,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -720,15 +739,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.11" @@ -777,23 +787,12 @@ dependencies = [ ] [[package]] -name = "linkme" -version = "0.3.27" +name = "link-cplusplus" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccb76662d78edc9f9bf56360d6919bdacc8b7761227727e5082f128eeb90bbf5" +checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" dependencies = [ - "linkme-impl", -] - -[[package]] -name = "linkme-impl" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dccda732e04fa3baf2e17cf835bfe2601c7c2edafd64417c627dabae3a8cda" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", + "cc", ] [[package]] @@ -995,16 +994,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "pretty_assertions" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" -dependencies = [ - "diff", - "yansi", -] - [[package]] name = "proc-macro2" version = "1.0.85" @@ -1152,20 +1141,6 @@ dependencies = [ "sct", ] -[[package]] -name = "rustls" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" -dependencies = [ - "log", - "ring", - "rustls-pki-types", - "rustls-webpki 0.102.4", - "subtle", - "zeroize", -] - [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -1193,9 +1168,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.4" +version = "0.102.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" dependencies = [ "ring", "rustls-pki-types", @@ -1220,6 +1195,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scratch" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" + [[package]] name = "sct" version = "0.7.1" @@ -1247,7 +1228,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1353,7 +1334,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.66", + "syn", ] [[package]] @@ -1362,17 +1343,6 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d0208408ba0c3df17ed26eb06992cb1a1268d41b2c0e12e65203fbe3972cee5" -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.66" @@ -1413,9 +1383,9 @@ dependencies = [ [[package]] name = "taskchampion" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55b167a2bea718f6f75f68c8d29f1550a6095d8917504d3b9c62626f4c4ef7cb" +checksum = "664ff35fa1777f8a7aabec512bf43926e14fb073de947b588355bea1814c3577" dependencies = [ "anyhow", "byteorder", @@ -1440,13 +1410,20 @@ dependencies = [ name = "taskchampion-lib" version = "0.1.0" dependencies = [ - "anyhow", - "ffizz-header", - "libc", - "pretty_assertions", + "cxx", + "cxx-build", "taskchampion", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.61" @@ -1464,7 +1441,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1539,7 +1516,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1590,7 +1567,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1644,6 +1621,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "untrusted" version = "0.9.0" @@ -1652,17 +1635,16 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.9.7" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11a831e3c0b56e438a28308e7c810799e3c118417f342d30ecec080105395cd" +checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" dependencies = [ "base64 0.22.1", "flate2", "log", "once_cell", - "rustls 0.22.4", + "rustls 0.23.12", "rustls-pki-types", - "rustls-webpki 0.102.4", "url", "webpki-roots 0.26.3", ] @@ -1686,9 +1668,9 @@ checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "uuid" -version = "1.9.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", "serde", @@ -1742,7 +1724,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "syn", "wasm-bindgen-shared", ] @@ -1776,7 +1758,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1983,21 +1965,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "xtask" -version = "0.4.1" -dependencies = [ - "anyhow", - "regex", - "taskchampion-lib", -] - -[[package]] -name = "yansi" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" - [[package]] name = "zerocopy" version = "0.7.34" @@ -2015,7 +1982,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b8e486d6a..7dabb449b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,7 @@ [workspace] members = [ - "src/tc/lib", - "xtask", + "src/taskchampion-cpp", ] resolver = "2" diff --git a/config.toml b/config.toml deleted file mode 100644 index 35049cbcb..000000000 --- a/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[alias] -xtask = "run --package xtask --" diff --git a/doc/devel/contrib/rust-and-c++.md b/doc/devel/contrib/rust-and-c++.md index 5317985e3..d658e850d 100644 --- a/doc/devel/contrib/rust-and-c++.md +++ b/doc/devel/contrib/rust-and-c++.md @@ -7,15 +7,12 @@ Taskwarrior has historically been a C++ project, but as of taskwarrior-3.0.0, th TaskChampion implements storage and access to "replicas" containing a user's tasks. It defines an abstract model for this data, and also provides a simple Rust API for manipulating replicas. It also defines a method of synchronizing replicas and provides an implementation of that method in the form of a sync server. -TaskChampion provides a C interface via the `taskchampion-lib` crate, at `src/tc/lib`. Other applications, besides Taskwarrior, can use TaskChampion to manage tasks. Taskwarrior is just one application using the TaskChampion interface. ## Taskwarrior's use of TaskChampion -Taskwarrior's interface to TaskChampion has a few layers: - -* A Rust library, `takschampion-lib`, that presents `extern "C"` functions for use from C++, essentially defining a C interface to TaskChampion. -* C++ wrappers for the types from `taskchampion-lib`, defined in [`src/tc`](../../src/tc), ensuring memory safety (with `unique_ptr`) and adding methods corresponding to the Rust API's methods. - The wrapper types are in the C++ namespace, `tc`. +Taskwarrior's interface to TaskChampion is in `src/taskchampion-cpp`. +This links to `taskchampion` as a Rust dependency, and uses [cxx](https://cxx.rs) to build a C++ API for it. +That API is defined, and documented, in `src/taskchampion-cpp/src/lib.rs`, and available in the `tc` namespace in C++ code. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1c269770b..15530c776 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,8 +1,6 @@ cmake_minimum_required (VERSION 3.22) include_directories (${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/src - ${CMAKE_SOURCE_DIR}/src/tc - ${CMAKE_SOURCE_DIR}/src/tc/lib ${CMAKE_SOURCE_DIR}/src/commands ${CMAKE_SOURCE_DIR}/src/columns ${CMAKE_SOURCE_DIR}/src/libshared/src @@ -28,6 +26,7 @@ add_library (task STATIC CLI2.cpp CLI2.h rules.cpp sort.cpp util.cpp util.h) +target_link_libraries(task taskchampion-cpp) add_library (libshared STATIC libshared/src/Color.cpp libshared/src/Color.h libshared/src/Configuration.cpp libshared/src/Configuration.h @@ -52,10 +51,9 @@ add_executable (calc_executable calc.cpp) add_executable (lex_executable lex.cpp) # Yes, 'task' (and hence libshared) is included twice, otherwise linking fails on assorted OSes. -# Similarly for `tc`. -target_link_libraries (task_executable task tc commands tc columns libshared task libshared ${TASK_LIBRARIES}) -target_link_libraries (calc_executable task tc commands tc columns libshared task libshared ${TASK_LIBRARIES}) -target_link_libraries (lex_executable task tc commands tc columns libshared task libshared ${TASK_LIBRARIES}) +target_link_libraries (task_executable task commands columns libshared task libshared ${TASK_LIBRARIES}) +target_link_libraries (calc_executable task commands columns libshared task libshared ${TASK_LIBRARIES}) +target_link_libraries (lex_executable task commands columns libshared task libshared ${TASK_LIBRARIES}) if (DARWIN) # SystemConfiguration is required by Rust libraries like reqwest, to get proxy configuration. target_link_libraries (task_executable "-framework CoreFoundation -framework Security -framework SystemConfiguration") diff --git a/src/Context.cpp b/src/Context.cpp index 363c1d640..3ca9e6c2c 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -37,9 +37,11 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -681,6 +683,11 @@ int Context::initialize(int argc, const char** argv) { rc = 2; } + catch (rust::Error& err) { + error(err.what()); + rc = 2; + } + catch (int) { // Hooks can terminate processing by throwing integers. rc = 4; @@ -689,7 +696,7 @@ int Context::initialize(int argc, const char** argv) { catch (const std::regex_error& e) { std::cout << "regex_error caught: " << e.what() << '\n'; } catch (...) { - error("knknown error. Please report."); + error("Unknown error. Please report."); rc = 3; } @@ -772,6 +779,11 @@ int Context::run() { rc = 2; } + catch (rust::Error& err) { + error(err.what()); + rc = 2; + } + catch (int) { // Hooks can terminate processing by throwing integers. rc = 4; diff --git a/src/TDB2.cpp b/src/TDB2.cpp index d175a61ff..30009b42c 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -46,21 +46,12 @@ #include #include -#include "tc/Server.h" -#include "tc/util.h" - bool TDB2::debug_mode = false; static void dependency_scan(std::vector&); -//////////////////////////////////////////////////////////////////////////////// -TDB2::TDB2() - : replica{tc::Replica()} // in-memory Replica - , - _working_set{} {} - //////////////////////////////////////////////////////////////////////////////// void TDB2::open_replica(const std::string& location, bool create_if_missing) { - replica = tc::Replica(location, create_if_missing); + _replica = tc::new_replica_on_disk(location, create_if_missing); } //////////////////////////////////////////////////////////////////////////////// @@ -71,54 +62,35 @@ void TDB2::add(Task& task) { // properties not otherwise given. task.validate(true); - std::string uuid = task.get("uuid"); + rust::Vec ops; + maybe_add_undo_point(ops); + + auto uuid = task.get("uuid"); changes[uuid] = task; + tc::Uuid tcuuid = tc::uuid_from_string(uuid); // run hooks for this new task Context::getContext().hooks.onAdd(task); - auto innertask = replica.import_task_with_uuid(uuid); + auto taskdata = tc::create_task(tcuuid, ops); - { - auto guard = replica.mutate_task(innertask); - - // add the task attributes - for (auto& attr : task.all()) { - // TaskChampion does not store uuid or id in the taskmap - if (attr == "uuid" || attr == "id") { - continue; - } - - // Use `set_status` for the task status, to get expected behavior - // with respect to the working set. - else if (attr == "status") { - innertask.set_status(Task::status2tc(Task::textToStatus(task.get(attr)))); - } - - // use `set_modified` to set the modified timestamp, avoiding automatic - // updates to this field by TaskChampion. - else if (attr == "modified") { - auto mod = (time_t)std::stoi(task.get(attr)); - innertask.set_modified(mod); - } - - // otherwise, just set the k/v map value - else { - innertask.set_value(attr, std::make_optional(task.get(attr))); - } + // add the task attributes + for (auto& attr : task.all()) { + // TaskChampion does not store uuid or id in the task data + if (attr == "uuid" || attr == "id") { + continue; } - } - auto ws = replica.working_set(); + taskdata->update(attr, task.get(attr), ops); + } + replica()->commit_operations(std::move(ops)); + + invalidate_cached_info(); // get the ID that was assigned to this task - auto id = ws.by_uuid(uuid); - - // update the cached working set with the new information - _working_set = std::make_optional(std::move(ws)); - - if (id.has_value()) { - task.id = id.value(); + auto id = working_set()->by_uuid(tcuuid); + if (id > 0) { + task.id = id; } } @@ -144,6 +116,9 @@ void TDB2::modify(Task& task) { task.validate(false); auto uuid = task.get("uuid"); + rust::Vec ops; + maybe_add_undo_point(ops); + changes[uuid] = task; // invoke the hook and allow it to modify the task before updating @@ -151,14 +126,15 @@ void TDB2::modify(Task& task) { get(uuid, original); Context::getContext().hooks.onModify(original, task); - auto maybe_tctask = replica.get_task(uuid); - if (!maybe_tctask.has_value()) { + tc::Uuid tcuuid = tc::uuid_from_string(uuid); + auto maybe_tctask = replica()->get_task_data(tcuuid); + if (maybe_tctask.is_none()) { throw std::string("task no longer exists"); } - auto tctask = std::move(maybe_tctask.value()); - auto guard = replica.mutate_task(tctask); - auto tctask_map = tctask.get_taskmap(); + auto tctask = maybe_tctask.take(); + // Perform the necessary `update` operations to set all keys in `tctask` + // equal to those in `task`. std::unordered_set seen; for (auto k : task.all()) { // ignore task keys that aren't stored @@ -168,45 +144,76 @@ void TDB2::modify(Task& task) { seen.insert(k); bool update = false; auto v_new = task.get(k); - try { - auto v_tctask = tctask_map.at(k); + std::string v_tctask; + if (tctask->get(k, v_tctask)) { update = v_tctask != v_new; - } catch (const std::out_of_range& oor) { - // tctask_map does not contain k, so update it + } else { + // tctask does not contain k, so update it update = true; } if (update) { // An empty string indicates the value should be removed. if (v_new == "") { - tctask.set_value(k, {}); + tctask->update_remove(k, ops); } else { - tctask.set_value(k, make_optional(v_new)); + tctask->update(k, v_new, ops); } } } // we've now added and updated properties; but must find any deleted properties - for (auto kv : tctask_map) { - if (seen.find(kv.first) == seen.end()) { - tctask.set_value(kv.first, {}); + for (auto k : tctask->properties()) { + auto kstr = static_cast(k); + if (seen.find(kstr) == seen.end()) { + tctask->update_remove(kstr, ops); } } + + replica()->commit_operations(std::move(ops)); + + invalidate_cached_info(); } //////////////////////////////////////////////////////////////////////////////// void TDB2::purge(Task& task) { - auto uuid = task.get("uuid"); - replica.delete_task(uuid); + auto uuid = tc::uuid_from_string(task.get("uuid")); + rust::Vec ops; + auto maybe_tctask = replica()->get_task_data(uuid); + if (maybe_tctask.is_some()) { + auto tctask = maybe_tctask.take(); + tctask->delete_task(ops); + replica()->commit_operations(std::move(ops)); + } + + invalidate_cached_info(); } //////////////////////////////////////////////////////////////////////////////// -const tc::WorkingSet& TDB2::working_set() { +rust::Box& TDB2::replica() { + // Create a replica in-memory if `open_replica` has not been called. This + // occurs in tests. + if (!_replica) { + _replica = tc::new_replica_in_memory(); + } + return _replica.value(); +} + +//////////////////////////////////////////////////////////////////////////////// +const rust::Box& TDB2::working_set() { if (!_working_set.has_value()) { - _working_set = std::make_optional(replica.working_set()); + _working_set = replica()->working_set(); } return _working_set.value(); } +//////////////////////////////////////////////////////////////////////////////// +void TDB2::maybe_add_undo_point(rust::Vec& ops) { + // Only add an UndoPoint if there are not yet any changes. + if (changes.size() == 0) { + tc::add_undo_point(ops); + } +} + //////////////////////////////////////////////////////////////////////////////// void TDB2::get_changes(std::vector& changes) { std::map& changes_map = this->changes; @@ -217,47 +224,58 @@ void TDB2::get_changes(std::vector& changes) { //////////////////////////////////////////////////////////////////////////////// void TDB2::revert() { - auto undo_ops = replica.get_undo_ops(); - if (undo_ops.len == 0) { + rust::Vec undo_ops = replica()->get_undo_operations(); + if (undo_ops.size() == 0) { std::cout << "No operations to undo."; return; } if (confirm_revert(undo_ops)) { - // Has the side-effect of freeing undo_ops. - replica.commit_undo_ops(undo_ops, NULL); - } else { - replica.free_replica_ops(undo_ops); + // Note that commit_reversed_operations rebuilds the working set, so that + // need not be done here. + if (!replica()->commit_reversed_operations(std::move(undo_ops))) { + std::cout << "Could not undo: other operations have occurred."; + } } - replica.rebuild_working_set(false); } //////////////////////////////////////////////////////////////////////////////// -bool TDB2::confirm_revert(struct tc::ffi::TCReplicaOpList undo_ops) { +bool TDB2::confirm_revert(rust::Vec& undo_ops) { // TODO Use show_diff rather than this basic listing of operations, though // this might be a worthy undo.style itself. - std::cout << "The following " << undo_ops.len << " operations would be reverted:\n"; - for (size_t i = 0; i < undo_ops.len; i++) { + std::cout << "The following " << undo_ops.size() << " operations would be reverted:\n"; + for (auto& op : undo_ops) { + if (op.is_undo_point()) { + continue; + } + std::cout << "- "; - tc::ffi::TCReplicaOp op = undo_ops.items[i]; - switch (op.operation_type) { - case tc::ffi::TCReplicaOpType::Create: - std::cout << "Create " << replica.get_op_uuid(op); - break; - case tc::ffi::TCReplicaOpType::Delete: - std::cout << "Delete " << replica.get_op_old_task_description(op); - break; - case tc::ffi::TCReplicaOpType::Update: - std::cout << "Update " << replica.get_op_uuid(op) << "\n"; - std::cout << " " << replica.get_op_property(op) << ": " - << option_string(replica.get_op_old_value(op)) << " -> " - << option_string(replica.get_op_value(op)); - break; - case tc::ffi::TCReplicaOpType::UndoPoint: - throw std::string("Can't undo UndoPoint."); - break; - default: - throw std::string("Can't undo non-operation."); - break; + std::string uuid = static_cast(op.get_uuid().to_string()); + if (op.is_create()) { + std::cout << "Create " << uuid; + } else if (op.is_delete()) { + std::cout << "Delete "; + auto old_task = op.get_old_task(); + bool found_description = false; + for (auto& pv : old_task) { + if (static_cast(pv.prop) == "description") { + std::cout << static_cast(pv.value) << " (" << uuid << ")"; + found_description = true; + } + } + if (!found_description) { + std::cout << uuid; + } + } else if (op.is_update()) { + std::cout << "Update " << uuid << "\n"; + std::string property; + op.get_property(property); + std::string value; + bool have_value = op.get_value(value); + std::string old_value; + bool have_old_value = op.get_old_value(old_value); + std::cout << " " << property << ": "; + std::cout << (have_old_value ? old_value : "") << " -> "; + std::cout << (have_value ? value : ""); } std::cout << "\n"; } @@ -265,11 +283,9 @@ bool TDB2::confirm_revert(struct tc::ffi::TCReplicaOpList undo_ops) { confirm( "The undo command is not reversible. Are you sure you want to revert to the previous " "state?"); + return true; } -//////////////////////////////////////////////////////////////////////////////// -std::string TDB2::option_string(std::string input) { return input == "" ? "" : input; } - //////////////////////////////////////////////////////////////////////////////// void TDB2::show_diff(const std::string& current, const std::string& prior, const std::string& when) { @@ -305,78 +321,102 @@ void TDB2::gc() { // Allowed as an override, but not recommended. if (Context::getContext().config.getBoolean("gc")) { - replica.rebuild_working_set(true); + replica()->rebuild_working_set(true); } Context::getContext().time_gc_us += timer.total_us(); } //////////////////////////////////////////////////////////////////////////////// -void TDB2::expire_tasks() { replica.expire_tasks(); } +void TDB2::expire_tasks() { replica()->expire_tasks(); } //////////////////////////////////////////////////////////////////////////////// // Latest ID is that of the last pending task. int TDB2::latest_id() { - const tc::WorkingSet& ws = working_set(); - return (int)ws.largest_index(); + auto& ws = working_set(); + return (int)ws->largest_index(); } //////////////////////////////////////////////////////////////////////////////// const std::vector TDB2::all_tasks() { - auto all_tctasks = replica.all_tasks(); + auto all_tctasks = replica()->all_task_data(); std::vector all; - for (auto& tctask : all_tctasks) all.push_back(Task(std::move(tctask))); + for (auto& maybe_tctask : all_tctasks) { + auto tctask = maybe_tctask.take(); + all.push_back(Task(std::move(tctask))); + } + + dependency_scan(all); return all; } //////////////////////////////////////////////////////////////////////////////// const std::vector TDB2::pending_tasks() { - const tc::WorkingSet& ws = working_set(); - auto largest_index = ws.largest_index(); + if (!_pending_tasks) { + auto& ws = working_set(); + auto largest_index = ws->largest_index(); - std::vector result; - for (size_t i = 0; i <= largest_index; i++) { - auto maybe_uuid = ws.by_index(i); - if (maybe_uuid.has_value()) { - auto maybe_task = replica.get_task(maybe_uuid.value()); - if (maybe_task.has_value()) { - result.push_back(Task(std::move(maybe_task.value()))); + std::vector result; + for (size_t i = 0; i <= largest_index; i++) { + auto uuid = ws->by_index(i); + if (!uuid.is_nil()) { + auto maybe_task = replica()->get_task_data(uuid); + if (maybe_task.is_some()) { + result.push_back(Task(maybe_task.take())); + } } } + + dependency_scan(result); + + _pending_tasks = result; } - dependency_scan(result); - - return result; + return *_pending_tasks; } //////////////////////////////////////////////////////////////////////////////// const std::vector TDB2::completed_tasks() { - auto all_tctasks = replica.all_tasks(); - const tc::WorkingSet& ws = working_set(); + if (!_completed_tasks) { + auto all_tctasks = replica()->all_task_data(); + auto& ws = working_set(); - std::vector result; - for (auto& tctask : all_tctasks) { - // if this task is _not_ in the working set, return it. - if (!ws.by_uuid(tctask.get_uuid())) { - result.push_back(Task(std::move(tctask))); + std::vector result; + for (auto& maybe_tctask : all_tctasks) { + auto tctask = maybe_tctask.take(); + // if this task is _not_ in the working set, return it. + if (ws->by_uuid(tctask->get_uuid()) == 0) { + result.push_back(Task(std::move(tctask))); + } } + _completed_tasks = result; } + return *_completed_tasks; +} - return result; +//////////////////////////////////////////////////////////////////////////////// +void TDB2::invalidate_cached_info() { + _pending_tasks = std::nullopt; + _completed_tasks = std::nullopt; + _working_set = std::nullopt; } //////////////////////////////////////////////////////////////////////////////// // Locate task by ID, wherever it is. bool TDB2::get(int id, Task& task) { - const tc::WorkingSet& ws = working_set(); - const auto maybe_uuid = ws.by_index(id); - if (maybe_uuid) { - auto maybe_task = replica.get_task(*maybe_uuid); - if (maybe_task) { - task = Task{std::move(*maybe_task)}; - return true; + auto& ws = working_set(); + const auto tcuuid = ws->by_index(id); + if (!tcuuid.is_nil()) { + std::string uuid = static_cast(tcuuid.to_string()); + // Load all pending tasks in order to get dependency data, and in particular + // `task.is_blocking` and `task.is_blocked`, set correctly. + std::vector pending = pending_tasks(); + for (auto& pending_task : pending) { + if (pending_task.get("uuid") == uuid) { + task = pending_task; + return true; + } } } @@ -386,22 +426,23 @@ bool TDB2::get(int id, Task& task) { //////////////////////////////////////////////////////////////////////////////// // Locate task by UUID, including by partial ID, wherever it is. bool TDB2::get(const std::string& uuid, Task& task) { + // Load all pending tasks in order to get dependency data, and in particular + // `task.is_blocking` and `task.is_blocked`, set correctly. + std::vector pending = pending_tasks(); + // try by raw uuid, if the length is right - if (uuid.size() == 36) { - try { - auto maybe_task = replica.get_task(uuid); - if (maybe_task) { - task = Task{std::move(*maybe_task)}; - return true; - } - } catch (const std::string& err) { - return false; + for (auto& pending_task : pending) { + if (closeEnough(pending_task.get("uuid"), uuid, uuid.length())) { + task = pending_task; + return true; } } - // Nothing to do but iterate over all tasks and check whether it's closeEnough - for (auto& tctask : replica.all_tasks()) { - if (closeEnough(tctask.get_uuid(), uuid, uuid.length())) { + // Nothing to do but iterate over all tasks and check whether it's closeEnough. + for (auto& maybe_tctask : replica()->all_task_data()) { + auto tctask = maybe_tctask.take(); + auto tctask_uuid = static_cast(tctask->get_uuid().to_string()); + if (closeEnough(tctask_uuid, uuid, uuid.length())) { task = Task{std::move(tctask)}; return true; } @@ -446,63 +487,59 @@ const std::vector TDB2::children(Task& parent) { std::vector results; std::string this_uuid = parent.get("uuid"); - const tc::WorkingSet& ws = working_set(); - size_t end_idx = ws.largest_index(); + auto& ws = working_set(); + size_t end_idx = ws->largest_index(); for (size_t i = 0; i <= end_idx; i++) { - auto uuid_opt = ws.by_index(i); - if (!uuid_opt) { + auto uuid = ws->by_index(i); + if (uuid.is_nil()) { continue; } - auto uuid = uuid_opt.value(); // skip self-references - if (uuid == this_uuid) { + if (uuid.to_string() == this_uuid) { continue; } - auto task_opt = replica.get_task(uuid_opt.value()); - if (!task_opt) { + auto task_opt = replica()->get_task_data(uuid); + if (task_opt.is_none()) { continue; } - auto task = std::move(task_opt.value()); + auto task = task_opt.take(); - auto parent_uuid_opt = task.get_value("parent"); - if (!parent_uuid_opt) { + std::string parent_uuid; + if (!task->get("parent", parent_uuid)) { continue; } - auto parent_uuid = parent_uuid_opt.value(); if (parent_uuid == this_uuid) { results.push_back(Task(std::move(task))); } } - return results; } //////////////////////////////////////////////////////////////////////////////// std::string TDB2::uuid(int id) { - const tc::WorkingSet& ws = working_set(); - return ws.by_index((size_t)id).value_or(""); + auto& ws = working_set(); + auto uuid = ws->by_index(id); + if (uuid.is_nil()) { + return ""; + } + return static_cast(uuid.to_string()); } //////////////////////////////////////////////////////////////////////////////// int TDB2::id(const std::string& uuid) { - const tc::WorkingSet& ws = working_set(); - return (int)ws.by_uuid(uuid).value_or(0); + auto& ws = working_set(); + return ws->by_uuid(tc::uuid_from_string(uuid)); } //////////////////////////////////////////////////////////////////////////////// -int TDB2::num_local_changes() { return (int)replica.num_local_operations(); } +int TDB2::num_local_changes() { return (int)replica()->num_local_operations(); } //////////////////////////////////////////////////////////////////////////////// -int TDB2::num_reverts_possible() { return (int)replica.num_undo_points(); } - -//////////////////////////////////////////////////////////////////////////////// -void TDB2::sync(tc::Server server, bool avoid_snapshots) { - replica.sync(std::move(server), avoid_snapshots); -} +int TDB2::num_reverts_possible() { return (int)replica()->num_undo_points(); } //////////////////////////////////////////////////////////////////////////////// void TDB2::dump() { diff --git a/src/TDB2.h b/src/TDB2.h index 03c8dca5b..9de0d9d7e 100644 --- a/src/TDB2.h +++ b/src/TDB2.h @@ -30,25 +30,21 @@ #include #include #include -#include -#include +#include #include +#include #include #include #include #include -namespace tc { -class Server; -} - // TDB2 Class represents all the files in the task database. class TDB2 { public: static bool debug_mode; - TDB2(); + TDB2() = default; void open_replica(const std::string &, bool create_if_missing); void add(Task &); @@ -79,18 +75,24 @@ class TDB2 { void dump(); - void sync(tc::Server server, bool avoid_snapshots); - bool confirm_revert(struct tc::ffi::TCReplicaOpList); + bool confirm_revert(rust::Vec &); + + rust::Box &replica(); private: - tc::Replica replica; - std::optional _working_set; + std::optional> _replica; + + // Cached information from the replica + std::optional> _working_set; + std::optional> _pending_tasks; + std::optional> _completed_tasks; + void invalidate_cached_info(); // UUID -> Task containing all tasks modified in this invocation. std::map changes; - const tc::WorkingSet &working_set(); - static std::string option_string(std::string input); + const rust::Box &working_set(); + void maybe_add_undo_point(rust::Vec &); static void show_diff(const std::string &, const std::string &, const std::string &); }; diff --git a/src/Task.cpp b/src/Task.cpp index a62533cf7..6474c4384 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -138,7 +138,7 @@ Task::Task(const json::object* obj) { } //////////////////////////////////////////////////////////////////////////////// -Task::Task(tc::Task obj) { +Task::Task(rust::Box obj) { id = 0; urgency_value = 0.0; recalc_urgency = true; @@ -146,7 +146,7 @@ Task::Task(tc::Task obj) { is_blocking = false; annotation_count = 0; - parseTC(obj); + parseTC(std::move(obj)); } //////////////////////////////////////////////////////////////////////////////// @@ -717,8 +717,12 @@ void Task::parseJSON(const json::object* root_obj) { //////////////////////////////////////////////////////////////////////////////// // Note that all fields undergo encode/decode. -void Task::parseTC(const tc::Task& task) { - data = task.get_taskmap(); +void Task::parseTC(rust::Box task) { + auto items = task->items(); + data.clear(); + for (auto& item : items) { + data[static_cast(item.prop)] = static_cast(item.value); + } // count annotations annotation_count = 0; @@ -728,11 +732,8 @@ void Task::parseTC(const tc::Task& task) { } } - data["uuid"] = task.get_uuid(); + data["uuid"] = static_cast(task->get_uuid().to_string()); id = Context::getContext().tdb2.id(data["uuid"]); - - is_blocking = task.is_blocking(); - is_blocked = task.is_blocked(); } //////////////////////////////////////////////////////////////////////////////// @@ -1602,40 +1603,6 @@ const std::string Task::decode(const std::string& value) const { return str_replace(modified, "&close;", "]"); } -//////////////////////////////////////////////////////////////////////////////// -tc::Status Task::status2tc(const Task::status status) { - switch (status) { - case Task::pending: - return tc::Status::Pending; - case Task::completed: - return tc::Status::Completed; - case Task::deleted: - return tc::Status::Deleted; - case Task::waiting: - return tc::Status::Pending; // waiting is no longer a status - case Task::recurring: - return tc::Status::Recurring; - default: - return tc::Status::Unknown; - } -} - -//////////////////////////////////////////////////////////////////////////////// -Task::status Task::tc2status(const tc::Status status) { - switch (status) { - case tc::Status::Pending: - return Task::pending; - case tc::Status::Completed: - return Task::completed; - case tc::Status::Deleted: - return Task::deleted; - case tc::Status::Recurring: - return Task::recurring; - default: - return Task::pending; - } -} - //////////////////////////////////////////////////////////////////////////////// int Task::determineVersion(const std::string& line) { // Version 2 looks like: diff --git a/src/Task.h b/src/Task.h index 28acb5c08..08d508f0a 100644 --- a/src/Task.h +++ b/src/Task.h @@ -31,7 +31,7 @@ #include #include #include -#include +#include #include #include @@ -66,7 +66,7 @@ class Task { bool operator!=(const Task&); Task(const std::string&); Task(const json::object*); - Task(tc::Task); + Task(rust::Box); void parse(const std::string&); std::string composeJSON(bool decorate = false) const; @@ -88,8 +88,6 @@ class Task { // Series of helper functions. static status textToStatus(const std::string&); static std::string statusToText(status); - static tc::Status status2tc(const Task::status); - static Task::status tc2status(const tc::Status); void setAsNow(const std::string&); bool has(const std::string&) const; @@ -186,7 +184,7 @@ class Task { int determineVersion(const std::string&); void parseJSON(const std::string&); void parseJSON(const json::object*); - void parseTC(const tc::Task&); + void parseTC(rust::Box); void parseLegacy(const std::string&); void validate_before(const std::string&, const std::string&); const std::string encode(const std::string&) const; diff --git a/src/columns/CMakeLists.txt b/src/columns/CMakeLists.txt index ced88cb77..575d7c660 100644 --- a/src/columns/CMakeLists.txt +++ b/src/columns/CMakeLists.txt @@ -1,8 +1,6 @@ cmake_minimum_required (VERSION 3.22) include_directories (${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/src - ${CMAKE_SOURCE_DIR}/src/tc - ${CMAKE_SOURCE_DIR}/src/tc/lib ${CMAKE_SOURCE_DIR}/src/commands ${CMAKE_SOURCE_DIR}/src/columns ${CMAKE_SOURCE_DIR}/src/libshared/src @@ -39,6 +37,7 @@ set (columns_SRCS Column.cpp Column.h ColWait.cpp ColWait.h) add_library (columns STATIC ${columns_SRCS}) +target_link_libraries(columns taskchampion-cpp) #SET(CMAKE_BUILD_TYPE gcov) #SET(CMAKE_CXX_FLAGS_GCOV "--coverage") diff --git a/src/commands/CMakeLists.txt b/src/commands/CMakeLists.txt index 16b8025af..e12f5cc0e 100644 --- a/src/commands/CMakeLists.txt +++ b/src/commands/CMakeLists.txt @@ -1,8 +1,6 @@ cmake_minimum_required (VERSION 3.22) include_directories (${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/src - ${CMAKE_SOURCE_DIR}/src/tc - ${CMAKE_SOURCE_DIR}/src/tc/lib ${CMAKE_SOURCE_DIR}/src/commands ${CMAKE_SOURCE_DIR}/src/columns ${CMAKE_SOURCE_DIR}/src/libshared/src @@ -61,6 +59,7 @@ set (commands_SRCS Command.cpp Command.h CmdVersion.cpp CmdVersion.h) add_library (commands STATIC ${commands_SRCS}) +target_link_libraries(commands taskchampion-cpp) #SET(CMAKE_BUILD_TYPE gcov) #SET(CMAKE_CXX_FLAGS_GCOV "--coverage") diff --git a/src/commands/CmdImport.h b/src/commands/CmdImport.h index e46f9a0d5..6ae736156 100644 --- a/src/commands/CmdImport.h +++ b/src/commands/CmdImport.h @@ -31,6 +31,7 @@ #include #include +#include class CmdImport : public Command { public: diff --git a/src/commands/CmdSync.cpp b/src/commands/CmdSync.cpp index 6245b5acc..361df7f4d 100644 --- a/src/commands/CmdSync.cpp +++ b/src/commands/CmdSync.cpp @@ -35,12 +35,11 @@ #include #include #include +#include #include #include -#include "tc/Server.h" - //////////////////////////////////////////////////////////////////////////////// CmdSync::CmdSync() { _keyword = "synchronize"; @@ -60,56 +59,53 @@ CmdSync::CmdSync() { int CmdSync::execute(std::string& output) { int status = 0; - tc::Server server; - std::string server_ident; + Context& context = Context::getContext(); + auto& replica = context.tdb2.replica(); + std::stringstream out; + bool avoid_snapshots = false; + bool verbose = Context::getContext().verbose("sync"); // If no server is set up, quit. std::string origin = Context::getContext().config.get("sync.server.origin"); std::string url = Context::getContext().config.get("sync.server.url"); std::string server_dir = Context::getContext().config.get("sync.local.server_dir"); + std::string client_id = Context::getContext().config.get("sync.server.client_id"); std::string gcp_credential_path = Context::getContext().config.get("sync.gcp.credential_path"); std::string gcp_bucket = Context::getContext().config.get("sync.gcp.bucket"); std::string encryption_secret = Context::getContext().config.get("sync.encryption_secret"); // sync.server.origin is a deprecated synonym for sync.server.url std::string server_url = url == "" ? origin : url; - - if (server_dir != "") { - server = tc::Server::new_local(server_dir); - server_ident = server_dir; - } else if (gcp_bucket != "") { - if (encryption_secret == "") { - throw std::string("sync.encryption_secret is required"); - } - server = tc::Server::new_gcp(gcp_bucket, gcp_credential_path, encryption_secret); - std::ostringstream os; - os << "GCP bucket " << gcp_bucket; - server_ident = os.str(); - } else if (server_url != "") { - std::string client_id = Context::getContext().config.get("sync.server.client_id"); - if (client_id == "" || encryption_secret == "") { - throw std::string("sync.server.client_id and sync.encryption_secret are required"); - } - server = tc::Server::new_sync(server_url, client_id, encryption_secret); - std::ostringstream os; - os << "Sync server at " << server_url; - server_ident = os.str(); - } else { - throw std::string("No sync.* settings are configured. See task-sync(5)."); - } - - std::stringstream out; if (origin != "") { out << "sync.server.origin is deprecated. Use sync.server.url instead.\n"; } - if (Context::getContext().verbose("sync")) { - out << format("Syncing with {1}", server_ident) << '\n'; + if (server_dir != "") { + if (verbose) { + out << format("Syncing with {1}", server_dir) << '\n'; + } + replica->sync_to_local(server_dir, avoid_snapshots); + } else if (gcp_bucket != "") { + if (encryption_secret == "") { + throw std::string("sync.encryption_secret is required"); + } + if (verbose) { + out << format("Syncing with GCP bucket {1}", gcp_bucket) << '\n'; + } + replica->sync_to_gcp(gcp_bucket, gcp_credential_path, encryption_secret, avoid_snapshots); + } else if (server_url != "") { + if (client_id == "" || encryption_secret == "") { + throw std::string("sync.server.client_id and sync.encryption_secret are required"); + } + if (verbose) { + out << format("Syncing with sync server at {1}", server_url) << '\n'; + } + replica->sync_to_remote(server_url, tc::uuid_from_string(client_id), encryption_secret, + avoid_snapshots); + } else { + throw std::string("No sync.* settings are configured. See task-sync(5)."); } - Context& context = Context::getContext(); - context.tdb2.sync(std::move(server), false); - if (context.config.getBoolean("purge.on-sync")) { context.tdb2.expire_tasks(); } diff --git a/src/main.cpp b/src/main.cpp index f64daeee7..cbdeb0994 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -28,6 +28,7 @@ // cmake.h include header must come first #include +#include #include #include @@ -55,6 +56,11 @@ int main(int argc, const char** argv) { status = -1; } + catch (rust::Error& err) { + std::cerr << err.what() << "\n"; + status = -1; + } + catch (std::bad_alloc& error) { std::cerr << "Error: Memory allocation failed: " << error.what() << "\n"; status = -3; diff --git a/src/tc/.gitignore b/src/taskchampion-cpp/.gitignore similarity index 100% rename from src/tc/.gitignore rename to src/taskchampion-cpp/.gitignore diff --git a/src/taskchampion-cpp/CMakeLists.txt b/src/taskchampion-cpp/CMakeLists.txt new file mode 100644 index 000000000..241273d6c --- /dev/null +++ b/src/taskchampion-cpp/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required (VERSION 3.22) + +OPTION(SYSTEM_CORROSION "Use system provided corrosion instead of vendored version" OFF) +if(SYSTEM_CORROSION) + find_package(Corrosion REQUIRED) +else() + add_subdirectory(${CMAKE_SOURCE_DIR}/src/taskchampion-cpp/corrosion) +endif() + +# Import taskchampion-lib as a CMake library. This implements the Rust side of +# the cxxbridge, and depends on the `taskchampion` crate. +corrosion_import_crate( + MANIFEST_PATH "${CMAKE_SOURCE_DIR}/Cargo.toml" + LOCKED + CRATES "taskchampion-lib") + +# Set up `taskchampion-cpp`, the C++ side of the bridge. +corrosion_add_cxxbridge(taskchampion-cpp + CRATE taskchampion_lib + FILES lib.rs +) diff --git a/src/taskchampion-cpp/Cargo.toml b/src/taskchampion-cpp/Cargo.toml new file mode 100644 index 000000000..91e557587 --- /dev/null +++ b/src/taskchampion-cpp/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "taskchampion-lib" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["staticlib"] + +[dependencies] +taskchampion = "0.7.0" +cxx = "1.0.124" + +[build-dependencies] +cxx-build = "1.0" diff --git a/src/taskchampion-cpp/build.rs b/src/taskchampion-cpp/build.rs new file mode 100644 index 000000000..7ad128819 --- /dev/null +++ b/src/taskchampion-cpp/build.rs @@ -0,0 +1,6 @@ +#[allow(unused_must_use)] +fn main() { + cxx_build::bridge("src/lib.rs"); + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=src/lib.rs"); +} diff --git a/src/tc/corrosion b/src/taskchampion-cpp/corrosion similarity index 100% rename from src/tc/corrosion rename to src/taskchampion-cpp/corrosion diff --git a/src/taskchampion-cpp/src/lib.rs b/src/taskchampion-cpp/src/lib.rs new file mode 100644 index 000000000..d58af7a60 --- /dev/null +++ b/src/taskchampion-cpp/src/lib.rs @@ -0,0 +1,974 @@ +use cxx::CxxString; +use std::path::PathBuf; +use std::pin::Pin; +use taskchampion as tc; + +// All Taskchampion FFI is contained in this module, due to issues with cxx and multiple modules +// such as https://github.com/dtolnay/cxx/issues/1323. + +/// FFI interface for TaskChampion. +/// +/// This loosely follows the TaskChampion API defined at +/// https://docs.rs/taskchampion/latest/taskchampion/, with adjustments made as necessary to +/// accomodate cxx's limitations. Consult that documentation for full descriptions of the types and +/// methods. +/// +/// This interface is an internal implementation detail of Taskwarrior and may change at any time. +#[cxx::bridge(namespace = "tc")] +mod ffi { + // --- Uuid + + #[derive(Debug, Eq, PartialEq, Clone, Copy)] + struct Uuid { + v: [u8; 16], + } + + extern "Rust" { + /// Generate a new, random Uuid. + fn uuid_v4() -> Uuid; + + /// Parse the given string as a Uuid, panicking if it is not valid. + fn uuid_from_string(uuid: Pin<&CxxString>) -> Uuid; + + /// Convert the given Uuid to a string. + fn to_string(self: &Uuid) -> String; + + /// Check whether this is the "nil" Uuid, used as a sentinel value. + fn is_nil(self: &Uuid) -> bool; + } + + // --- Operation and Operations + + extern "Rust" { + type Operation; + + /// Check if this is a Create operation. + fn is_create(&self) -> bool; + + /// Check if this is a Update operation. + fn is_update(&self) -> bool; + + /// Check if this is a Delete operation. + fn is_delete(&self) -> bool; + + /// Check if this is an UndoPoint operation. + fn is_undo_point(&self) -> bool; + + /// Get the operation's uuid. + /// + /// Only valid for create, update, and delete operations. + fn get_uuid(&self) -> Uuid; + + /// Get the `old_task` for this update operation. + /// + /// Only valid for delete operations. + fn get_old_task(&self) -> Vec; + + /// Get the `property` for this update operation. + /// + /// Only valid for update operations. + fn get_property(&self, property_out: Pin<&mut CxxString>); + + /// Get the `value` for this update operation, returning false if the + /// `value` field is None. + /// + /// Only valid for update operations. + fn get_value(&self, value_out: Pin<&mut CxxString>) -> bool; + + /// Get the `old_value` for this update operation, returning false if the + /// `old_value` field is None. + /// + /// Only valid for update operations. + fn get_old_value(&self, old_value_out: Pin<&mut CxxString>) -> bool; + + /// Get the `timestamp` for this update operation. + /// + /// Only valid for update operations. + fn get_timestamp(&self) -> i64; + + /// Create a new vector of operations. It's also fine to construct a + /// `rust::Vec` directly. + fn new_operations() -> Vec; + + /// Add an UndoPoint operation to the vector of operations. All other + /// operation types should be added via `TaskData`. + fn add_undo_point(ops: &mut Vec); + } + + // --- Replica + + extern "Rust" { + type Replica; + + /// Create a new in-memory replica, such as for testing. + fn new_replica_in_memory() -> Result>; + + /// Create a new replica stored on-disk. + fn new_replica_on_disk(taskdb_dir: String, create_if_missing: bool) + -> Result>; + + /// Commit the given operations to the replica. + fn commit_operations(&mut self, ops: Vec) -> Result<()>; + + /// Commit the reverse of the given operations. + fn commit_reversed_operations(&mut self, ops: Vec) -> Result; + + /// Get `TaskData` values for all tasks in the replica. + + /// This contains `OptionTaskData` to allow C++ to `take` values out of the vector and use + /// them as `rust::Box`. Cxx does not support `Vec>`. Cxx also does not + /// handle `HashMap`, so the result is not a map from uuid to task. The returned Vec is + /// fully populated, so it is safe to call `take` on each value in the returned Vec once . + fn all_task_data(&mut self) -> Result>; + + /// Get the UUIDs of all tasks. + fn all_task_uuids(&mut self) -> Result>; + + /// Expire old, deleted tasks. + fn expire_tasks(&mut self) -> Result<()>; + + /// Get an existing task by its UUID. + fn get_task_data(&mut self, uuid: Uuid) -> Result; + + /// Return the operations back to and including the last undo point, or since the last sync if + /// no undo point is found. + fn get_undo_operations(&mut self) -> Result>; + + /// Get the number of local, un-sync'd operations, excluding undo operations. + fn num_local_operations(&mut self) -> Result; + + /// Get the number of (un-synchronized) undo points in storage. + fn num_undo_points(&mut self) -> Result; + + /// Rebuild the working set. + fn rebuild_working_set(&mut self, renumber: bool) -> Result<()>; + + /// Get the working set for this replica. + fn working_set(&mut self) -> Result>; + + /// Sync with a server crated from `ServerConfig::Local`. + fn sync_to_local(&mut self, server_dir: String, avoid_snapshots: bool) -> Result<()>; + + /// Sync with a server created from `ServerConfig::Remote`. + fn sync_to_remote( + &mut self, + url: String, + client_id: Uuid, + encryption_secret: &CxxString, + avoid_snapshots: bool, + ) -> Result<()>; + + /// Sync with a server created from `ServerConfig::Gcp`. + /// + /// An empty value for `credential_path` is converted to `Option::None`. + fn sync_to_gcp( + &mut self, + bucket: String, + credential_path: String, + encryption_secret: &CxxString, + avoid_snapshots: bool, + ) -> Result<()>; + } + + // --- OptionTaskData + + /// Wrapper around `Option>`, required since cxx does not support Option. + /// + /// Note that if an OptionTaskData containing a task is dropped without calling `take`, + /// it will leak the contained task. C++ code should be careful to always take. + struct OptionTaskData { + maybe_task: *mut TaskData, + } + + extern "Rust" { + /// Check if the value contains a task. + fn is_some(self: &OptionTaskData) -> bool; + /// Check if the value does not contain a task. + fn is_none(self: &OptionTaskData) -> bool; + /// Get the contained task, or panic if there is no task. The OptionTaskData + /// will be reset to contain None. + fn take(self: &mut OptionTaskData) -> Box; + } + + // --- TaskData + + extern "Rust" { + type TaskData; + + /// Create a new task with the given Uuid. + fn create_task(uuid: Uuid, ops: &mut Vec) -> Box; + + /// Get the task's Uuid. + fn get_uuid(&self) -> Uuid; + + /// Get a value on this task. If the property exists, returns true and updates + /// the output parameter. If not, returns false. + fn get(&self, property: &CxxString, value_out: Pin<&mut CxxString>) -> bool; + + /// Check if the given property is set. + fn has(&self, property: &CxxString) -> bool; + + /// Enumerate all properties on this task, in arbitrary order. + fn properties(&self) -> Vec; + + /// Enumerate all properties and their values on this task, in arbitrary order, as a + /// vector. + fn items(&self) -> Vec; + + /// Update the given property with the given value. + fn update(&mut self, property: &CxxString, value: &CxxString, ops: &mut Vec); + + /// Like `update`, but removing the property by passing None for the value. + fn update_remove(&mut self, property: &CxxString, ops: &mut Vec); + + /// Delete the task. The name is `delete_task` because `delete` is a C++ keyword. + fn delete_task(&mut self, ops: &mut Vec); + } + + // --- PropValuePair + + #[derive(Debug, Eq, PartialEq)] + struct PropValuePair { + prop: String, + value: String, + } + + // --- WorkingSet + + extern "Rust" { + type WorkingSet; + + /// Get the "length" of the working set: the total number of uuids in the set. + fn len(&self) -> usize; + + /// Get the largest index in the working set, or zero if the set is empty. + fn largest_index(&self) -> usize; + + /// True if the length is zero + fn is_empty(&self) -> bool; + + /// Get the uuid with the given index, if any exists. Returns the nil UUID if + /// there is no task at that index. + fn by_index(&self, index: usize) -> Uuid; + + /// Get the index for the given uuid, or zero if it is not in the working set. + fn by_uuid(&self, uuid: Uuid) -> usize; + + /// Get the entire working set, as a vector indexed by each task's id. For example, the + /// UUID for task 5 will be at `all_uuids()[5]`. All elements of the vector not corresponding + /// to a task contain the nil UUID. + fn all_uuids(&self) -> Vec; + } +} + +#[derive(Debug)] +struct CppError(tc::Error); + +impl From for CppError { + fn from(err: tc::Error) -> Self { + CppError(err) + } +} + +impl std::fmt::Display for CppError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let tc::Error::Other(err) = &self.0 { + // The default `to_string` representation of `anyhow::Error` only shows the "outermost" + // context, e.g., "Could not connect to server", and omits the juicy details about what + // actually went wrong. So, join all of those contexts with `: ` for presentation to the C++ + // layer. + let entire_msg = err + .chain() + .skip(1) + .fold(err.to_string(), |a, b| format!("{}: {}", a, b)); + write!(f, "{}", entire_msg) + } else { + self.0.fmt(f) + } + } +} + +// --- Uuid + +impl From for tc::Uuid { + fn from(value: ffi::Uuid) -> Self { + tc::Uuid::from_bytes(value.v) + } +} + +impl From<&ffi::Uuid> for tc::Uuid { + fn from(value: &ffi::Uuid) -> Self { + tc::Uuid::from_bytes(value.v) + } +} + +impl From for ffi::Uuid { + fn from(uuid: tc::Uuid) -> ffi::Uuid { + ffi::Uuid { + v: *uuid.as_bytes(), + } + } +} + +impl From<&tc::Uuid> for ffi::Uuid { + fn from(uuid: &tc::Uuid) -> ffi::Uuid { + ffi::Uuid { + v: *uuid.as_bytes(), + } + } +} + +fn uuid_v4() -> ffi::Uuid { + tc::Uuid::new_v4().into() +} + +fn uuid_from_string(uuid: Pin<&CxxString>) -> ffi::Uuid { + let Ok(uuid) = tc::Uuid::parse_str(uuid.to_str().expect("invalid utf-8")) else { + panic!("{} is not a valid UUID", uuid); + }; + uuid.into() +} + +impl ffi::Uuid { + #[allow(clippy::inherent_to_string, clippy::wrong_self_convention)] + fn to_string(&self) -> String { + tc::Uuid::from(self).as_hyphenated().to_string() + } + + fn is_nil(&self) -> bool { + tc::Uuid::from(self).is_nil() + } +} + +// --- Operation and Operations + +#[repr(transparent)] // required for safety +pub struct Operation(tc::Operation); + +impl Operation { + fn is_create(&self) -> bool { + matches!(&self.0, tc::Operation::Create { .. }) + } + + fn is_update(&self) -> bool { + matches!(&self.0, tc::Operation::Update { .. }) + } + + fn is_delete(&self) -> bool { + matches!(&self.0, tc::Operation::Delete { .. }) + } + + fn is_undo_point(&self) -> bool { + matches!(&self.0, tc::Operation::UndoPoint) + } + + fn get_uuid(&self) -> ffi::Uuid { + match self.0 { + tc::Operation::Create { uuid, .. } => uuid, + tc::Operation::Update { uuid, .. } => uuid, + tc::Operation::Delete { uuid, .. } => uuid, + _ => panic!("operation has no uuid"), + } + .into() + } + + fn get_property(&self, mut property_out: Pin<&mut CxxString>) { + match &self.0 { + tc::Operation::Update { property, .. } => { + property_out.as_mut().clear(); + property_out.as_mut().push_str(property); + } + _ => panic!("operation is not an update"), + } + } + + fn get_value(&self, mut value_out: Pin<&mut CxxString>) -> bool { + match &self.0 { + tc::Operation::Update { value, .. } => { + if let Some(value) = value { + value_out.as_mut().clear(); + value_out.as_mut().push_str(value); + true + } else { + false + } + } + _ => panic!("operation is not an update"), + } + } + + fn get_old_value(&self, mut old_value_out: Pin<&mut CxxString>) -> bool { + match &self.0 { + tc::Operation::Update { old_value, .. } => { + if let Some(old_value) = old_value { + old_value_out.as_mut().clear(); + old_value_out.as_mut().push_str(old_value); + true + } else { + false + } + } + _ => panic!("operation is not an update"), + } + } + + fn get_timestamp(&self) -> i64 { + match &self.0 { + tc::Operation::Update { timestamp, .. } => timestamp.timestamp(), + _ => panic!("operation is not an update"), + } + } + + fn get_old_task(&self) -> Vec { + match &self.0 { + tc::Operation::Delete { old_task, .. } => old_task + .iter() + .map(|(p, v)| ffi::PropValuePair { + prop: p.into(), + value: v.into(), + }) + .collect(), + _ => panic!("operation is not a delete"), + } + } +} + +fn new_operations() -> Vec { + Vec::new() +} + +fn add_undo_point(ops: &mut Vec) { + ops.push(Operation(tc::Operation::UndoPoint)); +} + +// --- Replica + +struct Replica(tc::Replica); + +impl From for Replica { + fn from(inner: tc::Replica) -> Self { + Replica(inner) + } +} + +fn new_replica_on_disk( + taskdb_dir: String, + create_if_missing: bool, +) -> Result, CppError> { + let storage = tc::StorageConfig::OnDisk { + taskdb_dir: PathBuf::from(taskdb_dir), + create_if_missing, + } + .into_storage()?; + Ok(Box::new(tc::Replica::new(storage).into())) +} + +fn new_replica_in_memory() -> Result, CppError> { + let storage = tc::StorageConfig::InMemory.into_storage()?; + Ok(Box::new(tc::Replica::new(storage).into())) +} + +/// Utility function for Replica methods using Operations. +fn to_tc_operations(ops: Vec) -> Vec { + // SAFETY: Operation is a transparent newtype for tc::Operation, so a Vec of one is + // a Vec of the other. + unsafe { std::mem::transmute::, Vec>(ops) } +} + +/// Utility function for Replica methods using Operations. +fn from_tc_operations(ops: Vec) -> Vec { + // SAFETY: Operation is a transparent newtype for tc::Operation, so a Vec of one is + // a Vec of the other. + unsafe { std::mem::transmute::, Vec>(ops) } +} + +impl Replica { + fn commit_operations(&mut self, ops: Vec) -> Result<(), CppError> { + Ok(self.0.commit_operations(to_tc_operations(ops))?) + } + + fn commit_reversed_operations(&mut self, ops: Vec) -> Result { + Ok(self.0.commit_reversed_operations(to_tc_operations(ops))?) + } + + fn all_task_data(&mut self) -> Result, CppError> { + Ok(self + .0 + .all_task_data()? + .drain() + .map(|(_, t)| Some(t).into()) + .collect()) + } + + fn all_task_uuids(&mut self) -> Result, CppError> { + Ok(self + .0 + .all_task_uuids()? + .into_iter() + .map(ffi::Uuid::from) + .collect()) + } + + fn expire_tasks(&mut self) -> Result<(), CppError> { + Ok(self.0.expire_tasks()?) + } + + fn get_task_data(&mut self, uuid: ffi::Uuid) -> Result { + Ok(self.0.get_task_data(uuid.into())?.into()) + } + + fn get_undo_operations(&mut self) -> Result, CppError> { + Ok(from_tc_operations(self.0.get_undo_operations()?)) + } + + fn num_local_operations(&mut self) -> Result { + Ok(self.0.num_local_operations()?) + } + + fn num_undo_points(&mut self) -> Result { + Ok(self.0.num_undo_points()?) + } + + fn rebuild_working_set(&mut self, renumber: bool) -> Result<(), CppError> { + Ok(self.0.rebuild_working_set(renumber)?) + } + + fn working_set(&mut self) -> Result, CppError> { + Ok(Box::new(self.0.working_set()?.into())) + } + + fn sync_to_local(&mut self, server_dir: String, avoid_snapshots: bool) -> Result<(), CppError> { + let mut server = tc::server::ServerConfig::Local { + server_dir: server_dir.into(), + } + .into_server()?; + Ok(self.0.sync(&mut server, avoid_snapshots)?) + } + + fn sync_to_remote( + &mut self, + url: String, + client_id: ffi::Uuid, + encryption_secret: &CxxString, + avoid_snapshots: bool, + ) -> Result<(), CppError> { + let mut server = tc::server::ServerConfig::Remote { + url, + client_id: client_id.into(), + encryption_secret: encryption_secret.as_bytes().to_vec(), + } + .into_server()?; + Ok(self.0.sync(&mut server, avoid_snapshots)?) + } + + fn sync_to_gcp( + &mut self, + bucket: String, + credential_path: String, + encryption_secret: &CxxString, + avoid_snapshots: bool, + ) -> Result<(), CppError> { + let mut server = tc::server::ServerConfig::Gcp { + bucket, + credential_path: if credential_path.is_empty() { + None + } else { + Some(credential_path) + }, + encryption_secret: encryption_secret.as_bytes().to_vec(), + } + .into_server()?; + Ok(self.0.sync(&mut server, avoid_snapshots)?) + } +} + +// --- OptionTaskData + +impl From> for ffi::OptionTaskData { + fn from(value: Option) -> Self { + let Some(task) = value else { + return ffi::OptionTaskData { + maybe_task: std::ptr::null_mut(), + }; + }; + let boxed = Box::new(task.into()); + ffi::OptionTaskData { + maybe_task: Box::into_raw(boxed), + } + } +} + +impl ffi::OptionTaskData { + fn is_some(&self) -> bool { + !self.maybe_task.is_null() + } + + fn is_none(&self) -> bool { + self.maybe_task.is_null() + } + + fn take(&mut self) -> Box { + let mut ptr = std::ptr::null_mut(); + std::mem::swap(&mut ptr, &mut self.maybe_task); + if ptr.is_null() { + panic!("Cannot take an empty OptionTaskdata"); + } + // SAFETY: this value is not NULL and was created from `Box::into_raw` in the + // `From>` implementation above. + unsafe { Box::from_raw(ptr) } + } +} + +// --- TaskData + +pub struct TaskData(tc::TaskData); + +impl From for TaskData { + fn from(task: tc::TaskData) -> Self { + TaskData(task) + } +} + +/// Utility function for TaskData methods. +fn operations_ref(ops: &mut Vec) -> &mut Vec { + // SAFETY: Operation is a transparent newtype for tc::Operation, so a Vec of one is a + // Vec of the other. + unsafe { std::mem::transmute::<&mut Vec, &mut Vec>(ops) } +} + +fn create_task(uuid: ffi::Uuid, ops: &mut Vec) -> Box { + let t = tc::TaskData::create(uuid.into(), operations_ref(ops)); + Box::new(TaskData(t)) +} + +impl TaskData { + fn get_uuid(&self) -> ffi::Uuid { + self.0.get_uuid().into() + } + + fn get(&self, property: &CxxString, mut value_out: Pin<&mut CxxString>) -> bool { + let Some(value) = self.0.get(property.to_string_lossy()) else { + return false; + }; + value_out.as_mut().clear(); + value_out.as_mut().push_str(value); + true + } + + fn has(&self, property: &CxxString) -> bool { + self.0.has(property.to_string_lossy()) + } + + fn properties(&self) -> Vec { + self.0.properties().map(|s| s.to_owned()).collect() + } + + fn items(&self) -> Vec { + self.0 + .iter() + .map(|(p, v)| ffi::PropValuePair { + prop: p.into(), + value: v.into(), + }) + .collect() + } + + fn update(&mut self, property: &CxxString, value: &CxxString, ops: &mut Vec) { + self.0.update( + property.to_string_lossy(), + Some(value.to_string_lossy().into()), + operations_ref(ops), + ) + } + + fn update_remove(&mut self, property: &CxxString, ops: &mut Vec) { + self.0 + .update(property.to_string_lossy(), None, operations_ref(ops)) + } + + fn delete_task(&mut self, ops: &mut Vec) { + self.0.delete(operations_ref(ops)) + } +} + +// --- WorkingSet + +struct WorkingSet(tc::WorkingSet); + +impl From for WorkingSet { + fn from(task: tc::WorkingSet) -> Self { + WorkingSet(task) + } +} + +impl WorkingSet { + fn len(&self) -> usize { + self.0.len() + } + + fn largest_index(&self) -> usize { + self.0.largest_index() + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn by_index(&self, index: usize) -> ffi::Uuid { + self.0.by_index(index).unwrap_or_else(tc::Uuid::nil).into() + } + + fn by_uuid(&self, uuid: ffi::Uuid) -> usize { + self.0.by_uuid(uuid.into()).unwrap_or(0) + } + + fn all_uuids(&self) -> Vec { + let mut res = vec![tc::Uuid::nil().into(); self.0.largest_index() + 1]; + for (i, uuid) in self.0.iter() { + res[i] = uuid.into(); + } + res + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn uuids() { + let uuid = uuid_v4(); + assert_eq!(uuid.to_string().len(), 36); + } + + #[test] + fn operations() { + cxx::let_cxx_string!(prop = "prop"); + cxx::let_cxx_string!(prop2 = "prop2"); + cxx::let_cxx_string!(value = "value"); + cxx::let_cxx_string!(value2 = "value2"); + + let mut operations = new_operations(); + add_undo_point(&mut operations); + let mut i = 0; + assert_eq!(operations.len(), i + 1); + assert!(!operations[i].is_create()); + assert!(!operations[i].is_update()); + assert!(!operations[i].is_delete()); + assert!(operations[i].is_undo_point()); + + let uuid = uuid_v4(); + let mut t = create_task(uuid, &mut operations); + i += 1; + assert_eq!(operations.len(), i + 1); + assert!(operations[i].is_create()); + assert!(!operations[i].is_update()); + assert!(!operations[i].is_delete()); + assert!(!operations[i].is_undo_point()); + assert_eq!(operations[i].get_uuid(), uuid); + + t.update(&prop, &value, &mut operations); + i += 1; + assert_eq!(operations.len(), i + 1); + assert!(!operations[i].is_create()); + assert!(operations[i].is_update()); + assert!(!operations[i].is_delete()); + assert!(!operations[i].is_undo_point()); + assert_eq!(operations[i].get_uuid(), uuid); + // Note that `get_value` and `get_old_value` cannot be tested from Rust, as it is not + // possible to pass a reference to a CxxString and retain ownership of it. + assert!(operations[i].get_timestamp() > 0); + + t.update(&prop2, &value, &mut operations); + i += 1; + assert_eq!(operations.len(), i + 1); + assert!(!operations[i].is_create()); + assert!(operations[i].is_update()); + assert!(!operations[i].is_delete()); + assert!(!operations[i].is_undo_point()); + assert_eq!(operations[i].get_uuid(), uuid); + assert!(operations[i].get_timestamp() > 0); + + t.update(&prop2, &value2, &mut operations); + i += 1; + assert_eq!(operations.len(), i + 1); + assert!(!operations[i].is_create()); + assert!(operations[i].is_update()); + assert!(!operations[i].is_delete()); + assert!(!operations[i].is_undo_point()); + assert_eq!(operations[i].get_uuid(), uuid); + assert!(operations[i].get_timestamp() > 0); + + t.update_remove(&prop, &mut operations); + i += 1; + assert_eq!(operations.len(), i + 1); + assert!(!operations[i].is_create()); + assert!(operations[i].is_update()); + assert!(!operations[i].is_delete()); + assert!(!operations[i].is_undo_point()); + assert_eq!(operations[i].get_uuid(), uuid); + assert!(operations[i].get_timestamp() > 0); + + t.delete_task(&mut operations); + i += 1; + assert_eq!(operations.len(), i + 1); + assert!(!operations[i].is_create()); + assert!(!operations[i].is_update()); + assert!(operations[i].is_delete()); + assert!(!operations[i].is_undo_point()); + assert_eq!(operations[i].get_uuid(), uuid); + assert_eq!( + operations[i].get_old_task(), + vec![ffi::PropValuePair { + prop: "prop2".into(), + value: "value2".into(), + },] + ); + } + + #[test] + fn operation_counts() { + let mut rep = new_replica_in_memory().unwrap(); + let mut operations = new_operations(); + add_undo_point(&mut operations); + create_task(uuid_v4(), &mut operations); + create_task(uuid_v4(), &mut operations); + create_task(uuid_v4(), &mut operations); + add_undo_point(&mut operations); + rep.commit_operations(operations).unwrap(); + // Three non-undo-point operations. + assert_eq!(rep.num_local_operations().unwrap(), 3); + // Two undo points + assert_eq!(rep.num_undo_points().unwrap(), 2); + } + + #[test] + fn undo_operations() { + let mut rep = new_replica_in_memory().unwrap(); + let mut operations = new_operations(); + let (uuid1, uuid2, uuid3) = (uuid_v4(), uuid_v4(), uuid_v4()); + add_undo_point(&mut operations); + create_task(uuid1, &mut operations); + add_undo_point(&mut operations); + create_task(uuid2, &mut operations); + create_task(uuid3, &mut operations); + rep.commit_operations(operations).unwrap(); + + let undo_ops = rep.get_undo_operations().unwrap(); + assert_eq!(undo_ops.len(), 3); + assert!(undo_ops[0].is_undo_point()); + assert!(undo_ops[1].is_create()); + assert_eq!(undo_ops[1].get_uuid(), uuid2); + assert!(undo_ops[2].is_create()); + assert_eq!(undo_ops[2].get_uuid(), uuid3); + } + + #[test] + fn task_lists() { + let mut rep = new_replica_in_memory().unwrap(); + let mut operations = new_operations(); + add_undo_point(&mut operations); + create_task(uuid_v4(), &mut operations); + create_task(uuid_v4(), &mut operations); + create_task(uuid_v4(), &mut operations); + rep.commit_operations(operations).unwrap(); + + assert_eq!(rep.all_task_data().unwrap().len(), 3); + assert_eq!(rep.all_task_uuids().unwrap().len(), 3); + } + + #[test] + fn expire_tasks() { + let mut rep = new_replica_in_memory().unwrap(); + let mut operations = new_operations(); + add_undo_point(&mut operations); + create_task(uuid_v4(), &mut operations); + create_task(uuid_v4(), &mut operations); + create_task(uuid_v4(), &mut operations); + rep.commit_operations(operations).unwrap(); + rep.expire_tasks().unwrap(); + } + + #[test] + fn get_task_data() { + let mut rep = new_replica_in_memory().unwrap(); + + let uuid = uuid_v4(); + assert!(rep.get_task_data(uuid).unwrap().is_none()); + + let mut operations = new_operations(); + create_task(uuid, &mut operations); + rep.commit_operations(operations).unwrap(); + + let mut t = rep.get_task_data(uuid).unwrap(); + assert!(t.is_some()); + assert_eq!(t.take().get_uuid(), uuid); + } + + #[test] + fn task_properties() { + cxx::let_cxx_string!(prop = "prop"); + cxx::let_cxx_string!(prop2 = "prop2"); + cxx::let_cxx_string!(value = "value"); + + let mut rep = new_replica_in_memory().unwrap(); + + let uuid = uuid_v4(); + let mut operations = new_operations(); + let mut t = create_task(uuid, &mut operations); + t.update(&prop, &value, &mut operations); + rep.commit_operations(operations).unwrap(); + + let t = rep.get_task_data(uuid).unwrap().take(); + assert!(t.has(&prop)); + assert!(!t.has(&prop2)); + // Note that `get` cannot be tested from Rust, as it is not possible to pass a reference to + // a CxxString and retain ownership of it. + + assert_eq!(t.properties(), vec!["prop".to_string()]); + assert_eq!( + t.iter(), + vec![ffi::PropValuePair { + prop: "prop".into(), + value: "value".into(), + }] + ); + } + + #[test] + fn working_set() { + cxx::let_cxx_string!(status = "status"); + cxx::let_cxx_string!(pending = "pending"); + cxx::let_cxx_string!(completed = "completed"); + let (uuid1, uuid2, uuid3) = (uuid_v4(), uuid_v4(), uuid_v4()); + + let mut rep = new_replica_in_memory().unwrap(); + + let mut operations = new_operations(); + let mut t = create_task(uuid1, &mut operations); + t.update(&status, &pending, &mut operations); + rep.commit_operations(operations).unwrap(); + + let mut operations = new_operations(); + let mut t = create_task(uuid2, &mut operations); + t.update(&status, &pending, &mut operations); + rep.commit_operations(operations).unwrap(); + + let mut operations = new_operations(); + let mut t = create_task(uuid3, &mut operations); + t.update(&status, &completed, &mut operations); + rep.commit_operations(operations).unwrap(); + + rep.rebuild_working_set(false).unwrap(); + + let ws = rep.working_set().unwrap(); + assert!(!ws.is_empty()); + assert_eq!(ws.len(), 2); + assert_eq!(ws.largest_index(), 2); + assert_eq!(ws.by_index(1), uuid1); + assert_eq!(ws.by_uuid(uuid2), 2); + assert_eq!(ws.by_index(100), tc::Uuid::nil().into()); + assert_eq!(ws.by_uuid(uuid3), 0); + assert_eq!(ws.all_uuids(), vec![tc::Uuid::nil().into(), uuid1, uuid2]); + } +} diff --git a/src/tc/CMakeLists.txt b/src/tc/CMakeLists.txt deleted file mode 100644 index 907ecbad6..000000000 --- a/src/tc/CMakeLists.txt +++ /dev/null @@ -1,35 +0,0 @@ -cmake_minimum_required (VERSION 3.22) - -OPTION(SYSTEM_CORROSION "Use system provided corrosion instead of vendored version" OFF) -if(SYSTEM_CORROSION) - find_package(Corrosion REQUIRED) -else() - add_subdirectory(${CMAKE_SOURCE_DIR}/src/tc/corrosion) -endif() - -# Import taskchampion-lib as a CMake library. -corrosion_import_crate( - MANIFEST_PATH "${CMAKE_SOURCE_DIR}/Cargo.toml" - LOCKED - CRATES "taskchampion-lib") - -# TODO(#3425): figure out how to create taskchampion.h - -include_directories (${CMAKE_SOURCE_DIR} - ${CMAKE_SOURCE_DIR}/src - ${CMAKE_SOURCE_DIR}/src/tc - ${CMAKE_SOURCE_DIR}/src/tc/lib - ${CMAKE_SOURCE_DIR}/src/libshared/src - ${TASK_INCLUDE_DIRS}) - -set (tc_SRCS - ffi.h - lib/taskchampion.h - util.cpp util.h - Replica.cpp Replica.h - Server.cpp Server.h - WorkingSet.cpp WorkingSet.h - Task.cpp Task.h) - -add_library (tc STATIC ${tc_SRCS}) -target_link_libraries(tc taskchampion_lib) diff --git a/src/tc/Replica.cpp b/src/tc/Replica.cpp deleted file mode 100644 index a90ac2e0b..000000000 --- a/src/tc/Replica.cpp +++ /dev/null @@ -1,273 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// -// Copyright 2022, Dustin J. Mitchell -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// https://www.opensource.org/licenses/mit-license.php -// -//////////////////////////////////////////////////////////////////////////////// - -#include -// cmake.h include header must come first - -#include - -#include - -#include "tc/Replica.h" -#include "tc/Server.h" -#include "tc/Task.h" -#include "tc/WorkingSet.h" -#include "tc/util.h" - -using namespace tc::ffi; - -//////////////////////////////////////////////////////////////////////////////// -tc::ReplicaGuard::ReplicaGuard(Replica &replica, Task &task) : replica(replica), task(task) { - // "steal" the reference from the Replica and store it locally, so that any - // attempt to use the Replica will fail - tcreplica = replica.inner.release(); - task.to_mut(tcreplica); -} - -//////////////////////////////////////////////////////////////////////////////// -tc::ReplicaGuard::~ReplicaGuard() { - task.to_immut(); - // return the reference to the Replica. - replica.inner.reset(tcreplica); -} - -//////////////////////////////////////////////////////////////////////////////// -tc::Replica::Replica() { - inner = unique_tcreplica_ptr(tc_replica_new_in_memory(), - [](TCReplica *rep) { tc_replica_free(rep); }); -} - -//////////////////////////////////////////////////////////////////////////////// -tc::Replica::Replica(Replica &&other) noexcept { - // move inner from other - inner = unique_tcreplica_ptr(other.inner.release(), [](TCReplica *rep) { tc_replica_free(rep); }); -} - -//////////////////////////////////////////////////////////////////////////////// -tc::Replica &tc::Replica::operator=(Replica &&other) noexcept { - if (this != &other) { - // move inner from other - inner = - unique_tcreplica_ptr(other.inner.release(), [](TCReplica *rep) { tc_replica_free(rep); }); - } - return *this; -} - -//////////////////////////////////////////////////////////////////////////////// -tc::Replica::Replica(const std::string &dir, bool create_if_missing) { - TCString path = tc_string_borrow(dir.c_str()); - TCString error; - auto tcreplica = tc_replica_new_on_disk(path, create_if_missing, &error); - if (!tcreplica) { - auto errmsg = format("Could not create replica at {1}: {2}", dir, tc_string_content(&error)); - tc_string_free(&error); - throw errmsg; - } - inner = unique_tcreplica_ptr(tcreplica, [](TCReplica *rep) { tc_replica_free(rep); }); -} - -//////////////////////////////////////////////////////////////////////////////// -tc::WorkingSet tc::Replica::working_set() { - TCWorkingSet *tcws = tc_replica_working_set(&*inner); - if (!tcws) { - throw replica_error(); - } - return WorkingSet{tcws}; -} - -//////////////////////////////////////////////////////////////////////////////// -std::optional tc::Replica::get_task(const std::string &uuid) { - TCTask *tctask = tc_replica_get_task(&*inner, uuid2tc(uuid)); - if (!tctask) { - auto error = tc_replica_error(&*inner); - if (error.ptr) { - throw replica_error(error); - } else { - return std::nullopt; - } - } - return std::make_optional(Task(tctask)); -} - -//////////////////////////////////////////////////////////////////////////////// -tc::Task tc::Replica::new_task(tc::Status status, const std::string &description) { - TCTask *tctask = tc_replica_new_task(&*inner, (tc::ffi::TCStatus)status, string2tc(description)); - if (!tctask) { - throw replica_error(); - } - return Task(tctask); -} - -//////////////////////////////////////////////////////////////////////////////// -tc::Task tc::Replica::import_task_with_uuid(const std::string &uuid) { - TCTask *tctask = tc_replica_import_task_with_uuid(&*inner, uuid2tc(uuid)); - if (!tctask) { - throw replica_error(); - } - return Task(tctask); -} - -//////////////////////////////////////////////////////////////////////////////// -void tc::Replica::delete_task(const std::string &uuid) { - auto res = tc_replica_delete_task(&*inner, uuid2tc(uuid)); - if (res != TC_RESULT_OK) { - throw replica_error(); - } -} - -//////////////////////////////////////////////////////////////////////////////// -void tc::Replica::expire_tasks() { - auto res = tc_replica_expire_tasks(&*inner); - if (res != TC_RESULT_OK) { - throw replica_error(); - } -} - -//////////////////////////////////////////////////////////////////////////////// -void tc::Replica::sync(Server server, bool avoid_snapshots) { - // The server remains owned by this function, per tc_replica_sync docs. - auto res = tc_replica_sync(&*inner, server.inner.get(), avoid_snapshots); - if (res != TC_RESULT_OK) { - throw replica_error(); - } -} - -//////////////////////////////////////////////////////////////////////////////// -TCReplicaOpList tc::Replica::get_undo_ops() { return tc_replica_get_undo_ops(&*inner); } - -//////////////////////////////////////////////////////////////////////////////// -void tc::Replica::commit_undo_ops(TCReplicaOpList tc_undo_ops, int32_t *undone_out) { - auto res = tc_replica_commit_undo_ops(&*inner, tc_undo_ops, undone_out); - if (res != TC_RESULT_OK) { - throw replica_error(); - } -} - -//////////////////////////////////////////////////////////////////////////////// -void tc::Replica::free_replica_ops(TCReplicaOpList tc_undo_ops) { - tc_replica_op_list_free(&tc_undo_ops); -} - -//////////////////////////////////////////////////////////////////////////////// -std::string tc::Replica::get_op_uuid(TCReplicaOp &tc_replica_op) const { - TCString uuid = tc_replica_op_get_uuid(&tc_replica_op); - return tc2string(uuid); -} - -//////////////////////////////////////////////////////////////////////////////// -std::string tc::Replica::get_op_property(TCReplicaOp &tc_replica_op) const { - TCString property = tc_replica_op_get_property(&tc_replica_op); - return tc2string(property); -} - -//////////////////////////////////////////////////////////////////////////////// -std::string tc::Replica::get_op_value(TCReplicaOp &tc_replica_op) const { - TCString value = tc_replica_op_get_value(&tc_replica_op); - return tc2string(value); -} - -//////////////////////////////////////////////////////////////////////////////// -std::string tc::Replica::get_op_old_value(TCReplicaOp &tc_replica_op) const { - TCString old_value = tc_replica_op_get_old_value(&tc_replica_op); - return tc2string(old_value); -} - -//////////////////////////////////////////////////////////////////////////////// -std::string tc::Replica::get_op_timestamp(TCReplicaOp &tc_replica_op) const { - TCString timestamp = tc_replica_op_get_timestamp(&tc_replica_op); - return tc2string(timestamp); -} - -//////////////////////////////////////////////////////////////////////////////// -std::string tc::Replica::get_op_old_task_description(TCReplicaOp &tc_replica_op) const { - TCString description = tc_replica_op_get_old_task_description(&tc_replica_op); - return tc2string(description); -} - -//////////////////////////////////////////////////////////////////////////////// -int64_t tc::Replica::num_local_operations() { - auto num = tc_replica_num_local_operations(&*inner); - if (num < 0) { - throw replica_error(); - } - return num; -} - -//////////////////////////////////////////////////////////////////////////////// -int64_t tc::Replica::num_undo_points() { - auto num = tc_replica_num_undo_points(&*inner); - if (num < 0) { - throw replica_error(); - } - return num; -} - -//////////////////////////////////////////////////////////////////////////////// -std::vector tc::Replica::all_tasks() { - TCTaskList tasks = tc_replica_all_tasks(&*inner); - if (!tasks.items) { - throw replica_error(); - } - - std::vector all; - all.reserve(tasks.len); - for (size_t i = 0; i < tasks.len; i++) { - auto tctask = tc_task_list_take(&tasks, i); - if (tctask) { - all.push_back(Task(tctask)); - } - } - - return all; -} - -//////////////////////////////////////////////////////////////////////////////// -void tc::Replica::rebuild_working_set(bool force) { - auto res = tc_replica_rebuild_working_set(&*inner, force); - if (res != TC_RESULT_OK) { - throw replica_error(); - } -} - -//////////////////////////////////////////////////////////////////////////////// -tc::ReplicaGuard tc::Replica::mutate_task(tc::Task &task) { return ReplicaGuard(*this, task); } - -//////////////////////////////////////////////////////////////////////////////// -std::string tc::Replica::replica_error() { return replica_error(tc_replica_error(&*inner)); } - -//////////////////////////////////////////////////////////////////////////////// -std::string tc::Replica::replica_error(TCString error) { - std::string errmsg; - if (!error.ptr) { - errmsg = std::string("Unknown TaskChampion error"); - } else { - errmsg = std::string(tc_string_content(&error)); - } - tc_string_free(&error); - return errmsg; -} - -//////////////////////////////////////////////////////////////////////////////// diff --git a/src/tc/Replica.h b/src/tc/Replica.h deleted file mode 100644 index 8dd3ef188..000000000 --- a/src/tc/Replica.h +++ /dev/null @@ -1,127 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// -// Copyright 2022, Dustin J. Mitchell -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// https://www.opensource.org/licenses/mit-license.php -// -//////////////////////////////////////////////////////////////////////////////// - -#ifndef INCLUDED_TC_REPLICA -#define INCLUDED_TC_REPLICA - -#include -#include -#include -#include -#include - -#include "tc/Task.h" -#include "tc/ffi.h" - -namespace tc { -class Task; -class WorkingSet; -class Server; - -// a unique_ptr to a TCReplica which will automatically free the value when -// it goes out of scope. -using unique_tcreplica_ptr = - std::unique_ptr>; - -// ReplicaGuard uses RAII to ensure that a Replica is not accessed while it -// is mutably borrowed (specifically, to make a task mutable). -class ReplicaGuard { - protected: - friend class Replica; - explicit ReplicaGuard(Replica &, Task &); - - public: - ~ReplicaGuard(); - - // No moving or copying allowed - ReplicaGuard(const ReplicaGuard &) = delete; - ReplicaGuard &operator=(const ReplicaGuard &) = delete; - ReplicaGuard(ReplicaGuard &&) = delete; - ReplicaGuard &operator=(Replica &&) = delete; - - private: - Replica &replica; - tc::ffi::TCReplica *tcreplica; - Task &task; -}; - -// Replica wraps the TCReplica type, managing its memory, errors, and so on. -// -// Except as noted, method names match the suffix to `tc_replica_..`. -class Replica { - public: - Replica(); // tc_replica_new_in_memory - Replica(const std::string &dir, bool create_if_missing); // tc_replica_new_on_disk - - // This object "owns" inner, so copy is not allowed. - Replica(const Replica &) = delete; - Replica &operator=(const Replica &) = delete; - - // Explicit move constructor and assignment - Replica(Replica &&) noexcept; - Replica &operator=(Replica &&) noexcept; - - std::vector all_tasks(); - // TODO: struct TCUuidList tc_replica_all_task_uuids(struct TCReplica *rep); - tc::WorkingSet working_set(); - std::optional get_task(const std::string &uuid); - tc::Task new_task(Status status, const std::string &description); - tc::Task import_task_with_uuid(const std::string &uuid); - void delete_task(const std::string &uuid); - // TODO: struct TCTask *tc_replica_import_task_with_uuid(struct TCReplica *rep, struct TCUuid - // tcuuid); - void expire_tasks(); - void sync(Server server, bool avoid_snapshots); - tc::ffi::TCReplicaOpList get_undo_ops(); - void commit_undo_ops(tc::ffi::TCReplicaOpList tc_undo_ops, int32_t *undone_out); - void free_replica_ops(tc::ffi::TCReplicaOpList tc_undo_ops); - std::string get_op_uuid(tc::ffi::TCReplicaOp &tc_replica_op) const; - std::string get_op_property(tc::ffi::TCReplicaOp &tc_replica_op) const; - std::string get_op_value(tc::ffi::TCReplicaOp &tc_replica_op) const; - std::string get_op_old_value(tc::ffi::TCReplicaOp &tc_replica_op) const; - std::string get_op_timestamp(tc::ffi::TCReplicaOp &tc_replica_op) const; - std::string get_op_old_task_description(tc::ffi::TCReplicaOp &tc_replica_op) const; - int64_t num_local_operations(); - int64_t num_undo_points(); - // TODO: TCResult tc_replica_add_undo_point(struct TCReplica *rep, bool force); - void rebuild_working_set(bool force); - - ReplicaGuard mutate_task(tc::Task &); - void immut_task(tc::Task &); - - protected: - friend class ReplicaGuard; - unique_tcreplica_ptr inner; - - // construct an error message from tc_replica_error, or from the given - // string retrieved from tc_replica_error. - std::string replica_error(); - std::string replica_error(tc::ffi::TCString string); -}; -} // namespace tc - -#endif -//////////////////////////////////////////////////////////////////////////////// diff --git a/src/tc/Server.cpp b/src/tc/Server.cpp deleted file mode 100644 index e5adef9dd..000000000 --- a/src/tc/Server.cpp +++ /dev/null @@ -1,109 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// -// Copyright 2022, Dustin J. Mitchell -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// https://www.opensource.org/licenses/mit-license.php -// -//////////////////////////////////////////////////////////////////////////////// - -#include -// cmake.h include header must come first - -#include - -#include "tc/Server.h" -#include "tc/util.h" - -using namespace tc::ffi; - -//////////////////////////////////////////////////////////////////////////////// -tc::Server tc::Server::new_local(const std::string &server_dir) { - TCString tc_server_dir = tc_string_borrow(server_dir.c_str()); - TCString error; - auto tcserver = tc_server_new_local(tc_server_dir, &error); - if (!tcserver) { - std::string errmsg = format("Could not configure local server at {1}: {2}", server_dir, - tc_string_content(&error)); - tc_string_free(&error); - throw errmsg; - } - return Server(unique_tcserver_ptr(tcserver, [](TCServer *rep) { tc_server_free(rep); })); -} - -//////////////////////////////////////////////////////////////////////////////// -tc::Server tc::Server::new_sync(const std::string &url, const std::string &client_id, - const std::string &encryption_secret) { - TCString tc_url = tc_string_borrow(url.c_str()); - TCString tc_client_id = tc_string_borrow(client_id.c_str()); - TCString tc_encryption_secret = tc_string_borrow(encryption_secret.c_str()); - - TCUuid tc_client_uuid; - if (tc_uuid_from_str(tc_client_id, &tc_client_uuid) != TC_RESULT_OK) { - tc_string_free(&tc_url); - tc_string_free(&tc_encryption_secret); - throw format("client_id '{1}' is not a valid UUID", client_id); - } - - TCString error; - auto tcserver = tc_server_new_sync(tc_url, tc_client_uuid, tc_encryption_secret, &error); - if (!tcserver) { - std::string errmsg = format("Could not configure connection to server at {1}: {2}", url, - tc_string_content(&error)); - tc_string_free(&error); - throw errmsg; - } - return Server(unique_tcserver_ptr(tcserver, [](TCServer *rep) { tc_server_free(rep); })); -} - -//////////////////////////////////////////////////////////////////////////////// -tc::Server tc::Server::new_gcp(const std::string &bucket, const std::string &credential_path, - const std::string &encryption_secret) { - TCString tc_bucket = tc_string_borrow(bucket.c_str()); - TCString tc_encryption_secret = tc_string_borrow(encryption_secret.c_str()); - TCString tc_credential_path = tc_string_borrow(credential_path.c_str()); - - TCString error; - auto tcserver = tc_server_new_gcp(tc_bucket, tc_credential_path, tc_encryption_secret, &error); - if (!tcserver) { - std::string errmsg = format("Could not configure connection to GCP bucket {1}: {2}", bucket, - tc_string_content(&error)); - tc_string_free(&error); - throw errmsg; - } - return Server(unique_tcserver_ptr(tcserver, [](TCServer *rep) { tc_server_free(rep); })); -} - -//////////////////////////////////////////////////////////////////////////////// -tc::Server::Server(tc::Server &&other) noexcept { - // move inner from other - inner = unique_tcserver_ptr(other.inner.release(), [](TCServer *rep) { tc_server_free(rep); }); -} - -//////////////////////////////////////////////////////////////////////////////// -tc::Server &tc::Server::operator=(tc::Server &&other) noexcept { - if (this != &other) { - // move inner from other - inner = unique_tcserver_ptr(other.inner.release(), [](TCServer *rep) { tc_server_free(rep); }); - } - return *this; -} - -//////////////////////////////////////////////////////////////////////////////// diff --git a/src/tc/Server.h b/src/tc/Server.h deleted file mode 100644 index 489d73425..000000000 --- a/src/tc/Server.h +++ /dev/null @@ -1,85 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// -// Copyright 2022, Dustin J. Mitchell -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// https://www.opensource.org/licenses/mit-license.php -// -//////////////////////////////////////////////////////////////////////////////// - -#ifndef INCLUDED_TC_SERVER -#define INCLUDED_TC_SERVER - -#include -#include -#include -#include -#include - -#include "tc/ffi.h" - -namespace tc { -// a unique_ptr to a TCServer which will automatically free the value when -// it goes out of scope. -using unique_tcserver_ptr = - std::unique_ptr>; - -// Server wraps the TCServer type, managing its memory, errors, and so on. -// -// Except as noted, method names match the suffix to `tc_server_..`. -class Server { - public: - // Construct a null server - Server() = default; - - // Construct a local server (tc_server_new_local). - static Server new_local(const std::string &server_dir); - - // Construct a remote server (tc_server_new_sync). - static Server new_sync(const std::string &url, const std::string &client_id, - const std::string &encryption_secret); - - // Construct a GCP server (tc_server_new_gcp). - static Server new_gcp(const std::string &bucket, const std::string &credential_path, - const std::string &encryption_secret); - - // This object "owns" inner, so copy is not allowed. - Server(const Server &) = delete; - Server &operator=(const Server &) = delete; - - // Explicit move constructor and assignment - Server(Server &&) noexcept; - Server &operator=(Server &&) noexcept; - - protected: - Server(unique_tcserver_ptr inner) : inner(std::move(inner)) {}; - - unique_tcserver_ptr inner; - - // Replica accesses the inner pointer to call tc_replica_sync - friend class Replica; - - // construct an error message from the given string. - std::string server_error(tc::ffi::TCString string); -}; -} // namespace tc - -#endif -//////////////////////////////////////////////////////////////////////////////// diff --git a/src/tc/Task.cpp b/src/tc/Task.cpp deleted file mode 100644 index 5b5e5fd9a..000000000 --- a/src/tc/Task.cpp +++ /dev/null @@ -1,162 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// -// Copyright 2022, Dustin J. Mitchell -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// https://www.opensource.org/licenses/mit-license.php -// -//////////////////////////////////////////////////////////////////////////////// - -#include -// cmake.h include header must come first - -#include - -#include "tc/Task.h" -#include "tc/util.h" - -using namespace tc::ffi; - -//////////////////////////////////////////////////////////////////////////////// -tc::Task::Task(TCTask* tctask) { - inner = unique_tctask_ptr(tctask, [](TCTask* task) { tc_task_free(task); }); -} - -//////////////////////////////////////////////////////////////////////////////// -tc::Task::Task(Task&& other) noexcept { - // move inner from other - inner = unique_tctask_ptr(other.inner.release(), [](TCTask* task) { tc_task_free(task); }); -} - -//////////////////////////////////////////////////////////////////////////////// -tc::Task& tc::Task::operator=(Task&& other) noexcept { - if (this != &other) { - // move inner from other - inner = unique_tctask_ptr(other.inner.release(), [](TCTask* task) { tc_task_free(task); }); - } - return *this; -} - -//////////////////////////////////////////////////////////////////////////////// -void tc::Task::to_mut(TCReplica* replica) { tc_task_to_mut(&*inner, replica); } - -//////////////////////////////////////////////////////////////////////////////// -void tc::Task::to_immut() { tc_task_to_immut(&*inner); } - -//////////////////////////////////////////////////////////////////////////////// -std::string tc::Task::get_uuid() const { - auto uuid = tc_task_get_uuid(&*inner); - return tc2uuid(uuid); -} - -//////////////////////////////////////////////////////////////////////////////// -tc::Status tc::Task::get_status() const { - auto status = tc_task_get_status(&*inner); - return tc::Status(status); -} - -//////////////////////////////////////////////////////////////////////////////// -std::map tc::Task::get_taskmap() const { - TCKVList kv = tc_task_get_taskmap(&*inner); - if (!kv.items) { - throw task_error(); - } - - std::map taskmap; - for (size_t i = 0; i < kv.len; i++) { - auto k = tc2string_clone(kv.items[i].key); - auto v = tc2string_clone(kv.items[i].value); - taskmap[k] = v; - } - - return taskmap; -} - -//////////////////////////////////////////////////////////////////////////////// -std::string tc::Task::get_description() const { - auto desc = tc_task_get_description(&*inner); - return tc2string(desc); -} - -//////////////////////////////////////////////////////////////////////////////// -std::optional tc::Task::get_value(std::string property) const { - auto maybe_desc = tc_task_get_value(&*inner, string2tc(property)); - if (maybe_desc.ptr == NULL) { - return std::nullopt; - } - return std::make_optional(tc2string(maybe_desc)); -} - -bool tc::Task::is_waiting() const { return tc_task_is_waiting(&*inner); } - -//////////////////////////////////////////////////////////////////////////////// -bool tc::Task::is_active() const { return tc_task_is_active(&*inner); } - -//////////////////////////////////////////////////////////////////////////////// -bool tc::Task::is_blocked() const { return tc_task_is_blocked(&*inner); } - -//////////////////////////////////////////////////////////////////////////////// -bool tc::Task::is_blocking() const { return tc_task_is_blocking(&*inner); } - -//////////////////////////////////////////////////////////////////////////////// -void tc::Task::set_status(tc::Status status) { - TCResult res = tc_task_set_status(&*inner, (TCStatus)status); - if (res != TC_RESULT_OK) { - throw task_error(); - } -} - -//////////////////////////////////////////////////////////////////////////////// -void tc::Task::set_value(std::string property, std::optional value) { - TCResult res; - if (value.has_value()) { - res = tc_task_set_value(&*inner, string2tc(property), string2tc(value.value())); - } else { - TCString nullstr; - nullstr.ptr = NULL; - res = tc_task_set_value(&*inner, string2tc(property), nullstr); - } - if (res != TC_RESULT_OK) { - throw task_error(); - } -} - -//////////////////////////////////////////////////////////////////////////////// -void tc::Task::set_modified(time_t modified) { - TCResult res = tc_task_set_modified(&*inner, modified); - if (res != TC_RESULT_OK) { - throw task_error(); - } -} - -//////////////////////////////////////////////////////////////////////////////// -std::string tc::Task::task_error() const { - TCString error = tc_task_error(&*inner); - std::string errmsg; - if (!error.ptr) { - errmsg = std::string("Unknown TaskChampion error"); - } else { - errmsg = std::string(tc_string_content(&error)); - } - tc_string_free(&error); - return errmsg; -} - -//////////////////////////////////////////////////////////////////////////////// diff --git a/src/tc/Task.h b/src/tc/Task.h deleted file mode 100644 index d087dc810..000000000 --- a/src/tc/Task.h +++ /dev/null @@ -1,130 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// -// Copyright 2022, Dustin J. Mitchell, Tomas Babej, Paul Beckingham, Federico Hernandez. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// https://www.opensource.org/licenses/mit-license.php -// -//////////////////////////////////////////////////////////////////////////////// - -#ifndef INCLUDED_TC_TASK -#define INCLUDED_TC_TASK - -#include -#include -#include -#include -#include - -#include "tc/ffi.h" - -namespace tc { -class Replica; -class ReplicaGuard; - -enum Status { - Pending = tc::ffi::TC_STATUS_PENDING, - Completed = tc::ffi::TC_STATUS_COMPLETED, - Deleted = tc::ffi::TC_STATUS_DELETED, - Recurring = tc::ffi::TC_STATUS_RECURRING, - Unknown = tc::ffi::TC_STATUS_UNKNOWN, -}; - -// a unique_ptr to a TCReplica which will automatically free the value when -// it goes out of scope. -using unique_tctask_ptr = std::unique_ptr>; - -// Task wraps the TCTask type, managing its memory, errors, and so on. -// -// Except as noted, method names match the suffix to `tc_task_..`. -class Task { - protected: - // Tasks may only be created and made mutable/immutable - // by tc::Replica - friend class tc::Replica; - explicit Task(tc::ffi::TCTask *); - - // RplicaGuard handles mut/immut - friend class tc::ReplicaGuard; - void to_mut(tc::ffi::TCReplica *); - void to_immut(); - - public: - // This object "owns" inner, so copy is not allowed. - Task(const Task &) = delete; - Task &operator=(const Task &) = delete; - - // Explicit move constructor and assignment - Task(Task &&) noexcept; - Task &operator=(Task &&) noexcept; - - std::string get_uuid() const; - Status get_status() const; - std::map get_taskmap() const; - std::string get_description() const; - std::optional get_value(std::string property) const; - // TODO: time_t tc_task_get_entry(struct TCTask *task); - // TODO: time_t tc_task_get_wait(struct TCTask *task); - // TODO: time_t tc_task_get_modified(struct TCTask *task); - bool is_waiting() const; - bool is_active() const; - bool is_blocked() const; - bool is_blocking() const; - // TODO: bool tc_task_has_tag(struct TCTask *task, struct TCString tag); - // TODO: struct TCStringList tc_task_get_tags(struct TCTask *task); - // TODO: struct TCAnnotationList tc_task_get_annotations(struct TCTask *task); - // TODO: struct TCString tc_task_get_uda(struct TCTask *task, struct TCString ns, struct TCString - // key); - // TODO: struct TCString tc_task_get_legacy_uda(struct TCTask *task, struct TCString key); - // TODO: struct TCUdaList tc_task_get_udas(struct TCTask *task); - // TODO: struct TCUdaList tc_task_get_legacy_udas(struct TCTask *task); - void set_status(Status status); - // TODO: TCResult tc_task_set_description(struct TCTask *task, struct TCString description); - void set_value(std::string property, std::optional value); - // TODO: TCResult tc_task_set_entry(struct TCTask *task, time_t entry); - // TODO: TCResult tc_task_set_wait(struct TCTask *task, time_t wait); - void set_modified(time_t modified); - // TODO: TCResult tc_task_start(struct TCTask *task); - // TODO: TCResult tc_task_stop(struct TCTask *task); - // TODO: TCResult tc_task_done(struct TCTask *task); - // TODO: TCResult tc_task_delete(struct TCTask *task); - // TODO: TCResult tc_task_add_tag(struct TCTask *task, struct TCString tag); - // TODO: TCResult tc_task_remove_tag(struct TCTask *task, struct TCString tag); - // TODO: TCResult tc_task_add_annotation(struct TCTask *task, struct TCAnnotation *annotation); - // TODO: TCResult tc_task_remove_annotation(struct TCTask *task, int64_t entry); - // TODO: TCResult tc_task_set_uda(struct TCTask *task, - // TODO: TCResult tc_task_remove_uda(struct TCTask *task, struct TCString ns, struct TCString - // key); - // TODO: TCResult tc_task_set_legacy_uda(struct TCTask *task, struct TCString key, struct TCString - // value); - // TODO: TCResult tc_task_remove_legacy_uda(struct TCTask *task, struct TCString key); - - private: - unique_tctask_ptr inner; - - std::string task_error() const; // tc_task_error -}; -} // namespace tc - -// TODO: struct TCTask *tc_task_list_take(struct TCTaskList *tasks, size_t index); -// TODO: void tc_task_list_free(struct TCTaskList *tasks); - -#endif -//////////////////////////////////////////////////////////////////////////////// diff --git a/src/tc/WorkingSet.cpp b/src/tc/WorkingSet.cpp deleted file mode 100644 index 1adeb8f9d..000000000 --- a/src/tc/WorkingSet.cpp +++ /dev/null @@ -1,87 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// -// Copyright 2022, Dustin J. Mitchell -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// https://www.opensource.org/licenses/mit-license.php -// -//////////////////////////////////////////////////////////////////////////////// - -#include -// cmake.h include header must come first - -#include - -#include "tc/Task.h" -#include "tc/WorkingSet.h" -#include "tc/util.h" - -using namespace tc::ffi; - -//////////////////////////////////////////////////////////////////////////////// -tc::WorkingSet::WorkingSet(WorkingSet&& other) noexcept { - // move inner from other - inner = unique_tcws_ptr(other.inner.release(), [](TCWorkingSet* ws) { tc_working_set_free(ws); }); -} - -//////////////////////////////////////////////////////////////////////////////// -tc::WorkingSet& tc::WorkingSet::operator=(WorkingSet&& other) noexcept { - if (this != &other) { - // move inner from other - inner = - unique_tcws_ptr(other.inner.release(), [](TCWorkingSet* ws) { tc_working_set_free(ws); }); - } - return *this; -} - -//////////////////////////////////////////////////////////////////////////////// -tc::WorkingSet::WorkingSet(tc::ffi::TCWorkingSet* tcws) { - inner = unique_tcws_ptr(tcws, [](TCWorkingSet* ws) { tc_working_set_free(ws); }); -} - -//////////////////////////////////////////////////////////////////////////////// -size_t tc::WorkingSet::len() const noexcept { return tc_working_set_len(&*inner); } - -//////////////////////////////////////////////////////////////////////////////// -size_t tc::WorkingSet::largest_index() const noexcept { - return tc_working_set_largest_index(&*inner); -} - -//////////////////////////////////////////////////////////////////////////////// -std::optional tc::WorkingSet::by_index(size_t index) const noexcept { - TCUuid uuid; - if (tc_working_set_by_index(&*inner, index, &uuid)) { - return std::make_optional(tc2uuid(uuid)); - } else { - return std::nullopt; - } -} - -//////////////////////////////////////////////////////////////////////////////// -std::optional tc::WorkingSet::by_uuid(const std::string& uuid) const noexcept { - auto index = tc_working_set_by_uuid(&*inner, uuid2tc(uuid)); - if (index > 0) { - return std::make_optional(index); - } else { - return std::nullopt; - } -} - -//////////////////////////////////////////////////////////////////////////////// diff --git a/src/tc/WorkingSet.h b/src/tc/WorkingSet.h deleted file mode 100644 index e9a3a5d5c..000000000 --- a/src/tc/WorkingSet.h +++ /dev/null @@ -1,74 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// -// Copyright 2022, Dustin J. Mitchell -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// https://www.opensource.org/licenses/mit-license.php -// -//////////////////////////////////////////////////////////////////////////////// - -#ifndef INCLUDED_TC_WORKINGSET -#define INCLUDED_TC_WORKINGSET - -#include -#include -#include -#include - -#include "tc/Task.h" -#include "tc/ffi.h" - -namespace tc { -class Task; - -// a unique_ptr to a TCWorkingSet which will automatically free the value when -// it goes out of scope. -using unique_tcws_ptr = - std::unique_ptr>; - -// WorkingSet wraps the TCWorkingSet type, managing its memory, errors, and so on. -// -// Except as noted, method names match the suffix to `tc_working_set_..`. -class WorkingSet { - protected: - friend class tc::Replica; - WorkingSet(tc::ffi::TCWorkingSet *); // via tc_replica_working_set - - public: - // This object "owns" inner, so copy is not allowed. - WorkingSet(const WorkingSet &) = delete; - WorkingSet &operator=(const WorkingSet &) = delete; - - // Explicit move constructor and assignment - WorkingSet(WorkingSet &&) noexcept; - WorkingSet &operator=(WorkingSet &&) noexcept; - - size_t len() const noexcept; // tc_working_set_len - size_t largest_index() const noexcept; // tc_working_set_largest_index - std::optional by_index(size_t index) const noexcept; // tc_working_set_by_index - std::optional by_uuid(const std::string &index) const noexcept; // tc_working_set_by_uuid - - private: - unique_tcws_ptr inner; -}; -} // namespace tc - -#endif -//////////////////////////////////////////////////////////////////////////////// diff --git a/src/tc/ffi.h b/src/tc/ffi.h deleted file mode 100644 index 213fc4074..000000000 --- a/src/tc/ffi.h +++ /dev/null @@ -1,36 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// -// Copyright 2022, Dustin J. Mitchell, Tomas Babej, Paul Beckingham, Federico Hernandez. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// https://www.opensource.org/licenses/mit-license.php -// -//////////////////////////////////////////////////////////////////////////////// - -#ifndef INCLUDED_TC_FFI -#define INCLUDED_TC_FFI - -// The entire FFI API is embedded in the `tc::ffi` namespace -namespace tc::ffi { -#include -} - -#endif -//////////////////////////////////////////////////////////////////////////////// diff --git a/src/tc/lib/Cargo.toml b/src/tc/lib/Cargo.toml deleted file mode 100644 index 2a48d5293..000000000 --- a/src/tc/lib/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "taskchampion-lib" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -name = "taskchampion_lib" -crate-type = ["staticlib", "rlib"] - -[dependencies] -libc.workspace = true -anyhow.workspace = true -ffizz-header.workspace = true -taskchampion.workspace = true - -[dev-dependencies] -pretty_assertions.workspace = true diff --git a/src/tc/lib/src/annotation.rs b/src/tc/lib/src/annotation.rs deleted file mode 100644 index 5edfda4c8..000000000 --- a/src/tc/lib/src/annotation.rs +++ /dev/null @@ -1,186 +0,0 @@ -use crate::traits::*; -use crate::types::*; -use taskchampion::chrono::prelude::*; - -#[ffizz_header::item] -#[ffizz(order = 400)] -/// ***** TCAnnotation ***** -/// -/// TCAnnotation contains the details of an annotation. -/// -/// # Safety -/// -/// An annotation must be initialized from a tc_.. function, and later freed -/// with `tc_annotation_free` or `tc_annotation_list_free`. -/// -/// Any function taking a `*TCAnnotation` requires: -/// - the pointer must not be NUL; -/// - the pointer must be one previously returned from a tc_… function; -/// - the memory referenced by the pointer must never be modified by C code; and -/// - ownership transfers to the called function, and the value must not be used -/// after the call returns. In fact, the value will be zeroed out to ensure this. -/// -/// TCAnnotations are not threadsafe. -/// -/// ```c -/// typedef struct TCAnnotation { -/// // Time the annotation was made. Must be nonzero. -/// time_t entry; -/// // Content of the annotation. Must not be NULL. -/// TCString description; -/// } TCAnnotation; -/// ``` -#[repr(C)] -pub struct TCAnnotation { - pub entry: libc::time_t, - pub description: TCString, -} - -impl PassByValue for TCAnnotation { - // NOTE: we cannot use `RustType = Annotation` here because conversion of the - // Rust to a String can fail. - type RustType = (DateTime, RustString<'static>); - - unsafe fn from_ctype(mut self) -> Self::RustType { - // SAFETY: - // - any time_t value is valid - // - time_t is copy, so ownership is not important - let entry = unsafe { libc::time_t::val_from_arg(self.entry) }.unwrap(); - // SAFETY: - // - self.description is valid (came from return_val in as_ctype) - // - self is owned, so we can take ownership of this TCString - let description = - unsafe { TCString::take_val_from_arg(&mut self.description, TCString::default()) }; - (entry, description) - } - - fn as_ctype((entry, description): Self::RustType) -> Self { - TCAnnotation { - entry: libc::time_t::as_ctype(Some(entry)), - // SAFETY: - // - ownership of the TCString tied to ownership of Self - description: unsafe { TCString::return_val(description) }, - } - } -} - -impl Default for TCAnnotation { - fn default() -> Self { - TCAnnotation { - entry: 0 as libc::time_t, - description: TCString::default(), - } - } -} - -#[ffizz_header::item] -#[ffizz(order = 410)] -/// ***** TCAnnotationList ***** -/// -/// TCAnnotationList represents a list of annotations. -/// -/// The content of this struct must be treated as read-only. -/// -/// ```c -/// typedef struct TCAnnotationList { -/// // number of annotations in items -/// size_t len; -/// // reserved -/// size_t _u1; -/// // Array of annotations. These remain owned by the TCAnnotationList instance and will be freed by -/// // tc_annotation_list_free. This pointer is never NULL for a valid TCAnnotationList. -/// struct TCAnnotation *items; -/// } TCAnnotationList; -/// ``` -#[repr(C)] -pub struct TCAnnotationList { - len: libc::size_t, - /// total size of items (internal use only) - capacity: libc::size_t, - items: *mut TCAnnotation, -} - -impl CList for TCAnnotationList { - type Element = TCAnnotation; - - unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self { - TCAnnotationList { - len, - capacity: cap, - items, - } - } - - fn slice(&mut self) -> &mut [Self::Element] { - // SAFETY: - // - because we have &mut self, we have read/write access to items[0..len] - // - all items are properly initialized Element's - // - return value lifetime is equal to &mmut self's, so access is exclusive - // - items and len came from Vec, so total size is < isize::MAX - unsafe { std::slice::from_raw_parts_mut(self.items, self.len) } - } - - fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) { - (self.items, self.len, self.capacity) - } -} - -#[ffizz_header::item] -#[ffizz(order = 401)] -/// Free a TCAnnotation instance. The instance, and the TCString it contains, must not be used -/// after this call. -/// -/// ```c -/// EXTERN_C void tc_annotation_free(struct TCAnnotation *tcann); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_annotation_free(tcann: *mut TCAnnotation) { - debug_assert!(!tcann.is_null()); - // SAFETY: - // - tcann is not NULL - // - *tcann is a valid TCAnnotation (caller promised to treat it as read-only) - let annotation = unsafe { TCAnnotation::take_val_from_arg(tcann, TCAnnotation::default()) }; - drop(annotation); -} - -#[ffizz_header::item] -#[ffizz(order = 411)] -/// Free a TCAnnotationList instance. The instance, and all TCAnnotations it contains, must not be used after -/// this call. -/// -/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCAnnotationList. -/// -/// ```c -/// EXTERN_C void tc_annotation_list_free(struct TCAnnotationList *tcanns); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_annotation_list_free(tcanns: *mut TCAnnotationList) { - // SAFETY: - // - tcanns is not NULL and points to a valid TCAnnotationList (caller is not allowed to - // modify the list) - // - caller promises not to use the value after return - unsafe { drop_value_list(tcanns) } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn empty_list_has_non_null_pointer() { - let tcanns = unsafe { TCAnnotationList::return_val(Vec::new()) }; - assert!(!tcanns.items.is_null()); - assert_eq!(tcanns.len, 0); - assert_eq!(tcanns.capacity, 0); - } - - #[test] - fn free_sets_null_pointer() { - let mut tcanns = unsafe { TCAnnotationList::return_val(Vec::new()) }; - // SAFETY: testing expected behavior - unsafe { tc_annotation_list_free(&mut tcanns) }; - assert!(tcanns.items.is_null()); - assert_eq!(tcanns.len, 0); - assert_eq!(tcanns.capacity, 0); - } -} diff --git a/src/tc/lib/src/atomic.rs b/src/tc/lib/src/atomic.rs deleted file mode 100644 index 01c72059e..000000000 --- a/src/tc/lib/src/atomic.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Trait implementations for a few atomic types - -use crate::traits::*; -use taskchampion::chrono::{DateTime, Utc}; -use taskchampion::utc_timestamp; - -impl PassByValue for usize { - type RustType = usize; - - unsafe fn from_ctype(self) -> usize { - self - } - - fn as_ctype(arg: usize) -> usize { - arg - } -} - -/// Convert an Option> to a libc::time_t, or zero if not set. -impl PassByValue for libc::time_t { - type RustType = Option>; - - unsafe fn from_ctype(self) -> Option> { - if self != 0 { - return Some(utc_timestamp(self)); - } - None - } - - fn as_ctype(arg: Option>) -> libc::time_t { - arg.map(|ts| ts.timestamp() as libc::time_t) - .unwrap_or(0 as libc::time_t) - } -} diff --git a/src/tc/lib/src/kv.rs b/src/tc/lib/src/kv.rs deleted file mode 100644 index 3831c7016..000000000 --- a/src/tc/lib/src/kv.rs +++ /dev/null @@ -1,155 +0,0 @@ -use crate::traits::*; -use crate::types::*; - -#[ffizz_header::item] -#[ffizz(order = 600)] -/// ***** TCKV ***** -/// -/// TCKV contains a key/value pair that is part of a task. -/// -/// Neither key nor value are ever NULL. They remain owned by the TCKV and -/// will be freed when it is freed with tc_kv_list_free. -/// -/// ```c -/// typedef struct TCKV { -/// struct TCString key; -/// struct TCString value; -/// } TCKV; -/// ``` -#[repr(C)] -#[derive(Debug)] -pub struct TCKV { - pub key: TCString, - pub value: TCString, -} - -impl PassByValue for TCKV { - type RustType = (RustString<'static>, RustString<'static>); - - unsafe fn from_ctype(self) -> Self::RustType { - // SAFETY: - // - self.key is not NULL (field docstring) - // - self.key came from return_ptr in as_ctype - // - self is owned, so we can take ownership of this TCString - let key = unsafe { TCString::val_from_arg(self.key) }; - // SAFETY: (same) - let value = unsafe { TCString::val_from_arg(self.value) }; - (key, value) - } - - fn as_ctype((key, value): Self::RustType) -> Self { - TCKV { - // SAFETY: - // - ownership of the TCString tied to ownership of Self - key: unsafe { TCString::return_val(key) }, - // SAFETY: - // - ownership of the TCString tied to ownership of Self - value: unsafe { TCString::return_val(value) }, - } - } -} - -#[ffizz_header::item] -#[ffizz(order = 610)] -/// ***** TCKVList ***** -/// -/// TCKVList represents a list of key/value pairs. -/// -/// The content of this struct must be treated as read-only. -/// -/// ```c -/// typedef struct TCKVList { -/// // number of key/value pairs in items -/// size_t len; -/// // reserved -/// size_t _u1; -/// // Array of TCKV's. These remain owned by the TCKVList instance and will be freed by -/// // tc_kv_list_free. This pointer is never NULL for a valid TCKVList. -/// struct TCKV *items; -/// } TCKVList; -/// ``` -#[repr(C)] -#[derive(Debug)] -pub struct TCKVList { - pub len: libc::size_t, - /// total size of items (internal use only) - pub _capacity: libc::size_t, - pub items: *mut TCKV, -} - -impl CList for TCKVList { - type Element = TCKV; - - unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self { - TCKVList { - len, - _capacity: cap, - items, - } - } - - fn slice(&mut self) -> &mut [Self::Element] { - // SAFETY: - // - because we have &mut self, we have read/write access to items[0..len] - // - all items are properly initialized Element's - // - return value lifetime is equal to &mmut self's, so access is exclusive - // - items and len came from Vec, so total size is < isize::MAX - unsafe { std::slice::from_raw_parts_mut(self.items, self.len) } - } - - fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) { - (self.items, self.len, self._capacity) - } -} - -impl Default for TCKVList { - fn default() -> Self { - // SAFETY: - // - caller will free this list - unsafe { TCKVList::return_val(Vec::new()) } - } -} - -// NOTE: callers never have a TCKV that is not in a list, so there is no tc_kv_free. - -#[ffizz_header::item] -#[ffizz(order = 611)] -/// Free a TCKVList instance. The instance, and all TCKVs it contains, must not be used after -/// this call. -/// -/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCKVList. -/// -/// ```c -/// EXTERN_C void tc_kv_list_free(struct TCKVList *tckvs); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_kv_list_free(tckvs: *mut TCKVList) { - // SAFETY: - // - tckvs is not NULL and points to a valid TCKVList (caller is not allowed to - // modify the list) - // - caller promises not to use the value after return - unsafe { drop_value_list(tckvs) } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn empty_list_has_non_null_pointer() { - let tckvs = unsafe { TCKVList::return_val(Vec::new()) }; - assert!(!tckvs.items.is_null()); - assert_eq!(tckvs.len, 0); - assert_eq!(tckvs._capacity, 0); - } - - #[test] - fn free_sets_null_pointer() { - let mut tckvs = unsafe { TCKVList::return_val(Vec::new()) }; - // SAFETY: testing expected behavior - unsafe { tc_kv_list_free(&mut tckvs) }; - assert!(tckvs.items.is_null()); - assert_eq!(tckvs.len, 0); - assert_eq!(tckvs._capacity, 0); - } -} diff --git a/src/tc/lib/src/lib.rs b/src/tc/lib/src/lib.rs deleted file mode 100644 index ad1fc31f7..000000000 --- a/src/tc/lib/src/lib.rs +++ /dev/null @@ -1,172 +0,0 @@ -#![warn(unsafe_op_in_unsafe_fn)] -#![allow(unused_unsafe)] -// Not working yet in stable - https://github.com/rust-lang/rust-clippy/issues/8020 -// #![warn(clippy::undocumented_unsafe_blocks)] - -// docstrings for extern "C" functions are reflected into C, and do not benefit -// from safety docs. -#![allow(clippy::missing_safety_doc)] -// deny some things that are typically warnings -#![deny(clippy::derivable_impls)] -#![deny(clippy::wrong_self_convention)] -#![deny(clippy::extra_unused_lifetimes)] -#![deny(clippy::unnecessary_to_owned)] - -// ffizz_header orders: -// -// 000-099: header matter -// 100-199: TCResult -// 200-299: TCString / List -// 300-399: TCUuid / List -// 400-499: TCAnnotation / List -// 500-599: TCUda / List -// 600-699: TCKV / List -// 700-799: TCStatus -// 800-899: TCServer -// 900-999: TCReplica -// 1000-1099: TCTask / List -// 1100-1199: TCWorkingSet -// 10000-10099: footer - -ffizz_header::snippet! { -#[ffizz(name="intro", order=0)] -/// TaskChampion -/// -/// This file defines the C interface to libtaskchampion. This is a thin wrapper around the Rust -/// `taskchampion` crate. Refer to the documentation for that crate at -/// https://docs.rs/taskchampion/latest/taskchampion/ for API details. The comments in this file -/// focus mostly on the low-level details of passing values to and from TaskChampion. -/// -/// # Overview -/// -/// This library defines four major types used to interact with the API, that map directly to Rust -/// types. -/// -/// * TCReplica - see https://docs.rs/taskchampion/latest/taskchampion/struct.Replica.html -/// * TCTask - see https://docs.rs/taskchampion/latest/taskchampion/struct.Task.html -/// * TCServer - see https://docs.rs/taskchampion/latest/taskchampion/trait.Server.html -/// * TCWorkingSet - see https://docs.rs/taskchampion/latest/taskchampion/struct.WorkingSet.html -/// -/// It also defines a few utility types: -/// -/// * TCString - a wrapper around both C (NUL-terminated) and Rust (always utf-8) strings. -/// * TC…List - a list of objects represented as a C array -/// * see below for the remainder -/// -/// # Safety -/// -/// Each type contains specific instructions to ensure memory safety. The general rules are as -/// follows. -/// -/// No types in this library are threadsafe. All values should be used in only one thread for their -/// entire lifetime. It is safe to use unrelated values in different threads (for example, -/// different threads may use different TCReplica values concurrently). -/// -/// ## Pass by Pointer -/// -/// Several types such as TCReplica and TCString are "opaque" types and always handled as pointers -/// in C. The bytes these pointers address are private to the Rust implementation and must not be -/// accessed from C. -/// -/// Pass-by-pointer values have exactly one owner, and that owner is responsible for freeing the -/// value (using a `tc_…_free` function), or transferring ownership elsewhere. Except where -/// documented otherwise, when a value is passed to C, ownership passes to C as well. When a value -/// is passed to Rust, ownership stays with the C code. The exception is TCString, ownership of -/// which passes to Rust when it is used as a function argument. -/// -/// The limited circumstances where one value must not outlive another, due to pointer references -/// between them, are documented below. -/// -/// ## Pass by Value -/// -/// Types such as TCUuid and TC…List are passed by value, and contain fields that are accessible -/// from C. C code is free to access the content of these types in a _read_only_ fashion. -/// -/// Pass-by-value values that contain pointers also have exactly one owner, responsible for freeing -/// the value or transferring ownership. The tc_…_free functions for these types will replace the -/// pointers with NULL to guard against use-after-free errors. The interior pointers in such values -/// should never be freed directly (for example, `tc_string_free(tcuda.value)` is an error). -/// -/// TCUuid is a special case, because it does not contain pointers. It can be freely copied and -/// need not be freed. -/// -/// ## Lists -/// -/// Lists are a special kind of pass-by-value type. Each contains `len` and `items`, where `items` -/// is an array of length `len`. Lists, and the values in the `items` array, must be treated as -/// read-only. On return from an API function, a list's ownership is with the C caller, which must -/// eventually free the list. List data must be freed with the `tc_…_list_free` function. It is an -/// error to free any value in the `items` array of a list. -} - -ffizz_header::snippet! { -#[ffizz(name="topmatter", order=1)] -/// ```c -/// #ifndef TASKCHAMPION_H -/// #define TASKCHAMPION_H -/// -/// #include -/// #include -/// #include -/// -/// #ifdef __cplusplus -/// #define EXTERN_C extern "C" -/// #else -/// #define EXTERN_C -/// #endif // __cplusplus -/// ``` -} - -ffizz_header::snippet! { -#[ffizz(name="bottomatter", order=10000)] -/// ```c -/// #endif /* TASKCHAMPION_H */ -/// ``` -} - -mod traits; -mod util; - -pub mod annotation; -pub use annotation::*; -pub mod atomic; -pub mod kv; -pub use kv::*; -pub mod replica; -pub use replica::*; -pub mod result; -pub use result::*; -pub mod server; -pub use server::*; -pub mod status; -pub use status::*; -pub mod string; -pub use string::*; -pub mod task; -pub use task::*; -pub mod uda; -pub use uda::*; -pub mod uuid; -pub use uuid::*; -pub mod workingset; -pub use workingset::*; - -pub(crate) mod types { - pub(crate) use crate::annotation::{TCAnnotation, TCAnnotationList}; - pub(crate) use crate::kv::TCKVList; - 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::{RustString, TCString, TCStringList}; - pub(crate) use crate::task::{TCTask, TCTaskList}; - pub(crate) use crate::uda::{TCUda, TCUdaList, Uda}; - pub(crate) use crate::uuid::{TCUuid, TCUuidList}; - pub(crate) use crate::workingset::TCWorkingSet; -} - -#[cfg(debug_assertions)] -/// Generate the taskchapion.h header -pub fn generate_header() -> String { - ffizz_header::generate() -} diff --git a/src/tc/lib/src/replica.rs b/src/tc/lib/src/replica.rs deleted file mode 100644 index 340ef878b..000000000 --- a/src/tc/lib/src/replica.rs +++ /dev/null @@ -1,958 +0,0 @@ -use crate::traits::*; -use crate::types::*; -use crate::util::err_to_ruststring; -use std::ptr::NonNull; -use taskchampion::storage::ReplicaOp; -use taskchampion::{Replica, StorageConfig}; - -#[ffizz_header::item] -#[ffizz(order = 900)] -/// ***** TCReplica ***** -/// -/// A replica represents an instance of a user's task data, providing an easy interface -/// for querying and modifying that data. -/// -/// # Error Handling -/// -/// When a `tc_replica_..` function that returns a TCResult returns TC_RESULT_ERROR, then -/// `tc_replica_error` will return the error message. -/// -/// # Safety -/// -/// The `*TCReplica` returned from `tc_replica_new…` functions is owned by the caller and -/// must later be freed to avoid a memory leak. -/// -/// Any function taking a `*TCReplica` requires: -/// - the pointer must not be NUL; -/// - the pointer must be one previously returned from a tc_… function; -/// - the memory referenced by the pointer must never be modified by C code; and -/// - except for `tc_replica_free`, ownership of a `*TCReplica` remains with the caller. -/// -/// Once passed to `tc_replica_free`, a `*TCReplica` becomes invalid and must not be used again. -/// -/// TCReplicas are not threadsafe. -/// -/// ```c -/// typedef struct TCReplica TCReplica; -/// ``` -pub struct TCReplica { - /// The wrapped Replica - inner: Replica, - - /// If true, this replica has an outstanding &mut (for a TaskMut) - mut_borrowed: bool, - - /// The error from the most recent operation, if any - error: Option>, -} - -impl PassByPointer for TCReplica {} - -impl TCReplica { - /// Mutably borrow the inner Replica - pub(crate) fn borrow_mut(&mut self) -> &mut Replica { - if self.mut_borrowed { - panic!("replica is already borrowed"); - } - self.mut_borrowed = true; - &mut self.inner - } - - /// Release the borrow made by [`borrow_mut`] - pub(crate) fn release_borrow(&mut self) { - if !self.mut_borrowed { - panic!("replica is not borrowed"); - } - self.mut_borrowed = false; - } -} - -impl From for TCReplica { - fn from(rep: Replica) -> TCReplica { - TCReplica { - inner: rep, - mut_borrowed: false, - error: None, - } - } -} - -/// Utility function to allow using `?` notation to return an error value. This makes -/// a mutable borrow, because most Replica methods require a `&mut`. -fn wrap(rep: *mut TCReplica, f: F, err_value: T) -> T -where - F: FnOnce(&mut Replica) -> anyhow::Result, -{ - debug_assert!(!rep.is_null()); - // SAFETY: - // - rep is not NULL (promised by caller) - // - *rep is a valid TCReplica (promised by caller) - // - rep is valid for the duration of this function - // - rep is not modified by anything else (not threadsafe) - let rep: &mut TCReplica = unsafe { TCReplica::from_ptr_arg_ref_mut(rep) }; - if rep.mut_borrowed { - panic!("replica is borrowed and cannot be used"); - } - rep.error = None; - match f(&mut rep.inner) { - Ok(v) => v, - Err(e) => { - rep.error = Some(err_to_ruststring(e)); - err_value - } - } -} - -/// Utility function to allow using `?` notation to return an error value in the constructor. -fn wrap_constructor(f: F, error_out: *mut TCString, err_value: T) -> T -where - F: FnOnce() -> anyhow::Result, -{ - if !error_out.is_null() { - // SAFETY: - // - error_out is not NULL (just checked) - // - properly aligned and valid (promised by caller) - unsafe { *error_out = TCString::default() }; - } - - match f() { - Ok(v) => v, - Err(e) => { - if !error_out.is_null() { - // SAFETY: - // - error_out is not NULL (just checked) - // - properly aligned and valid (promised by caller) - unsafe { - TCString::val_to_arg_out(err_to_ruststring(e), error_out); - } - } - err_value - } - } -} - -#[ffizz_header::item] -#[ffizz(order = 900)] -/// ***** TCReplicaOpType ***** -/// -/// ```c -/// enum TCReplicaOpType -/// #ifdef __cplusplus -/// : uint32_t -/// #endif // __cplusplus -/// { -/// Create = 0, -/// Delete = 1, -/// Update = 2, -/// UndoPoint = 3, -/// }; -/// #ifndef __cplusplus -/// typedef uint32_t TCReplicaOpType; -/// #endif // __cplusplus -/// ``` -#[derive(Debug, Default)] -#[repr(u32)] -pub enum TCReplicaOpType { - Create = 0, - Delete = 1, - Update = 2, - UndoPoint = 3, - #[default] - Error = 4, -} - -#[ffizz_header::item] -#[ffizz(order = 901)] -/// Create a new TCReplica with an in-memory database. The contents of the database will be -/// lost when it is freed with tc_replica_free. -/// -/// ```c -/// EXTERN_C struct TCReplica *tc_replica_new_in_memory(void); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_new_in_memory() -> *mut TCReplica { - let storage = StorageConfig::InMemory - .into_storage() - .expect("in-memory always succeeds"); - // SAFETY: - // - caller promises to free this value - unsafe { TCReplica::from(Replica::new(storage)).return_ptr() } -} - -#[ffizz_header::item] -#[ffizz(order = 901)] -/// 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. -/// -/// ```c -/// EXTERN_C struct TCReplica *tc_replica_new_on_disk(struct TCString path, -/// bool create_if_missing, -/// struct TCString *error_out); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_new_on_disk( - path: TCString, - create_if_missing: bool, - error_out: *mut TCString, -) -> *mut TCReplica { - wrap_constructor( - || { - // SAFETY: - // - path is valid (promised by caller) - // - caller will not use path after this call (convention) - let mut path = unsafe { TCString::val_from_arg(path) }; - let storage = StorageConfig::OnDisk { - taskdb_dir: path.to_path_buf_mut()?, - create_if_missing, - } - .into_storage()?; - - // SAFETY: - // - caller promises to free this value - Ok(unsafe { TCReplica::from(Replica::new(storage)).return_ptr() }) - }, - error_out, - std::ptr::null_mut(), - ) -} - -#[ffizz_header::item] -#[ffizz(order = 901)] -/// ***** TCReplicaOp ***** -/// -/// ```c -/// struct TCReplicaOp { -/// TCReplicaOpType operation_type; -/// void* inner; -/// }; -/// -/// typedef struct TCReplicaOp TCReplicaOp; -/// ``` -#[derive(Debug)] -#[repr(C)] -pub struct TCReplicaOp { - operation_type: TCReplicaOpType, - inner: Box, -} - -impl From for TCReplicaOp { - fn from(replica_op: ReplicaOp) -> TCReplicaOp { - match replica_op { - ReplicaOp::Create { .. } => TCReplicaOp { - operation_type: TCReplicaOpType::Create, - inner: Box::new(replica_op), - }, - ReplicaOp::Delete { .. } => TCReplicaOp { - operation_type: TCReplicaOpType::Delete, - inner: Box::new(replica_op), - }, - ReplicaOp::Update { .. } => TCReplicaOp { - operation_type: TCReplicaOpType::Update, - inner: Box::new(replica_op), - }, - ReplicaOp::UndoPoint => TCReplicaOp { - operation_type: TCReplicaOpType::UndoPoint, - inner: Box::new(replica_op), - }, - } - } -} - -#[ffizz_header::item] -#[ffizz(order = 901)] -/// ***** TCReplicaOpList ***** -/// -/// ```c -/// struct TCReplicaOpList { -/// struct TCReplicaOp *items; -/// size_t len; -/// size_t capacity; -/// }; -/// -/// typedef struct TCReplicaOpList TCReplicaOpList; -/// ``` -#[repr(C)] -#[derive(Debug)] -pub struct TCReplicaOpList { - items: *mut TCReplicaOp, - len: usize, - capacity: usize, -} - -impl Default for TCReplicaOpList { - fn default() -> Self { - // SAFETY: - // - caller will free this value - unsafe { TCReplicaOpList::return_val(Vec::new()) } - } -} - -impl CList for TCReplicaOpList { - type Element = TCReplicaOp; - - unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self { - TCReplicaOpList { - len, - capacity: cap, - items, - } - } - - fn slice(&mut self) -> &mut [Self::Element] { - // SAFETY: - // - because we have &mut self, we have read/write access to items[0..len] - // - all items are properly initialized Element's - // - return value lifetime is equal to &mmut self's, so access is exclusive - // - items and len came from Vec, so total size is < isize::MAX - unsafe { std::slice::from_raw_parts_mut(self.items, self.len) } - } - - fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) { - (self.items, self.len, self.capacity) - } -} - -#[ffizz_header::item] -#[ffizz(order = 902)] -/// Get a list of all tasks in the replica. -/// -/// Returns a TCTaskList with a NULL items field on error. -/// -/// ```c -/// EXTERN_C struct TCTaskList tc_replica_all_tasks(struct TCReplica *rep); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_all_tasks(rep: *mut TCReplica) -> TCTaskList { - wrap( - rep, - |rep| { - // note that the Replica API returns a hashmap here, but we discard - // the keys and return a simple list. The task UUIDs are available - // from task.get_uuid(), so information is not lost. - let tasks: Vec<_> = rep - .all_tasks()? - .drain() - .map(|(_uuid, t)| { - Some( - NonNull::new( - // SAFETY: - // - caller promises to free this value (via freeing the list) - unsafe { TCTask::from(t).return_ptr() }, - ) - .expect("TCTask::return_ptr returned NULL"), - ) - }) - .collect(); - // SAFETY: - // - value is not allocated and need not be freed - Ok(unsafe { TCTaskList::return_val(tasks) }) - }, - TCTaskList::null_value(), - ) -} - -#[ffizz_header::item] -#[ffizz(order = 902)] -/// Get a list of all uuids for tasks in the replica. -/// -/// Returns a TCUuidList with a NULL items field on error. -/// -/// The caller must free the UUID list with `tc_uuid_list_free`. -/// -/// ```c -/// EXTERN_C struct TCUuidList tc_replica_all_task_uuids(struct TCReplica *rep); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_all_task_uuids(rep: *mut TCReplica) -> TCUuidList { - wrap( - rep, - |rep| { - let uuids: Vec<_> = rep - .all_task_uuids()? - .drain(..) - // SAFETY: - // - value is not allocated and need not be freed - .map(|uuid| unsafe { TCUuid::return_val(uuid) }) - .collect(); - // SAFETY: - // - value will be freed (promised by caller) - Ok(unsafe { TCUuidList::return_val(uuids) }) - }, - TCUuidList::null_value(), - ) -} - -#[ffizz_header::item] -#[ffizz(order = 902)] -/// Get the current working set for this replica. The resulting value must be freed -/// with tc_working_set_free. -/// -/// Returns NULL on error. -/// -/// ```c -/// EXTERN_C struct TCWorkingSet *tc_replica_working_set(struct TCReplica *rep); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_working_set(rep: *mut TCReplica) -> *mut TCWorkingSet { - wrap( - rep, - |rep| { - let ws = rep.working_set()?; - // SAFETY: - // - caller promises to free this value - Ok(unsafe { TCWorkingSet::return_ptr(ws.into()) }) - }, - std::ptr::null_mut(), - ) -} - -#[ffizz_header::item] -#[ffizz(order = 902)] -/// Get an existing task by its UUID. -/// -/// Returns NULL when the task does not exist, and on error. Consult tc_replica_error -/// to distinguish the two conditions. -/// -/// ```c -/// EXTERN_C struct TCTask *tc_replica_get_task(struct TCReplica *rep, struct TCUuid tcuuid); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_get_task(rep: *mut TCReplica, tcuuid: TCUuid) -> *mut TCTask { - wrap( - rep, - |rep| { - // SAFETY: - // - tcuuid is a valid TCUuid (all bytes are valid) - // - tcuuid is Copy so ownership doesn't matter - let uuid = unsafe { TCUuid::val_from_arg(tcuuid) }; - if let Some(task) = rep.get_task(uuid)? { - // SAFETY: - // - caller promises to free this task - Ok(unsafe { TCTask::from(task).return_ptr() }) - } else { - Ok(std::ptr::null_mut()) - } - }, - std::ptr::null_mut(), - ) -} - -#[ffizz_header::item] -#[ffizz(order = 902)] -/// Create a new task. The task must not already exist. -/// -/// Returns the task, or NULL on error. -/// -/// ```c -/// EXTERN_C struct TCTask *tc_replica_new_task(struct TCReplica *rep, -/// enum TCStatus status, -/// struct TCString description); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_new_task( - rep: *mut TCReplica, - status: TCStatus, - description: TCString, -) -> *mut TCTask { - // SAFETY: - // - description is valid (promised by caller) - // - caller will not use description after this call (convention) - let mut description = unsafe { TCString::val_from_arg(description) }; - wrap( - rep, - |rep| { - let task = rep.new_task(status.into(), description.as_str()?.to_string())?; - // SAFETY: - // - caller promises to free this task - Ok(unsafe { TCTask::from(task).return_ptr() }) - }, - std::ptr::null_mut(), - ) -} - -#[ffizz_header::item] -#[ffizz(order = 902)] -/// Create a new task. The task must not already exist. -/// -/// Returns the task, or NULL on error. -/// -/// ```c -/// EXTERN_C struct TCTask *tc_replica_import_task_with_uuid(struct TCReplica *rep, struct TCUuid tcuuid); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_import_task_with_uuid( - rep: *mut TCReplica, - tcuuid: TCUuid, -) -> *mut TCTask { - wrap( - rep, - |rep| { - // SAFETY: - // - tcuuid is a valid TCUuid (all bytes are valid) - // - tcuuid is Copy so ownership doesn't matter - let uuid = unsafe { TCUuid::val_from_arg(tcuuid) }; - let task = rep.import_task_with_uuid(uuid)?; - // SAFETY: - // - caller promises to free this task - Ok(unsafe { TCTask::from(task).return_ptr() }) - }, - std::ptr::null_mut(), - ) -} - -#[ffizz_header::item] -#[ffizz(order = 902)] -/// Delete a task. The task must exist. Note that this is different from setting status to -/// Deleted; this is the final purge of the task. -/// -/// Deletion may interact poorly with modifications to the same task on other replicas. For -/// example, if a task is deleted on replica 1 and its description modified on replica 2, then -/// after both replicas have fully synced, the resulting task will only have a `description` -/// property. -/// -/// ```c -/// EXTERN_C TCResult tc_replica_delete_task(struct TCReplica *rep, struct TCUuid tcuuid); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_delete_task(rep: *mut TCReplica, tcuuid: TCUuid) -> TCResult { - wrap( - rep, - |rep| { - // SAFETY: - // - tcuuid is a valid TCUuid (all bytes are valid) - // - tcuuid is Copy so ownership doesn't matter - let uuid = unsafe { TCUuid::val_from_arg(tcuuid) }; - rep.delete_task(uuid)?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 902)] -/// Synchronize this replica with a server. -/// -/// The `server` argument remains owned by the caller, and must be freed explicitly. -/// -/// ```c -/// EXTERN_C TCResult tc_replica_sync(struct TCReplica *rep, struct TCServer *server, bool avoid_snapshots); -/// ``` -#[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, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 902)] -/// Expire old, deleted tasks. -/// -/// Expiration entails removal of tasks from the replica. Any modifications that occur after -/// the deletion (such as operations synchronized from other replicas) will do nothing. -/// -/// Tasks are eligible for expiration when they have status Deleted and have not been modified -/// for 180 days (about six months). Note that completed tasks are not eligible. -/// -/// ```c -/// EXTERN_C TCResult tc_replica_expire_tasks(struct TCReplica *rep); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_expire_tasks(rep: *mut TCReplica) -> TCResult { - wrap( - rep, - |rep| { - rep.expire_tasks()?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 902)] -/// Return undo local operations until the most recent UndoPoint. -/// -/// ```c -/// EXTERN_C TCReplicaOpList tc_replica_get_undo_ops(struct TCReplica *rep); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_get_undo_ops(rep: *mut TCReplica) -> TCReplicaOpList { - wrap( - rep, - |rep| { - // SAFETY: - // - caller will free this value, either with tc_replica_commit_undo_ops or - // tc_replica_op_list_free. - Ok(unsafe { - TCReplicaOpList::return_val( - rep.get_undo_ops()? - .into_iter() - .map(TCReplicaOp::from) - .collect(), - ) - }) - }, - TCReplicaOpList::default(), - ) -} - -#[ffizz_header::item] -#[ffizz(order = 902)] -/// Undo local operations in storage. -/// -/// If undone_out is not NULL, then on success it is set to 1 if operations were undone, or 0 if -/// there are no operations that can be done. -/// -/// ```c -/// EXTERN_C TCResult tc_replica_commit_undo_ops(struct TCReplica *rep, TCReplicaOpList tc_undo_ops, int32_t *undone_out); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_commit_undo_ops( - rep: *mut TCReplica, - tc_undo_ops: TCReplicaOpList, - undone_out: *mut i32, -) -> TCResult { - wrap( - rep, - |rep| { - // SAFETY: - // - `tc_undo_ops` is a valid value, as it was acquired from `tc_replica_get_undo_ops`. - let undo_ops: Vec = unsafe { TCReplicaOpList::val_from_arg(tc_undo_ops) } - .into_iter() - .map(|op| *op.inner) - .collect(); - let undone = i32::from(rep.commit_undo_ops(undo_ops)?); - if !undone_out.is_null() { - // SAFETY: - // - undone_out is not NULL (just checked) - // - undone_out is properly aligned (implicitly promised by caller) - unsafe { *undone_out = undone }; - } - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 902)] -/// Get the number of local, un-synchronized operations (not including undo points), or -1 on -/// error. -/// -/// ```c -/// EXTERN_C int64_t tc_replica_num_local_operations(struct TCReplica *rep); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_num_local_operations(rep: *mut TCReplica) -> i64 { - wrap( - rep, - |rep| { - let count = rep.num_local_operations()? as i64; - Ok(count) - }, - -1, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 902)] -/// Get the number of undo points (number of undo calls possible), or -1 on error. -/// -/// ```c -/// EXTERN_C int64_t tc_replica_num_undo_points(struct TCReplica *rep); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_num_undo_points(rep: *mut TCReplica) -> i64 { - wrap( - rep, - |rep| { - let count = rep.num_undo_points()? as i64; - Ok(count) - }, - -1, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 902)] -/// Add an UndoPoint, if one has not already been added by this Replica. This occurs automatically -/// when a change is made. The `force` flag allows forcing a new UndoPoint even if one has already -/// been created by this Replica, and may be useful when a Replica instance is held for a long time -/// and used to apply more than one user-visible change. -/// -/// ```c -/// EXTERN_C TCResult tc_replica_add_undo_point(struct TCReplica *rep, bool force); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_add_undo_point(rep: *mut TCReplica, force: bool) -> TCResult { - wrap( - rep, - |rep| { - rep.add_undo_point(force)?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 902)] -/// Rebuild this replica's working set, based on whether tasks are pending or not. If `renumber` -/// is true, then existing tasks may be moved to new working-set indices; in any case, on -/// completion all pending tasks are in the working set and all non- pending tasks are not. -/// -/// ```c -/// EXTERN_C TCResult tc_replica_rebuild_working_set(struct TCReplica *rep, bool renumber); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_rebuild_working_set( - rep: *mut TCReplica, - renumber: bool, -) -> TCResult { - wrap( - rep, - |rep| { - rep.rebuild_working_set(renumber)?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 902)] -/// Get the latest error for a replica, or a string with NULL ptr if no error exists. Subsequent -/// calls to this function will return NULL. The rep pointer must not be NULL. The caller must -/// free the returned string. -/// -/// ```c -/// EXTERN_C struct TCString tc_replica_error(struct TCReplica *rep); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_error(rep: *mut TCReplica) -> TCString { - // SAFETY: - // - rep is not NULL (promised by caller) - // - *rep is a valid TCReplica (promised by caller) - // - rep is valid for the duration of this function - // - rep is not modified by anything else (not threadsafe) - let rep: &mut TCReplica = unsafe { TCReplica::from_ptr_arg_ref_mut(rep) }; - if let Some(rstring) = rep.error.take() { - // SAFETY: - // - caller promises to free this string - unsafe { TCString::return_val(rstring) } - } else { - TCString::default() - } -} - -#[ffizz_header::item] -#[ffizz(order = 903)] -/// Free a replica. The replica may not be used after this function returns and must not be freed -/// more than once. -/// -/// ```c -/// EXTERN_C void tc_replica_free(struct TCReplica *rep); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_free(rep: *mut TCReplica) { - // SAFETY: - // - replica is not NULL (promised by caller) - // - replica is valid (promised by caller) - // - caller will not use description after this call (promised by caller) - let replica = unsafe { TCReplica::take_from_ptr_arg(rep) }; - if replica.mut_borrowed { - panic!("replica is borrowed and cannot be freed"); - } - drop(replica); -} - -#[ffizz_header::item] -#[ffizz(order = 903)] -/// Free a vector of ReplicaOp. The vector may not be used after this function returns and must not be freed -/// more than once. -/// -/// ```c -/// EXTERN_C void tc_replica_op_list_free(struct TCReplicaOpList *oplist); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_op_list_free(oplist: *mut TCReplicaOpList) { - debug_assert!(!oplist.is_null()); - // SAFETY: - // - arg is not NULL (just checked) - // - `*oplist` is valid (guaranteed by caller not double-freeing this value) - unsafe { - TCReplicaOpList::take_val_from_arg( - oplist, - // SAFETY: - // - value is empty, so the caller need not free it. - TCReplicaOpList::return_val(Vec::new()), - ) - }; -} - -#[ffizz_header::item] -#[ffizz(order = 903)] -/// Return uuid field of ReplicaOp. -/// -/// ```c -/// EXTERN_C struct TCString tc_replica_op_get_uuid(struct TCReplicaOp *op); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_op_get_uuid(op: *const TCReplicaOp) -> TCString { - // SAFETY: - // - inner is not null - // - inner is a living object - let rop: &ReplicaOp = unsafe { (*op).inner.as_ref() }; - - if let ReplicaOp::Create { uuid } - | ReplicaOp::Delete { uuid, .. } - | ReplicaOp::Update { uuid, .. } = rop - { - let uuid_rstr: RustString = uuid.to_string().into(); - // SAFETY: - // - caller promises to free this string - unsafe { TCString::return_val(uuid_rstr) } - } else { - panic!("Operation has no uuid: {:#?}", rop); - } -} - -#[ffizz_header::item] -#[ffizz(order = 903)] -/// Return property field of ReplicaOp. -/// -/// ```c -/// EXTERN_C struct TCString tc_replica_op_get_property(struct TCReplicaOp *op); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_op_get_property(op: *const TCReplicaOp) -> TCString { - // SAFETY: - // - inner is not null - // - inner is a living object - let rop: &ReplicaOp = unsafe { (*op).inner.as_ref() }; - - if let ReplicaOp::Update { property, .. } = rop { - // SAFETY: - // - caller promises to free this string - unsafe { TCString::return_val(property.clone().into()) } - } else { - panic!("Operation has no property: {:#?}", rop); - } -} - -#[ffizz_header::item] -#[ffizz(order = 903)] -/// Return value field of ReplicaOp. -/// -/// ```c -/// EXTERN_C struct TCString tc_replica_op_get_value(struct TCReplicaOp *op); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_op_get_value(op: *const TCReplicaOp) -> TCString { - // SAFETY: - // - inner is not null - // - inner is a living object - let rop: &ReplicaOp = unsafe { (*op).inner.as_ref() }; - - if let ReplicaOp::Update { value, .. } = rop { - let value_rstr: RustString = value.clone().unwrap_or(String::new()).into(); - // SAFETY: - // - caller promises to free this string - unsafe { TCString::return_val(value_rstr) } - } else { - panic!("Operation has no value: {:#?}", rop); - } -} - -#[ffizz_header::item] -#[ffizz(order = 903)] -/// Return old value field of ReplicaOp. -/// -/// ```c -/// EXTERN_C struct TCString tc_replica_op_get_old_value(struct TCReplicaOp *op); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_op_get_old_value(op: *const TCReplicaOp) -> TCString { - // SAFETY: - // - inner is not null - // - inner is a living object - let rop: &ReplicaOp = unsafe { (*op).inner.as_ref() }; - - if let ReplicaOp::Update { old_value, .. } = rop { - let old_value_rstr: RustString = old_value.clone().unwrap_or(String::new()).into(); - // SAFETY: - // - caller promises to free this string - unsafe { TCString::return_val(old_value_rstr) } - } else { - panic!("Operation has no old value: {:#?}", rop); - } -} - -#[ffizz_header::item] -#[ffizz(order = 903)] -/// Return timestamp field of ReplicaOp. -/// -/// ```c -/// EXTERN_C struct TCString tc_replica_op_get_timestamp(struct TCReplicaOp *op); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_op_get_timestamp(op: *const TCReplicaOp) -> TCString { - // SAFETY: - // - inner is not null - // - inner is a living object - let rop: &ReplicaOp = unsafe { (*op).inner.as_ref() }; - - if let ReplicaOp::Update { timestamp, .. } = rop { - let timestamp_rstr: RustString = timestamp.to_string().into(); - // SAFETY: - // - caller promises to free this string - unsafe { TCString::return_val(timestamp_rstr) } - } else { - panic!("Operation has no timestamp: {:#?}", rop); - } -} - -#[ffizz_header::item] -#[ffizz(order = 903)] -/// Return description field of old task field of ReplicaOp. -/// -/// ```c -/// EXTERN_C struct TCString tc_replica_op_get_old_task_description(struct TCReplicaOp *op); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_replica_op_get_old_task_description( - op: *const TCReplicaOp, -) -> TCString { - // SAFETY: - // - inner is not null - // - inner is a living object - let rop: &ReplicaOp = unsafe { (*op).inner.as_ref() }; - - if let ReplicaOp::Delete { old_task, .. } = rop { - let description_rstr: RustString = old_task["description"].clone().into(); - // SAFETY: - // - caller promises to free this string - unsafe { TCString::return_val(description_rstr) } - } else { - panic!("Operation has no timestamp: {:#?}", rop); - } -} diff --git a/src/tc/lib/src/result.rs b/src/tc/lib/src/result.rs deleted file mode 100644 index bb72efb1d..000000000 --- a/src/tc/lib/src/result.rs +++ /dev/null @@ -1,25 +0,0 @@ -#[ffizz_header::item] -#[ffizz(order = 100)] -/// ***** TCResult ***** -/// -/// A result from a TC operation. Typically if this value is TC_RESULT_ERROR, -/// the associated object's `tc_.._error` method will return an error message. -/// -/// ```c -/// enum TCResult -/// #ifdef __cplusplus -/// : int32_t -/// #endif // __cplusplus -/// { -/// TC_RESULT_ERROR = -1, -/// TC_RESULT_OK = 0, -/// }; -/// #ifndef __cplusplus -/// typedef int32_t TCResult; -/// #endif // __cplusplus -/// ``` -#[repr(i32)] -pub enum TCResult { - Error = -1, - Ok = 0, -} diff --git a/src/tc/lib/src/server.rs b/src/tc/lib/src/server.rs deleted file mode 100644 index 4b3ffc3b9..000000000 --- a/src/tc/lib/src/server.rs +++ /dev/null @@ -1,234 +0,0 @@ -use crate::traits::*; -use crate::types::*; -use crate::util::err_to_ruststring; -use taskchampion::{Server, ServerConfig}; - -#[ffizz_header::item] -#[ffizz(order = 800)] -/// ***** TCServer ***** -/// -/// 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. -/// -/// ```c -/// typedef struct TCServer TCServer; -/// ``` -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 TCString, err_value: T) -> T -where - F: FnOnce() -> anyhow::Result, -{ - if !error_out.is_null() { - // SAFETY: - // - error_out is not NULL (just checked) - // - properly aligned and valid (promised by caller) - unsafe { *error_out = TCString::default() }; - } - - match f() { - Ok(v) => v, - Err(e) => { - if !error_out.is_null() { - // SAFETY: - // - error_out is not NULL (just checked) - // - properly aligned and valid (promised by caller) - unsafe { - TCString::val_to_arg_out(err_to_ruststring(e), error_out); - } - } - err_value - } - } -} - -#[ffizz_header::item] -#[ffizz(order = 801)] -/// 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. -/// -/// ```c -/// EXTERN_C struct TCServer *tc_server_new_local(struct TCString server_dir, struct TCString *error_out); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_server_new_local( - server_dir: TCString, - error_out: *mut TCString, -) -> *mut TCServer { - wrap( - || { - // SAFETY: - // - server_dir is valid (promised by caller) - // - caller will not use server_dir after this call (convention) - let mut server_dir = unsafe { TCString::val_from_arg(server_dir) }; - let server_config = ServerConfig::Local { - server_dir: server_dir.to_path_buf_mut()?, - }; - 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(), - ) -} - -#[ffizz_header::item] -#[ffizz(order = 801)] -/// 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. -/// -/// ```c -/// EXTERN_C struct TCServer *tc_server_new_sync(struct TCString url, -/// struct TCUuid client_id, -/// struct TCString encryption_secret, -/// struct TCString *error_out); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_server_new_sync( - url: TCString, - client_id: TCUuid, - encryption_secret: TCString, - error_out: *mut TCString, -) -> *mut TCServer { - wrap( - || { - // SAFETY: - // - url is valid (promised by caller) - // - url ownership is transferred to this function - let url = unsafe { TCString::val_from_arg(url) }.into_string()?; - - // SAFETY: - // - client_id is a valid Uuid (any 8-byte sequence counts) - let client_id = unsafe { TCUuid::val_from_arg(client_id) }; - - // SAFETY: - // - encryption_secret is valid (promised by caller) - // - encryption_secret ownership is transferred to this function - let encryption_secret = unsafe { TCString::val_from_arg(encryption_secret) } - .as_bytes() - .to_vec(); - - let server_config = ServerConfig::Remote { - url, - client_id, - 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(), - ) -} - -#[ffizz_header::item] -#[ffizz(order = 802)] -/// Create a new TCServer that connects to the Google Cloud Platform. 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. -/// -/// ```c -/// EXTERN_C struct TCServer *tc_server_new_gcp(struct TCString bucket, -/// struct TCString credential_path, -/// struct TCString encryption_secret, -/// struct TCString *error_out); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_server_new_gcp( - bucket: TCString, - credential_path_argument: TCString, - encryption_secret: TCString, - error_out: *mut TCString, -) -> *mut TCServer { - wrap( - || { - // SAFETY: - // - bucket is valid (promised by caller) - // - bucket ownership is transferred to this function - let bucket = unsafe { TCString::val_from_arg(bucket) }.into_string()?; - - // SAFETY: - // - credential_path is valid (promised by caller) - // - credential_path ownership is transferred to this function - - let credential_path = - unsafe { TCString::val_from_arg(credential_path_argument) }.into_string()?; - let credential_path = if credential_path.is_empty() { - None - } else { - Some(credential_path) - }; - - // SAFETY: - // - encryption_secret is valid (promised by caller) - // - encryption_secret ownership is transferred to this function - let encryption_secret = unsafe { TCString::val_from_arg(encryption_secret) } - .as_bytes() - .to_vec(); - let server_config = ServerConfig::Gcp { - bucket, - credential_path, - 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(), - ) -} - -#[ffizz_header::item] -#[ffizz(order = 899)] -/// Free a server. The server may not be used after this function returns and must not be freed -/// more than once. -/// -/// ```c -/// EXTERN_C void tc_server_free(struct TCServer *server); -/// ``` -#[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/src/tc/lib/src/status.rs b/src/tc/lib/src/status.rs deleted file mode 100644 index df5332401..000000000 --- a/src/tc/lib/src/status.rs +++ /dev/null @@ -1,73 +0,0 @@ -pub use taskchampion::Status; - -#[ffizz_header::item] -#[ffizz(order = 700)] -/// ***** TCStatus ***** -/// -/// The status of a task, as defined by the task data model. -/// -/// ```c -/// #ifdef __cplusplus -/// typedef enum TCStatus : int32_t { -/// #else // __cplusplus -/// typedef int32_t TCStatus; -/// enum TCStatus { -/// #endif // __cplusplus -/// TC_STATUS_PENDING = 0, -/// TC_STATUS_COMPLETED = 1, -/// TC_STATUS_DELETED = 2, -/// TC_STATUS_RECURRING = 3, -/// // Unknown signifies a status in the task DB that was not -/// // recognized. -/// TC_STATUS_UNKNOWN = -1, -/// #ifdef __cplusplus -/// } TCStatus; -/// #else // __cplusplus -/// }; -/// #endif // __cplusplus -/// ``` -#[repr(i32)] -pub enum TCStatus { - Pending = 0, - Completed = 1, - Deleted = 2, - Recurring = 3, - Unknown = -1, -} - -impl From for Status { - fn from(status: TCStatus) -> Status { - match status { - TCStatus::Pending => Status::Pending, - TCStatus::Completed => Status::Completed, - TCStatus::Deleted => Status::Deleted, - TCStatus::Recurring => Status::Recurring, - _ => Status::Unknown(format!("unknown TCStatus {}", status as i32)), - } - } -} - -impl From for TCStatus { - fn from(status: Status) -> TCStatus { - match status { - Status::Pending => TCStatus::Pending, - Status::Completed => TCStatus::Completed, - Status::Deleted => TCStatus::Deleted, - Status::Recurring => TCStatus::Recurring, - Status::Unknown(_) => TCStatus::Unknown, - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn conversion_from_unknown_tc_status_provides_discriminant_in_message() { - let tc_status = TCStatus::Unknown; - let status = Status::from(tc_status); - - assert!(matches!(status, Status::Unknown(msg) if msg == "unknown TCStatus -1")); - } -} diff --git a/src/tc/lib/src/string.rs b/src/tc/lib/src/string.rs deleted file mode 100644 index 01036c999..000000000 --- a/src/tc/lib/src/string.rs +++ /dev/null @@ -1,773 +0,0 @@ -use crate::traits::*; -use crate::util::{string_into_raw_parts, vec_into_raw_parts}; -use std::ffi::{CStr, CString, OsString}; -use std::os::raw::c_char; -use std::path::PathBuf; - -#[ffizz_header::item] -#[ffizz(order = 200)] -/// ***** TCString ***** -/// -/// TCString supports passing strings into and out of the TaskChampion API. -/// -/// # Rust Strings and C Strings -/// -/// A Rust string can contain embedded NUL characters, while C considers such a character to mark -/// the end of a string. Strings containing embedded NULs cannot be represented as a "C string" -/// and must be accessed using `tc_string_content_and_len` and `tc_string_clone_with_len`. In -/// general, these two functions should be used for handling arbitrary data, while more convenient -/// forms may be used where embedded NUL characters are impossible, such as in static strings. -/// -/// # UTF-8 -/// -/// TaskChampion expects all strings to be valid UTF-8. `tc_string_…` functions will fail if given -/// a `*TCString` containing invalid UTF-8. -/// -/// # Safety -/// -/// The `ptr` field may be checked for NULL, where documentation indicates this is possible. All -/// other fields in a TCString are private and must not be used from C. They exist in the struct -/// to ensure proper allocation and alignment. -/// -/// When a `TCString` appears as a return value or output argument, ownership is passed to the -/// caller. The caller must pass that ownership back to another function or free the string. -/// -/// Any function taking a `TCString` requires: -/// - the pointer must not be NUL; -/// - the pointer must be one previously returned from a tc_… function; and -/// - the memory referenced by the pointer must never be modified by C code. -/// -/// Unless specified otherwise, TaskChampion functions take ownership of a `TCString` when it is -/// given as a function argument, and the caller must not use or free TCStrings after passing them -/// to such API functions. -/// -/// A TCString with a NULL `ptr` field need not be freed, although tc_free_string will not fail -/// for such a value. -/// -/// TCString is not threadsafe. -/// -/// ```c -/// typedef struct TCString { -/// void *ptr; // opaque, but may be checked for NULL -/// size_t _u1; // reserved -/// size_t _u2; // reserved -/// uint8_t _u3; // reserved -/// } TCString; -/// ``` -#[repr(C)] -#[derive(Debug)] -pub struct TCString { - // defined based on the type - ptr: *mut libc::c_void, - len: usize, - cap: usize, - - // type of TCString this represents - ty: u8, -} - -// TODO: figure out how to ignore this but still use it in TCString -/// A discriminator for TCString -#[repr(u8)] -enum TCStringType { - /// Null. Nothing is contained in this string. - /// - /// * `ptr` is NULL. - /// * `len` and `cap` are zero. - Null = 0, - - /// A CString. - /// - /// * `ptr` is the result of CString::into_raw, containing a terminating NUL. It may not be - /// valid UTF-8. - /// * `len` and `cap` are zero. - CString, - - /// A CStr, referencing memory borrowed from C - /// - /// * `ptr` points to the string, containing a terminating NUL. It may not be valid UTF-8. - /// * `len` and `cap` are zero. - CStr, - - /// A String. - /// - /// * `ptr`, `len`, and `cap` are as would be returned from String::into_raw_parts. - String, - - /// A byte sequence. - /// - /// * `ptr`, `len`, and `cap` are as would be returned from Vec::into_raw_parts. - Bytes, -} - -impl Default for TCString { - fn default() -> Self { - TCString { - ptr: std::ptr::null_mut(), - len: 0, - cap: 0, - ty: TCStringType::Null as u8, - } - } -} - -impl TCString { - pub(crate) fn is_null(&self) -> bool { - self.ptr.is_null() - } -} - -#[derive(PartialEq, Eq, Debug, Default)] -pub enum RustString<'a> { - #[default] - Null, - CString(CString), - CStr(&'a CStr), - String(String), - Bytes(Vec), -} - -impl PassByValue for TCString { - type RustType = RustString<'static>; - - unsafe fn from_ctype(self) -> Self::RustType { - match self.ty { - ty if ty == TCStringType::CString as u8 => { - // SAFETY: - // - ptr was derived from CString::into_raw - // - data was not modified since that time (caller promises) - RustString::CString(unsafe { CString::from_raw(self.ptr as *mut c_char) }) - } - ty if ty == TCStringType::CStr as u8 => { - // SAFETY: - // - ptr was created by CStr::as_ptr - // - data was not modified since that time (caller promises) - RustString::CStr(unsafe { CStr::from_ptr(self.ptr as *mut c_char) }) - } - ty if ty == TCStringType::String as u8 => { - // SAFETY: - // - ptr was created by string_into_raw_parts - // - data was not modified since that time (caller promises) - RustString::String(unsafe { - String::from_raw_parts(self.ptr as *mut u8, self.len, self.cap) - }) - } - ty if ty == TCStringType::Bytes as u8 => { - // SAFETY: - // - ptr was created by vec_into_raw_parts - // - data was not modified since that time (caller promises) - RustString::Bytes(unsafe { - Vec::from_raw_parts(self.ptr as *mut u8, self.len, self.cap) - }) - } - _ => RustString::Null, - } - } - - fn as_ctype(arg: Self::RustType) -> Self { - match arg { - RustString::Null => Self { - ty: TCStringType::Null as u8, - ..Default::default() - }, - RustString::CString(cstring) => Self { - ty: TCStringType::CString as u8, - ptr: cstring.into_raw() as *mut libc::c_void, - ..Default::default() - }, - RustString::CStr(cstr) => Self { - ty: TCStringType::CStr as u8, - ptr: cstr.as_ptr() as *mut libc::c_void, - ..Default::default() - }, - RustString::String(string) => { - let (ptr, len, cap) = string_into_raw_parts(string); - Self { - ty: TCStringType::String as u8, - ptr: ptr as *mut libc::c_void, - len, - cap, - } - } - RustString::Bytes(bytes) => { - let (ptr, len, cap) = vec_into_raw_parts(bytes); - Self { - ty: TCStringType::Bytes as u8, - ptr: ptr as *mut libc::c_void, - len, - cap, - } - } - } - } -} - -impl<'a> RustString<'a> { - /// Get a regular Rust &str for this value. - pub(crate) fn as_str(&mut self) -> Result<&str, std::str::Utf8Error> { - match self { - RustString::CString(cstring) => cstring.as_c_str().to_str(), - RustString::CStr(cstr) => cstr.to_str(), - RustString::String(ref string) => Ok(string.as_ref()), - RustString::Bytes(_) => { - self.bytes_to_string()?; - self.as_str() // now the String variant, so won't recurse - } - RustString::Null => unreachable!(), - } - } - - /// Consume this RustString and return an equivalent String, or an error if not - /// valid UTF-8. In the error condition, the original data is lost. - pub(crate) fn into_string(mut self) -> Result { - match self { - RustString::CString(cstring) => cstring.into_string().map_err(|e| e.utf8_error()), - RustString::CStr(cstr) => cstr.to_str().map(|s| s.to_string()), - RustString::String(string) => Ok(string), - RustString::Bytes(_) => { - self.bytes_to_string()?; - self.into_string() // now the String variant, so won't recurse - } - RustString::Null => unreachable!(), - } - } - - pub(crate) fn as_bytes(&self) -> &[u8] { - match self { - RustString::CString(cstring) => cstring.as_bytes(), - RustString::CStr(cstr) => cstr.to_bytes(), - RustString::String(string) => string.as_bytes(), - RustString::Bytes(bytes) => bytes.as_ref(), - RustString::Null => unreachable!(), - } - } - - /// Convert the RustString, in place, from the Bytes to String variant. On successful return, - /// the RustString has variant RustString::String. - fn bytes_to_string(&mut self) -> Result<(), std::str::Utf8Error> { - let mut owned = RustString::Null; - // temporarily swap a Null value into self; we'll swap that back - // shortly. - std::mem::swap(self, &mut owned); - match owned { - RustString::Bytes(bytes) => match String::from_utf8(bytes) { - Ok(string) => { - *self = RustString::String(string); - Ok(()) - } - Err(e) => { - let (e, bytes) = (e.utf8_error(), e.into_bytes()); - // put self back as we found it - *self = RustString::Bytes(bytes); - Err(e) - } - }, - _ => { - // not bytes, so just swap back - std::mem::swap(self, &mut owned); - Ok(()) - } - } - } - - /// Convert the RustString, in place, into one of the C variants. If this is not - /// possible, such as if the string contains an embedded NUL, then the string - /// remains unchanged. - fn string_to_cstring(&mut self) { - let mut owned = RustString::Null; - // temporarily swap a Null value into self; we'll swap that back shortly - std::mem::swap(self, &mut owned); - match owned { - RustString::String(string) => { - match CString::new(string) { - Ok(cstring) => { - *self = RustString::CString(cstring); - } - Err(nul_err) => { - // recover the underlying String from the NulError and restore - // the RustString - let original_bytes = nul_err.into_vec(); - // SAFETY: original_bytes came from a String moments ago, so still valid utf8 - let string = unsafe { String::from_utf8_unchecked(original_bytes) }; - *self = RustString::String(string); - } - } - } - _ => { - // not a CString, so just swap back - std::mem::swap(self, &mut owned); - } - } - } - - pub(crate) fn to_path_buf_mut(&mut self) -> Result { - #[cfg(unix)] - let path: OsString = { - // on UNIX, we can use the bytes directly, without requiring that they - // be valid UTF-8. - use std::ffi::OsStr; - use std::os::unix::ffi::OsStrExt; - OsStr::from_bytes(self.as_bytes()).to_os_string() - }; - #[cfg(windows)] - let path: OsString = { - // on Windows, we assume the filename is valid Unicode, so it can be - // represented as UTF-8. - OsString::from(self.as_str()?.to_string()) - }; - Ok(path.into()) - } -} - -impl<'a> From for RustString<'a> { - fn from(string: String) -> RustString<'a> { - RustString::String(string) - } -} - -impl From<&str> for RustString<'static> { - fn from(string: &str) -> RustString<'static> { - RustString::String(string.to_string()) - } -} - -/// Utility function to borrow a TCString from a pointer arg, modify it, -/// and restore it. -/// -/// This implements a kind of "interior mutability", relying on the -/// single-threaded use of all TC* types. -/// -/// # SAFETY -/// -/// - tcstring must not be NULL -/// - *tcstring must be a valid TCString -/// - *tcstring must not be accessed by anything else, despite the *const -unsafe fn wrap(tcstring: *const TCString, f: F) -> T -where - F: FnOnce(&mut RustString) -> T, -{ - debug_assert!(!tcstring.is_null()); - - // SAFETY: - // - we have exclusive to *tcstring (promised by caller) - let tcstring = tcstring as *mut TCString; - - // SAFETY: - // - tcstring is not NULL - // - *tcstring is a valid string (promised by caller) - let mut rstring = unsafe { TCString::take_val_from_arg(tcstring, TCString::default()) }; - - let rv = f(&mut rstring); - - // update the caller's TCString with the updated RustString - // SAFETY: - // - tcstring is not NULL (we just took from it) - // - tcstring points to valid memory (we just took from it) - unsafe { TCString::val_to_arg_out(rstring, tcstring) }; - - rv -} - -#[ffizz_header::item] -#[ffizz(order = 210)] -/// ***** TCStringList ***** -/// -/// TCStringList represents a list of strings. -/// -/// The content of this struct must be treated as read-only. -/// -/// ```c -/// typedef struct TCStringList { -/// // number of strings in items -/// size_t len; -/// // reserved -/// size_t _u1; -/// // TCStringList representing each string. These remain owned by the TCStringList instance and will -/// // be freed by tc_string_list_free. This pointer is never NULL for a valid TCStringList, and the -/// // *TCStringList at indexes 0..len-1 are not NULL. -/// struct TCString *items; -/// } TCStringList; -/// ``` -#[repr(C)] -pub struct TCStringList { - len: libc::size_t, - /// total size of items (internal use only) - capacity: libc::size_t, - items: *mut TCString, -} - -impl CList for TCStringList { - type Element = TCString; - - unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self { - TCStringList { - len, - capacity: cap, - items, - } - } - - fn slice(&mut self) -> &mut [Self::Element] { - // SAFETY: - // - because we have &mut self, we have read/write access to items[0..len] - // - all items are properly initialized Element's - // - return value lifetime is equal to &mmut self's, so access is exclusive - // - items and len came from Vec, so total size is < isize::MAX - unsafe { std::slice::from_raw_parts_mut(self.items, self.len) } - } - - fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) { - (self.items, self.len, self.capacity) - } -} - -#[ffizz_header::item] -#[ffizz(order = 201)] -/// 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 -/// static string. -/// -/// NOTE: this function does _not_ take responsibility for freeing the given C string. The -/// given string can be freed once the TCString referencing it has been freed. -/// -/// For example: -/// -/// ```text -/// char *url = get_item_url(..); // dynamically allocate C string -/// tc_task_annotate(task, tc_string_borrow(url)); // TCString created, passed, and freed -/// free(url); // string is no longer referenced and can be freed -/// ``` -/// -/// ```c -/// EXTERN_C struct TCString tc_string_borrow(const char *cstr); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_string_borrow(cstr: *const libc::c_char) -> TCString { - debug_assert!(!cstr.is_null()); - // SAFETY: - // - cstr is not NULL (promised by caller, verified by assertion) - // - cstr's lifetime exceeds that of the TCString (promised by caller) - // - cstr contains a valid NUL terminator (promised by caller) - // - cstr's content will not change before it is destroyed (promised by caller) - let cstr: &CStr = unsafe { CStr::from_ptr(cstr) }; - // SAFETY: - // - caller promises to free this string - unsafe { TCString::return_val(RustString::CStr(cstr)) } -} - -#[ffizz_header::item] -#[ffizz(order = 201)] -/// Create a new TCString by cloning the content of the given C string. The resulting TCString -/// is independent of the given string, which can be freed or overwritten immediately. -/// -/// ```c -/// EXTERN_C struct TCString tc_string_clone(const char *cstr); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> TCString { - debug_assert!(!cstr.is_null()); - // SAFETY: - // - cstr is not NULL (promised by caller, verified by assertion) - // - cstr's lifetime exceeds that of this function (by C convention) - // - cstr contains a valid NUL terminator (promised by caller) - // - cstr's content will not change before it is destroyed (by C convention) - let cstr: &CStr = unsafe { CStr::from_ptr(cstr) }; - let cstring: CString = cstr.into(); - // SAFETY: - // - caller promises to free this string - unsafe { TCString::return_val(RustString::CString(cstring)) } -} - -#[ffizz_header::item] -#[ffizz(order = 201)] -/// Create a new TCString containing the given string with the given length. This allows creation -/// of strings containing embedded NUL characters. As with `tc_string_clone`, the resulting -/// TCString is independent of the passed buffer, which may be reused or freed immediately. -/// -/// The length should _not_ include any trailing NUL. -/// -/// The given length must be less than half the maximum value of usize. -/// -/// ```c -/// EXTERN_C struct TCString tc_string_clone_with_len(const char *buf, size_t len); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_string_clone_with_len( - buf: *const libc::c_char, - len: usize, -) -> TCString { - debug_assert!(!buf.is_null()); - debug_assert!(len < isize::MAX as usize); - // SAFETY: - // - buf is valid for len bytes (by C convention) - // - (no alignment requirements for a byte slice) - // - content of buf will not be mutated during the lifetime of this slice (lifetime - // does not outlive this function call) - // - the length of the buffer is less than isize::MAX (promised by caller) - let slice = unsafe { std::slice::from_raw_parts(buf as *const u8, len) }; - - // allocate and copy into Rust-controlled memory - let vec = slice.to_vec(); - - // SAFETY: - // - caller promises to free this string - unsafe { TCString::return_val(RustString::Bytes(vec)) } -} - -#[ffizz_header::item] -#[ffizz(order = 201)] -/// Get the content of the string as a regular C string. The given string must be valid. The -/// returned value is NULL if the string contains NUL bytes or (in some cases) invalid UTF-8. The -/// returned C string is valid until the TCString is freed or passed to another TC API function. -/// -/// In general, prefer [`tc_string_content_with_len`] except when it's certain that the string is -/// valid and NUL-free. -/// -/// This function takes the TCString by pointer because it may be modified in-place to add a NUL -/// terminator. The pointer must not be NULL. -/// -/// This function does _not_ take ownership of the TCString. -/// -/// ```c -/// EXTERN_C const char *tc_string_content(const struct TCString *tcstring); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_string_content(tcstring: *const TCString) -> *const libc::c_char { - // SAFETY; - // - tcstring is not NULL (promised by caller) - // - *tcstring is valid (promised by caller) - // - *tcstring is not accessed concurrently (single-threaded) - unsafe { - wrap(tcstring, |rstring| { - // try to eliminate the Bytes variant. If this fails, we'll return NULL - // below, so the error is ignorable. - let _ = rstring.bytes_to_string(); - - // and eliminate the String variant - rstring.string_to_cstring(); - - match &rstring { - RustString::CString(cstring) => cstring.as_ptr(), - RustString::String(_) => std::ptr::null(), // string_to_cstring failed - RustString::CStr(cstr) => cstr.as_ptr(), - RustString::Bytes(_) => std::ptr::null(), // already returned above - RustString::Null => unreachable!(), - } - }) - } -} - -#[ffizz_header::item] -#[ffizz(order = 201)] -/// Get the content of the string as a pointer and length. The given string must not be NULL. -/// This function can return any string, even one including NUL bytes or invalid UTF-8. The -/// returned buffer is valid until the TCString is freed or passed to another TaskChampio -/// function. -/// -/// This function takes the TCString by pointer because it may be modified in-place to add a NUL -/// terminator. The pointer must not be NULL. -/// -/// This function does _not_ take ownership of the TCString. -/// -/// ```c -/// EXTERN_C const char *tc_string_content_with_len(const struct TCString *tcstring, size_t *len_out); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_string_content_with_len( - tcstring: *const TCString, - len_out: *mut usize, -) -> *const libc::c_char { - // SAFETY; - // - tcstring is not NULL (promised by caller) - // - *tcstring is valid (promised by caller) - // - *tcstring is not accessed concurrently (single-threaded) - unsafe { - wrap(tcstring, |rstring| { - let bytes = rstring.as_bytes(); - - // SAFETY: - // - len_out is not NULL (promised by caller) - // - len_out points to valid memory (promised by caller) - // - len_out is properly aligned (C convention) - usize::val_to_arg_out(bytes.len(), len_out); - bytes.as_ptr() as *const libc::c_char - }) - } -} - -#[ffizz_header::item] -#[ffizz(order = 201)] -/// Free a TCString. The given string must not be NULL. The string must not be used -/// after this function returns, and must not be freed more than once. -/// -/// ```c -/// EXTERN_C void tc_string_free(struct TCString *tcstring); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_string_free(tcstring: *mut TCString) { - // SAFETY: - // - tcstring is not NULL (promised by caller) - // - caller is exclusive owner of tcstring (promised by caller) - drop(unsafe { TCString::take_val_from_arg(tcstring, TCString::default()) }); -} - -#[ffizz_header::item] -#[ffizz(order = 211)] -/// Free a TCStringList instance. The instance, and all TCStringList it contains, must not be used after -/// this call. -/// -/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCStringList. -/// -/// ```c -/// EXTERN_C void tc_string_list_free(struct TCStringList *tcstrings); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_string_list_free(tcstrings: *mut TCStringList) { - // SAFETY: - // - tcstrings is not NULL and points to a valid TCStringList (caller is not allowed to - // modify the list) - // - caller promises not to use the value after return - unsafe { drop_value_list(tcstrings) }; -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn empty_list_has_non_null_pointer() { - let tcstrings = unsafe { TCStringList::return_val(Vec::new()) }; - assert!(!tcstrings.items.is_null()); - assert_eq!(tcstrings.len, 0); - assert_eq!(tcstrings.capacity, 0); - } - - #[test] - fn free_sets_null_pointer() { - let mut tcstrings = unsafe { TCStringList::return_val(Vec::new()) }; - // SAFETY: testing expected behavior - unsafe { tc_string_list_free(&mut tcstrings) }; - assert!(tcstrings.items.is_null()); - assert_eq!(tcstrings.len, 0); - assert_eq!(tcstrings.capacity, 0); - } - - const INVALID_UTF8: &[u8] = b"abc\xf0\x28\x8c\x28"; - - fn make_cstring() -> RustString<'static> { - RustString::CString(CString::new("a string").unwrap()) - } - - fn make_cstr() -> RustString<'static> { - let cstr = CStr::from_bytes_with_nul(b"a string\0").unwrap(); - RustString::CStr(cstr) - } - - fn make_string() -> RustString<'static> { - RustString::String("a string".into()) - } - - fn make_string_with_nul() -> RustString<'static> { - RustString::String("a \0 nul!".into()) - } - - fn make_invalid_bytes() -> RustString<'static> { - RustString::Bytes(INVALID_UTF8.to_vec()) - } - - fn make_bytes() -> RustString<'static> { - RustString::Bytes(b"bytes".to_vec()) - } - - #[test] - fn cstring_as_str() { - assert_eq!(make_cstring().as_str().unwrap(), "a string"); - } - - #[test] - fn cstr_as_str() { - assert_eq!(make_cstr().as_str().unwrap(), "a string"); - } - - #[test] - fn string_as_str() { - assert_eq!(make_string().as_str().unwrap(), "a string"); - } - - #[test] - fn string_with_nul_as_str() { - assert_eq!(make_string_with_nul().as_str().unwrap(), "a \0 nul!"); - } - - #[test] - fn invalid_bytes_as_str() { - let as_str_err = make_invalid_bytes().as_str().unwrap_err(); - assert_eq!(as_str_err.valid_up_to(), 3); // "abc" is valid - } - - #[test] - fn valid_bytes_as_str() { - assert_eq!(make_bytes().as_str().unwrap(), "bytes"); - } - - #[test] - fn cstring_as_bytes() { - assert_eq!(make_cstring().as_bytes(), b"a string"); - } - - #[test] - fn cstr_as_bytes() { - assert_eq!(make_cstr().as_bytes(), b"a string"); - } - - #[test] - fn string_as_bytes() { - assert_eq!(make_string().as_bytes(), b"a string"); - } - - #[test] - fn string_with_nul_as_bytes() { - assert_eq!(make_string_with_nul().as_bytes(), b"a \0 nul!"); - } - - #[test] - fn invalid_bytes_as_bytes() { - assert_eq!(make_invalid_bytes().as_bytes(), INVALID_UTF8); - } - - #[test] - fn cstring_string_to_cstring() { - let mut tcstring = make_cstring(); - tcstring.string_to_cstring(); - assert_eq!(tcstring, make_cstring()); // unchanged - } - - #[test] - fn cstr_string_to_cstring() { - let mut tcstring = make_cstr(); - tcstring.string_to_cstring(); - assert_eq!(tcstring, make_cstr()); // unchanged - } - - #[test] - fn string_string_to_cstring() { - let mut tcstring = make_string(); - tcstring.string_to_cstring(); - assert_eq!(tcstring, make_cstring()); // converted to CString, same content - } - - #[test] - fn string_with_nul_string_to_cstring() { - let mut tcstring = make_string_with_nul(); - tcstring.string_to_cstring(); - assert_eq!(tcstring, make_string_with_nul()); // unchanged - } - - #[test] - fn bytes_string_to_cstring() { - let mut tcstring = make_bytes(); - tcstring.string_to_cstring(); - assert_eq!(tcstring, make_bytes()); // unchanged - } -} diff --git a/src/tc/lib/src/task.rs b/src/tc/lib/src/task.rs deleted file mode 100644 index a5d2e80de..000000000 --- a/src/tc/lib/src/task.rs +++ /dev/null @@ -1,1304 +0,0 @@ -use crate::traits::*; -use crate::types::*; -use crate::util::err_to_ruststring; -use crate::TCKV; -use std::convert::TryFrom; -use std::ops::Deref; -use std::ptr::NonNull; -use std::str::FromStr; -use taskchampion::{utc_timestamp, Annotation, Tag, Task, TaskMut, Uuid}; - -#[ffizz_header::item] -#[ffizz(order = 1000)] -/// ***** TCTask ***** -/// -/// A task, as publicly exposed by this library. -/// -/// A task begins in "immutable" mode. It must be converted to "mutable" mode -/// to make any changes, and doing so requires exclusive access to the replica -/// until the task is freed or converted back to immutable mode. -/// -/// An immutable task carries no reference to the replica that created it, and can be used until it -/// is freed or converted to a TaskMut. A mutable task carries a reference to the replica and -/// must be freed or made immutable before the replica is freed. -/// -/// All `tc_task_..` functions taking a task as an argument require that it not be NULL. -/// -/// When a `tc_task_..` function that returns a TCResult returns TC_RESULT_ERROR, then -/// `tc_task_error` will return the error message. -/// -/// # Safety -/// -/// A task is an owned object, and must be freed with tc_task_free (or, if part of a list, -/// with tc_task_list_free). -/// -/// Any function taking a `*TCTask` requires: -/// - the pointer must not be NUL; -/// - the pointer must be one previously returned from a tc_… function; -/// - the memory referenced by the pointer must never be modified by C code; and -/// - except for `tc_{task,task_list}_free`, ownership of a `*TCTask` remains with the caller. -/// -/// Once passed to tc_task_free, a `*TCTask` becomes invalid and must not be used again. -/// -/// TCTasks are not threadsafe. -/// -/// ```c -/// typedef struct TCTask TCTask; -/// ``` -pub struct TCTask { - /// The wrapped Task or TaskMut - inner: Inner, - - /// The error from the most recent operation, if any - error: Option>, -} - -enum Inner { - /// A regular, immutable task - Immutable(Task), - - /// A mutable task, together with the replica to which it holds an exclusive - /// reference. - Mutable(TaskMut<'static>, *mut TCReplica), - - /// A transitional state for a TCTask as it goes from mutable to immutable and back. A task - /// can only be in this state outside of [`to_mut`] and [`to_immut`] if a panic occurs during - /// one of those methods. - Invalid, -} - -impl PassByPointer for TCTask {} - -impl TCTask { - /// Make an immutable TCTask into a mutable TCTask. Does nothing if the task - /// is already mutable. - /// - /// # Safety - /// - /// The tcreplica pointer must not be NULL, and the replica it points to must not - /// be freed before TCTask.to_immut completes. - unsafe fn to_mut(&mut self, tcreplica: *mut TCReplica) { - self.inner = match std::mem::replace(&mut self.inner, Inner::Invalid) { - Inner::Immutable(task) => { - // SAFETY: - // - tcreplica is not null (promised by caller) - // - tcreplica outlives the pointer in this variant (promised by caller) - let tcreplica_ref: &mut TCReplica = - unsafe { TCReplica::from_ptr_arg_ref_mut(tcreplica) }; - let rep_ref = tcreplica_ref.borrow_mut(); - Inner::Mutable(task.into_mut(rep_ref), tcreplica) - } - Inner::Mutable(task, tcreplica) => Inner::Mutable(task, tcreplica), - Inner::Invalid => unreachable!(), - } - } - - /// Make an mutable TCTask into a immutable TCTask. Does nothing if the task - /// is already immutable. - #[allow(clippy::wrong_self_convention)] // to_immut_mut is not better! - fn to_immut(&mut self) { - self.inner = match std::mem::replace(&mut self.inner, Inner::Invalid) { - Inner::Immutable(task) => Inner::Immutable(task), - Inner::Mutable(task, tcreplica) => { - // SAFETY: - // - tcreplica is not null (promised by caller of to_mut, which created this - // variant) - // - tcreplica is still alive (promised by caller of to_mut) - let tcreplica_ref: &mut TCReplica = - unsafe { TCReplica::from_ptr_arg_ref_mut(tcreplica) }; - tcreplica_ref.release_borrow(); - Inner::Immutable(task.into_immut()) - } - Inner::Invalid => unreachable!(), - } - } -} - -impl From for TCTask { - fn from(task: Task) -> TCTask { - TCTask { - inner: Inner::Immutable(task), - error: None, - } - } -} - -/// Utility function to get a shared reference to the underlying Task. All Task getters -/// are error-free, so this does not handle errors. -fn wrap(task: *mut TCTask, f: F) -> T -where - F: FnOnce(&Task) -> T, -{ - // SAFETY: - // - task is not null (promised by caller) - // - task outlives this function (promised by caller) - let tctask: &mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; - let task: &Task = match &tctask.inner { - Inner::Immutable(t) => t, - Inner::Mutable(t, _) => t.deref(), - Inner::Invalid => unreachable!(), - }; - tctask.error = None; - f(task) -} - -/// Utility function to get a mutable reference to the underlying Task. The -/// TCTask must be mutable. The inner function may use `?` syntax to return an -/// error, which will be represented with the `err_value` returned to C. -fn wrap_mut(task: *mut TCTask, f: F, err_value: T) -> T -where - F: FnOnce(&mut TaskMut) -> anyhow::Result, -{ - // SAFETY: - // - task is not null (promised by caller) - // - task outlives this function (promised by caller) - let tctask: &mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; - let task: &mut TaskMut = match tctask.inner { - Inner::Immutable(_) => panic!("Task is immutable"), - Inner::Mutable(ref mut t, _) => t, - Inner::Invalid => unreachable!(), - }; - tctask.error = None; - match f(task) { - Ok(rv) => rv, - Err(e) => { - tctask.error = Some(err_to_ruststring(e)); - err_value - } - } -} - -impl TryFrom> for Tag { - type Error = anyhow::Error; - - fn try_from(mut rstring: RustString) -> Result { - let tagstr = rstring.as_str()?; - Tag::from_str(tagstr) - } -} - -#[ffizz_header::item] -#[ffizz(order = 1010)] -/// ***** TCTaskList ***** -/// -/// TCTaskList represents a list of tasks. -/// -/// The content of this struct must be treated as read-only: no fields or anything they reference -/// should be modified directly by C code. -/// -/// When an item is taken from this list, its pointer in `items` is set to NULL. -/// -/// ```c -/// typedef struct TCTaskList { -/// // number of tasks in items -/// size_t len; -/// // reserved -/// size_t _u1; -/// // Array of pointers representing each task. These remain owned by the TCTaskList instance and -/// // will be freed by tc_task_list_free. This pointer is never NULL for a valid TCTaskList. -/// // Pointers in the array may be NULL after `tc_task_list_take`. -/// struct TCTask **items; -/// } TCTaskList; -/// ``` -#[repr(C)] -pub struct TCTaskList { - /// number of tasks in items - len: libc::size_t, - - /// total size of items (internal use only) - capacity: libc::size_t, - - /// Array of pointers representing each task. These remain owned by the TCTaskList instance and - /// will be freed by tc_task_list_free. This pointer is never NULL for a valid TCTaskList. - /// Pointers in the array may be NULL after `tc_task_list_take`. - items: *mut Option>, -} - -impl CList for TCTaskList { - type Element = Option>; - - unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self { - TCTaskList { - len, - capacity: cap, - items, - } - } - - fn slice(&mut self) -> &mut [Self::Element] { - // SAFETY: - // - because we have &mut self, we have read/write access to items[0..len] - // - all items are properly initialized Element's - // - return value lifetime is equal to &mmut self's, so access is exclusive - // - items and len came from Vec, so total size is < isize::MAX - unsafe { std::slice::from_raw_parts_mut(self.items, self.len) } - } - - fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) { - (self.items, self.len, self.capacity) - } -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Get a task's UUID. -/// -/// ```c -/// EXTERN_C struct TCUuid tc_task_get_uuid(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_get_uuid(task: *mut TCTask) -> TCUuid { - wrap(task, |task| { - // SAFETY: - // - value is not allocated and need not be freed - unsafe { TCUuid::return_val(task.get_uuid()) } - }) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Get a task's status. -/// -/// ```c -/// EXTERN_C enum TCStatus tc_task_get_status(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_get_status(task: *mut TCTask) -> TCStatus { - wrap(task, |task| task.get_status().into()) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Get the underlying key/value pairs for this task. The returned TCKVList is -/// a "snapshot" of the task and will not be updated if the task is subsequently -/// modified. It is the caller's responsibility to free the TCKVList. -/// -/// ```c -/// EXTERN_C struct TCKVList tc_task_get_taskmap(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_get_taskmap(task: *mut TCTask) -> TCKVList { - wrap(task, |task| { - let vec: Vec = task - .get_taskmap() - .iter() - .map(|(k, v)| { - let key = RustString::from(k.as_ref()); - let value = RustString::from(v.as_ref()); - TCKV::as_ctype((key, value)) - }) - .collect(); - // SAFETY: - // - caller will free this list - unsafe { TCKVList::return_val(vec) } - }) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Get a task's description. -/// -/// ```c -/// EXTERN_C struct TCString tc_task_get_description(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_get_description(task: *mut TCTask) -> TCString { - wrap(task, |task| { - let descr = task.get_description(); - // SAFETY: - // - caller promises to free this string - unsafe { TCString::return_val(descr.into()) } - }) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Get a task property's value, or NULL if the task has no such property, (including if the -/// property name is not valid utf-8). -/// -/// ```c -/// EXTERN_C struct TCString tc_task_get_value(struct TCTask *task, struct TCString property); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_get_value(task: *mut TCTask, property: TCString) -> TCString { - // SAFETY: - // - property is valid (promised by caller) - // - caller will not use property after this call (convention) - let mut property = unsafe { TCString::val_from_arg(property) }; - wrap(task, |task| { - if let Ok(property) = property.as_str() { - let value = task.get_value(property); - if let Some(value) = value { - // SAFETY: - // - caller promises to free this string - return unsafe { TCString::return_val(value.into()) }; - } - } - TCString::default() // null value - }) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Get the entry timestamp for a task (when it was created), or 0 if not set. -/// -/// ```c -/// EXTERN_C time_t tc_task_get_entry(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_get_entry(task: *mut TCTask) -> libc::time_t { - wrap(task, |task| libc::time_t::as_ctype(task.get_entry())) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Get the wait timestamp for a task, or 0 if not set. -/// -/// ```c -/// EXTERN_C time_t tc_task_get_wait(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_get_wait(task: *mut TCTask) -> libc::time_t { - wrap(task, |task| libc::time_t::as_ctype(task.get_wait())) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Get the modified timestamp for a task, or 0 if not set. -/// -/// ```c -/// EXTERN_C time_t tc_task_get_modified(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_get_modified(task: *mut TCTask) -> libc::time_t { - wrap(task, |task| libc::time_t::as_ctype(task.get_modified())) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Check if a task is waiting. -/// -/// ```c -/// EXTERN_C bool tc_task_is_waiting(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_is_waiting(task: *mut TCTask) -> bool { - wrap(task, |task| task.is_waiting()) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Check if a task is active (started and not stopped). -/// -/// ```c -/// EXTERN_C bool tc_task_is_active(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_is_active(task: *mut TCTask) -> bool { - wrap(task, |task| task.is_active()) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Check if a task is blocked (depends on at least one other task). -/// -/// ```c -/// EXTERN_C bool tc_task_is_blocked(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_is_blocked(task: *mut TCTask) -> bool { - wrap(task, |task| task.is_blocked()) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Check if a task is blocking (at least one other task depends on it). -/// -/// ```c -/// EXTERN_C bool tc_task_is_blocking(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_is_blocking(task: *mut TCTask) -> bool { - wrap(task, |task| task.is_blocking()) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Check if a task has the given tag. If the tag is invalid, this function will return false, as -/// that (invalid) tag is not present. No error will be reported via `tc_task_error`. -/// -/// ```c -/// EXTERN_C bool tc_task_has_tag(struct TCTask *task, struct TCString tag); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_has_tag(task: *mut TCTask, tag: TCString) -> bool { - // SAFETY: - // - tag is valid (promised by caller) - // - caller will not use tag after this call (convention) - let tcstring = unsafe { TCString::val_from_arg(tag) }; - wrap(task, |task| { - if let Ok(tag) = Tag::try_from(tcstring) { - task.has_tag(&tag) - } else { - false - } - }) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Get the tags for the task. -/// -/// The caller must free the returned TCStringList instance. The TCStringList instance does not -/// reference the task and the two may be freed in any order. -/// -/// ```c -/// EXTERN_C struct TCStringList tc_task_get_tags(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_get_tags(task: *mut TCTask) -> TCStringList { - wrap(task, |task| { - let vec: Vec = task - .get_tags() - .map(|t| { - // SAFETY: - // - this TCString will be freed via tc_string_list_free. - unsafe { TCString::return_val(t.as_ref().into()) } - }) - .collect(); - // SAFETY: - // - caller will free the list - unsafe { TCStringList::return_val(vec) } - }) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Get the annotations for the task. -/// -/// The caller must free the returned TCAnnotationList instance. The TCStringList instance does not -/// reference the task and the two may be freed in any order. -/// -/// ```c -/// EXTERN_C struct TCAnnotationList tc_task_get_annotations(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_get_annotations(task: *mut TCTask) -> TCAnnotationList { - wrap(task, |task| { - let vec: Vec = task - .get_annotations() - .map(|a| { - let description = RustString::from(a.description); - TCAnnotation::as_ctype((a.entry, description)) - }) - .collect(); - // SAFETY: - // - caller will free the list - unsafe { TCAnnotationList::return_val(vec) } - }) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Get the named UDA from the task. -/// -/// Returns a TCString with NULL ptr field if the UDA does not exist. -/// -/// ```c -/// EXTERN_C struct TCString tc_task_get_uda(struct TCTask *task, struct TCString ns, struct TCString key); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_get_uda( - task: *mut TCTask, - ns: TCString, - key: TCString, -) -> TCString { - wrap(task, |task| { - // SAFETY: - // - ns is valid (promised by caller) - // - caller will not use ns after this call (convention) - if let Ok(ns) = unsafe { TCString::val_from_arg(ns) }.as_str() { - // SAFETY: same - if let Ok(key) = unsafe { TCString::val_from_arg(key) }.as_str() { - if let Some(value) = task.get_uda(ns, key) { - // SAFETY: - // - caller will free this string (caller promises) - return unsafe { TCString::return_val(value.into()) }; - } - } - } - TCString::default() - }) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Get the named legacy UDA from the task. -/// -/// Returns NULL if the UDA does not exist. -/// -/// ```c -/// EXTERN_C struct TCString tc_task_get_legacy_uda(struct TCTask *task, struct TCString key); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_get_legacy_uda(task: *mut TCTask, key: TCString) -> TCString { - wrap(task, |task| { - // SAFETY: - // - key is valid (promised by caller) - // - caller will not use key after this call (convention) - if let Ok(key) = unsafe { TCString::val_from_arg(key) }.as_str() { - if let Some(value) = task.get_legacy_uda(key) { - // SAFETY: - // - caller will free this string (caller promises) - return unsafe { TCString::return_val(value.into()) }; - } - } - TCString::default() - }) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Get all UDAs for this task. -/// -/// Legacy UDAs are represented with an empty string in the ns field. -/// -/// ```c -/// EXTERN_C struct TCUdaList tc_task_get_udas(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_get_udas(task: *mut TCTask) -> TCUdaList { - wrap(task, |task| { - let vec: Vec = task - .get_udas() - .map(|((ns, key), value)| { - // SAFETY: - // - will be freed by tc_uda_list_free - unsafe { - TCUda::return_val(Uda { - ns: Some(ns.into()), - key: key.into(), - value: value.into(), - }) - } - }) - .collect(); - // SAFETY: - // - caller will free this list - unsafe { TCUdaList::return_val(vec) } - }) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Get all UDAs for this task. -/// -/// All TCUdas in this list have a NULL ns field. The entire UDA key is -/// included in the key field. The caller must free the returned list. -/// -/// ```c -/// EXTERN_C struct TCUdaList tc_task_get_legacy_udas(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_get_legacy_udas(task: *mut TCTask) -> TCUdaList { - wrap(task, |task| { - let vec: Vec = task - .get_legacy_udas() - .map(|(key, value)| { - // SAFETY: - // - will be freed by tc_uda_list_free - unsafe { - TCUda::return_val(Uda { - ns: None, - key: key.into(), - value: value.into(), - }) - } - }) - .collect(); - // SAFETY: - // - caller will free this list - unsafe { TCUdaList::return_val(vec) } - }) -} - -#[ffizz_header::item] -#[ffizz(order = 1001)] -/// Get all dependencies for a task. -/// -/// ```c -/// EXTERN_C struct TCUuidList tc_task_get_dependencies(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_get_dependencies(task: *mut TCTask) -> TCUuidList { - wrap(task, |task| { - let vec: Vec = task - .get_dependencies() - .map(|u| { - // SAFETY: - // - value is not allocated - unsafe { TCUuid::return_val(u) } - }) - .collect(); - // SAFETY: - // - caller will free this list - unsafe { TCUuidList::return_val(vec) } - }) -} - -#[ffizz_header::item] -#[ffizz(order = 1002)] -/// Convert an immutable task into a mutable task. -/// -/// The task must not be NULL. It is modified in-place, and becomes mutable. -/// -/// The replica must not be NULL. After this function returns, the replica _cannot be used at all_ -/// until this task is made immutable again. This implies that it is not allowed for more than one -/// task associated with a replica to be mutable at any time. -/// -/// Typically mutation of tasks is bracketed with `tc_task_to_mut` and `tc_task_to_immut`: -/// -/// ```text -/// tc_task_to_mut(task, rep); -/// success = tc_task_done(task); -/// tc_task_to_immut(task, rep); -/// if (!success) { ... } -/// ``` -/// -/// ```c -/// EXTERN_C void tc_task_to_mut(struct TCTask *task, struct TCReplica *tcreplica); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_to_mut(task: *mut TCTask, tcreplica: *mut TCReplica) { - // SAFETY: - // - task is not null (promised by caller) - // - task outlives 'a (promised by caller) - let tctask: &mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; - // SAFETY: - // - tcreplica is not NULL (promised by caller) - // - tcreplica lives until later call to to_immut via tc_task_to_immut (promised by caller, - // who cannot call tc_replica_free during this time) - unsafe { tctask.to_mut(tcreplica) }; -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Set a mutable task's status. -/// -/// ```c -/// EXTERN_C TCResult tc_task_set_status(struct TCTask *task, enum TCStatus status); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_set_status(task: *mut TCTask, status: TCStatus) -> TCResult { - wrap_mut( - task, - |task| { - task.set_status(status.into())?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Set a mutable task's property value by name. If value.ptr is NULL, the property is removed. -/// -/// ```c -/// EXTERN_C TCResult tc_task_set_value(struct TCTask *task, struct TCString property, struct TCString value); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_set_value( - task: *mut TCTask, - property: TCString, - value: TCString, -) -> TCResult { - // SAFETY: - // - property is valid (promised by caller) - // - caller will not use property after this call (convention) - let mut property = unsafe { TCString::val_from_arg(property) }; - let value = if value.is_null() { - None - } else { - // SAFETY: - // - value is valid (promised by caller, after NULL check) - // - caller will not use value after this call (convention) - Some(unsafe { TCString::val_from_arg(value) }) - }; - wrap_mut( - task, - |task| { - let value_str = if let Some(mut v) = value { - Some(v.as_str()?.to_string()) - } else { - None - }; - task.set_value(property.as_str()?.to_string(), value_str)?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Set a mutable task's description. -/// -/// ```c -/// EXTERN_C TCResult tc_task_set_description(struct TCTask *task, struct TCString description); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_set_description( - task: *mut TCTask, - description: TCString, -) -> TCResult { - // SAFETY: - // - description is valid (promised by caller) - // - caller will not use description after this call (convention) - let mut description = unsafe { TCString::val_from_arg(description) }; - wrap_mut( - task, - |task| { - task.set_description(description.as_str()?.to_string())?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Set a mutable task's entry (creation time). Pass entry=0 to unset -/// the entry field. -/// -/// ```c -/// EXTERN_C TCResult tc_task_set_entry(struct TCTask *task, time_t entry); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_set_entry(task: *mut TCTask, entry: libc::time_t) -> TCResult { - wrap_mut( - task, - |task| { - // SAFETY: any time_t value is a valid timestamp - task.set_entry(unsafe { entry.from_ctype() })?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Set a mutable task's wait timestamp. Pass wait=0 to unset the wait field. -/// -/// ```c -/// EXTERN_C TCResult tc_task_set_wait(struct TCTask *task, time_t wait); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_set_wait(task: *mut TCTask, wait: libc::time_t) -> TCResult { - wrap_mut( - task, - |task| { - // SAFETY: any time_t value is a valid timestamp - task.set_wait(unsafe { wait.from_ctype() })?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Set a mutable task's modified timestamp. The value cannot be zero. -/// -/// ```c -/// EXTERN_C TCResult tc_task_set_modified(struct TCTask *task, time_t modified); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_set_modified( - task: *mut TCTask, - modified: libc::time_t, -) -> TCResult { - wrap_mut( - task, - |task| { - task.set_modified( - // SAFETY: any time_t value is a valid timestamp - unsafe { modified.from_ctype() } - .ok_or_else(|| anyhow::anyhow!("modified cannot be zero"))?, - )?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Start a task. -/// -/// ```c -/// EXTERN_C TCResult tc_task_start(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_start(task: *mut TCTask) -> TCResult { - wrap_mut( - task, - |task| { - task.start()?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Stop a task. -/// -/// ```c -/// EXTERN_C TCResult tc_task_stop(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_stop(task: *mut TCTask) -> TCResult { - wrap_mut( - task, - |task| { - task.stop()?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Mark a task as done. -/// -/// ```c -/// EXTERN_C TCResult tc_task_done(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_done(task: *mut TCTask) -> TCResult { - wrap_mut( - task, - |task| { - task.done()?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Mark a task as deleted. -/// -/// ```c -/// EXTERN_C TCResult tc_task_delete(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_delete(task: *mut TCTask) -> TCResult { - wrap_mut( - task, - |task| { - task.delete()?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Add a tag to a mutable task. -/// -/// ```c -/// EXTERN_C TCResult tc_task_add_tag(struct TCTask *task, struct TCString tag); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_add_tag(task: *mut TCTask, tag: TCString) -> TCResult { - // SAFETY: - // - tag is valid (promised by caller) - // - caller will not use tag after this call (convention) - let tcstring = unsafe { TCString::val_from_arg(tag) }; - wrap_mut( - task, - |task| { - let tag = Tag::try_from(tcstring)?; - task.add_tag(&tag)?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Remove a tag from a mutable task. -/// -/// ```c -/// EXTERN_C TCResult tc_task_remove_tag(struct TCTask *task, struct TCString tag); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_remove_tag(task: *mut TCTask, tag: TCString) -> TCResult { - // SAFETY: - // - tag is valid (promised by caller) - // - caller will not use tag after this call (convention) - let tcstring = unsafe { TCString::val_from_arg(tag) }; - wrap_mut( - task, - |task| { - let tag = Tag::try_from(tcstring)?; - task.remove_tag(&tag)?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Add an annotation to a mutable task. This call takes ownership of the -/// passed annotation, which must not be used after the call returns. -/// -/// ```c -/// EXTERN_C TCResult tc_task_add_annotation(struct TCTask *task, struct TCAnnotation *annotation); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_add_annotation( - task: *mut TCTask, - annotation: *mut TCAnnotation, -) -> TCResult { - // SAFETY: - // - annotation is not NULL (promised by caller) - // - annotation is return from a tc_string_.. so is valid - // - caller will not use annotation after this call - let (entry, description) = - unsafe { TCAnnotation::take_val_from_arg(annotation, TCAnnotation::default()) }; - wrap_mut( - task, - |task| { - let description = description.into_string()?; - task.add_annotation(Annotation { entry, description })?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Remove an annotation from a mutable task. -/// -/// ```c -/// EXTERN_C TCResult tc_task_remove_annotation(struct TCTask *task, int64_t entry); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_remove_annotation(task: *mut TCTask, entry: i64) -> TCResult { - wrap_mut( - task, - |task| { - task.remove_annotation(utc_timestamp(entry))?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Set a UDA on a mutable task. -/// -/// ```c -/// EXTERN_C TCResult tc_task_set_uda(struct TCTask *task, -/// struct TCString ns, -/// struct TCString key, -/// struct TCString value); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_set_uda( - task: *mut TCTask, - ns: TCString, - key: TCString, - value: TCString, -) -> TCResult { - // safety: - // - ns is valid (promised by caller) - // - caller will not use ns after this call (convention) - let mut ns = unsafe { TCString::val_from_arg(ns) }; - // SAFETY: same - let mut key = unsafe { TCString::val_from_arg(key) }; - // SAFETY: same - let mut value = unsafe { TCString::val_from_arg(value) }; - wrap_mut( - task, - |task| { - task.set_uda(ns.as_str()?, key.as_str()?, value.as_str()?.to_string())?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Remove a UDA fraom a mutable task. -/// -/// ```c -/// EXTERN_C TCResult tc_task_remove_uda(struct TCTask *task, struct TCString ns, struct TCString key); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_remove_uda( - task: *mut TCTask, - ns: TCString, - key: TCString, -) -> TCResult { - // safety: - // - ns is valid (promised by caller) - // - caller will not use ns after this call (convention) - let mut ns = unsafe { TCString::val_from_arg(ns) }; - // SAFETY: same - let mut key = unsafe { TCString::val_from_arg(key) }; - wrap_mut( - task, - |task| { - task.remove_uda(ns.as_str()?, key.as_str()?)?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Set a legacy UDA on a mutable task. -/// -/// ```c -/// EXTERN_C TCResult tc_task_set_legacy_uda(struct TCTask *task, struct TCString key, struct TCString value); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_set_legacy_uda( - task: *mut TCTask, - key: TCString, - value: TCString, -) -> TCResult { - // safety: - // - key is valid (promised by caller) - // - caller will not use key after this call (convention) - let mut key = unsafe { TCString::val_from_arg(key) }; - // SAFETY: same - let mut value = unsafe { TCString::val_from_arg(value) }; - wrap_mut( - task, - |task| { - task.set_legacy_uda(key.as_str()?.to_string(), value.as_str()?.to_string())?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Remove a UDA fraom a mutable task. -/// -/// ```c -/// EXTERN_C TCResult tc_task_remove_legacy_uda(struct TCTask *task, struct TCString key); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_remove_legacy_uda(task: *mut TCTask, key: TCString) -> TCResult { - // safety: - // - key is valid (promised by caller) - // - caller will not use key after this call (convention) - let mut key = unsafe { TCString::val_from_arg(key) }; - wrap_mut( - task, - |task| { - task.remove_legacy_uda(key.as_str()?.to_string())?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Add a dependency. -/// -/// ```c -/// EXTERN_C TCResult tc_task_add_dependency(struct TCTask *task, struct TCUuid dep); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_add_dependency(task: *mut TCTask, dep: TCUuid) -> TCResult { - // SAFETY: - // - tcuuid is a valid TCUuid (all byte patterns are valid) - let dep: Uuid = unsafe { TCUuid::val_from_arg(dep) }; - wrap_mut( - task, - |task| { - task.add_dependency(dep)?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1003)] -/// Remove a dependency. -/// -/// ```c -/// EXTERN_C TCResult tc_task_remove_dependency(struct TCTask *task, struct TCUuid dep); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_remove_dependency(task: *mut TCTask, dep: TCUuid) -> TCResult { - // SAFETY: - // - tcuuid is a valid TCUuid (all byte patterns are valid) - let dep: Uuid = unsafe { TCUuid::val_from_arg(dep) }; - wrap_mut( - task, - |task| { - task.remove_dependency(dep)?; - Ok(TCResult::Ok) - }, - TCResult::Error, - ) -} - -#[ffizz_header::item] -#[ffizz(order = 1004)] -/// Convert a mutable task into an immutable task. -/// -/// The task must not be NULL. It is modified in-place, and becomes immutable. -/// -/// The replica passed to `tc_task_to_mut` may be used freely after this call. -/// -/// ```c -/// EXTERN_C void tc_task_to_immut(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_to_immut(task: *mut TCTask) { - // SAFETY: - // - task is not null (promised by caller) - // - task outlives 'a (promised by caller) - let tctask: &mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; - tctask.to_immut(); -} - -#[ffizz_header::item] -#[ffizz(order = 1005)] -/// Get the latest error for a task, or a string NULL ptr field if the last operation succeeded. -/// Subsequent calls to this function will return NULL. The task pointer must not be NULL. The -/// caller must free the returned string. -/// -/// ```c -/// EXTERN_C struct TCString tc_task_error(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_error(task: *mut TCTask) -> TCString { - // SAFETY: - // - task is not null (promised by caller) - // - task outlives 'a (promised by caller) - let task: &mut TCTask = unsafe { TCTask::from_ptr_arg_ref_mut(task) }; - if let Some(rstring) = task.error.take() { - // SAFETY: - // - caller promises to free this value - unsafe { TCString::return_val(rstring) } - } else { - TCString::default() - } -} - -#[ffizz_header::item] -#[ffizz(order = 1006)] -/// Free a task. The given task must not be NULL. The task must not be used after this function -/// returns, and must not be freed more than once. -/// -/// If the task is currently mutable, it will first be made immutable. -/// -/// ```c -/// EXTERN_C void tc_task_free(struct TCTask *task); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_free(task: *mut TCTask) { - // SAFETY: - // - task is not NULL (promised by caller) - // - caller will not use the TCTask after this (promised by caller) - let mut tctask = unsafe { TCTask::take_from_ptr_arg(task) }; - - // convert to immut if it was mutable - tctask.to_immut(); - - drop(tctask); -} - -#[ffizz_header::item] -#[ffizz(order = 1011)] -/// Take an item from a TCTaskList. After this call, the indexed item is no longer associated -/// with the list and becomes the caller's responsibility, just as if it had been returned from -/// `tc_replica_get_task`. -/// -/// The corresponding element in the `items` array will be set to NULL. If that field is already -/// NULL (that is, if the item has already been taken), this function will return NULL. If the -/// index is out of bounds, this function will also return NULL. -/// -/// The passed TCTaskList remains owned by the caller. -/// -/// ```c -/// EXTERN_C struct TCTask *tc_task_list_take(struct TCTaskList *tasks, size_t index); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_list_take(tasks: *mut TCTaskList, index: usize) -> *mut TCTask { - // SAFETY: - // - tasks is not NULL and points to a valid TCTaskList (caller is not allowed to - // modify the list directly, and tc_task_list_take leaves the list valid) - let p = unsafe { take_optional_pointer_list_item(tasks, index) }; - if let Some(p) = p { - p.as_ptr() - } else { - std::ptr::null_mut() - } -} - -#[ffizz_header::item] -#[ffizz(order = 1011)] -/// Free a TCTaskList instance. The instance, and all TCTaskList it contains, must not be used after -/// this call. -/// -/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCTaskList. -/// -/// ```c -/// EXTERN_C void tc_task_list_free(struct TCTaskList *tasks); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_task_list_free(tasks: *mut TCTaskList) { - // SAFETY: - // - tasks is not NULL and points to a valid TCTaskList (caller is not allowed to - // modify the list directly, and tc_task_list_take leaves the list valid) - // - caller promises not to use the value after return - unsafe { drop_optional_pointer_list(tasks) }; -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn empty_list_has_non_null_pointer() { - let tasks = unsafe { TCTaskList::return_val(Vec::new()) }; - assert!(!tasks.items.is_null()); - assert_eq!(tasks.len, 0); - assert_eq!(tasks.capacity, 0); - } - - #[test] - fn free_sets_null_pointer() { - let mut tasks = unsafe { TCTaskList::return_val(Vec::new()) }; - // SAFETY: testing expected behavior - unsafe { tc_task_list_free(&mut tasks) }; - assert!(tasks.items.is_null()); - assert_eq!(tasks.len, 0); - assert_eq!(tasks.capacity, 0); - } -} diff --git a/src/tc/lib/src/traits.rs b/src/tc/lib/src/traits.rs deleted file mode 100644 index 0b2eac970..000000000 --- a/src/tc/lib/src/traits.rs +++ /dev/null @@ -1,338 +0,0 @@ -use crate::util::vec_into_raw_parts; -use std::ptr::NonNull; - -/// Support for values passed to Rust by value. These are represented as full structs in C. Such -/// values are implicitly copyable, via C's struct assignment. -/// -/// The Rust and C types may differ, with from_ctype and as_ctype converting between them. -/// Implement this trait for the C type. -/// -/// The RustType must be droppable (not containing raw pointers). -pub(crate) trait PassByValue: Sized { - type RustType; - - /// Convert a C value to a Rust value. - /// - /// # Safety - /// - /// `self` must be a valid CType. - #[allow(clippy::wrong_self_convention)] - unsafe fn from_ctype(self) -> Self::RustType; - - /// Convert a Rust value to a C value. - fn as_ctype(arg: Self::RustType) -> Self; - - /// Take a value from C as an argument. - /// - /// # Safety - /// - /// - `self` must be a valid instance of the C type. This is typically ensured either by - /// requiring that C code not modify it, or by defining the valid values in C comments. - unsafe fn val_from_arg(arg: Self) -> Self::RustType { - // SAFETY: - // - arg is a valid CType (promised by caller) - unsafe { arg.from_ctype() } - } - - /// Take a value from C as a pointer argument, replacing it with the given value. This is used - /// to invalidate the C value as an additional assurance against subsequent use of the value. - /// - /// # Safety - /// - /// - arg must not be NULL - /// - *arg must be a valid, properly aligned instance of the C type - unsafe fn take_val_from_arg(arg: *mut Self, mut replacement: Self) -> Self::RustType { - // SAFETY: - // - arg is valid (promised by caller) - // - replacement is valid and aligned (guaranteed by Rust) - unsafe { std::ptr::swap(arg, &mut replacement) }; - // SAFETY: - // - replacement (formerly *arg) is a valid CType (promised by caller) - unsafe { PassByValue::val_from_arg(replacement) } - } - - /// Return a value to C - /// - /// # Safety - /// - /// - if the value is allocated, the caller must ensure that the value is eventually freed - unsafe fn return_val(arg: Self::RustType) -> Self { - Self::as_ctype(arg) - } - - /// Return a value to C, via an "output parameter" - /// - /// # Safety - /// - /// - `arg_out` must not be NULL and must be properly aligned and pointing to valid memory - /// of the size of CType. - unsafe fn val_to_arg_out(val: Self::RustType, arg_out: *mut Self) { - debug_assert!(!arg_out.is_null()); - // SAFETY: - // - arg_out is not NULL (promised by caller, asserted) - // - arg_out is properly aligned and points to valid memory (promised by caller) - unsafe { *arg_out = Self::as_ctype(val) }; - } -} - -/// Support for values passed to Rust by pointer. These are represented as opaque structs in C, -/// and always handled as pointers. -pub(crate) trait PassByPointer: Sized { - /// Take a value from C as an argument. - /// - /// # Safety - /// - /// - arg must not be NULL - /// - arg must be a value returned from Box::into_raw (via return_ptr) - /// - arg becomes invalid and must not be used after this call - unsafe fn take_from_ptr_arg(arg: *mut Self) -> Self { - debug_assert!(!arg.is_null()); - // SAFETY: see docstring - unsafe { *(Box::from_raw(arg)) } - } - - /// Borrow a value from C as an argument. - /// - /// # Safety - /// - /// - arg must not be NULL - /// - *arg must be a valid instance of Self - /// - arg must be valid for the lifetime assigned by the caller - /// - arg must not be modified by anything else during that lifetime - unsafe fn from_ptr_arg_ref<'a>(arg: *const Self) -> &'a Self { - debug_assert!(!arg.is_null()); - // SAFETY: see docstring - unsafe { &*arg } - } - - /// Mutably borrow a value from C as an argument. - /// - /// # Safety - /// - /// - arg must not be NULL - /// - *arg must be a valid instance of Self - /// - arg must be valid for the lifetime assigned by the caller - /// - arg must not be accessed by anything else during that lifetime - unsafe fn from_ptr_arg_ref_mut<'a>(arg: *mut Self) -> &'a mut Self { - debug_assert!(!arg.is_null()); - // SAFETY: see docstring - unsafe { &mut *arg } - } - - /// Return a value to C, transferring ownership - /// - /// # Safety - /// - /// - the caller must ensure that the value is eventually freed - unsafe fn return_ptr(self) -> *mut Self { - Box::into_raw(Box::new(self)) - } -} - -/// Support for C lists of objects referenced by value. -/// -/// The underlying C type should have three fields, containing items, length, and capacity. The -/// required trait functions just fetch and set these fields. -/// -/// The PassByValue trait will be implemented automatically, converting between the C type and -/// `Vec`. -/// -/// The element type can be PassByValue or PassByPointer. If the latter, it should use either -/// `NonNull` or `Option>` to represent the element. The latter is an "optional -/// pointer list", where elements can be omitted. -/// -/// For most cases, it is only necessary to implement `tc_.._free` that calls one of the -/// drop_..._list functions. -/// -/// # Safety -/// -/// The C type must be documented as read-only. None of the fields may be modified, nor anything -/// accessible via the `items` array. The exception is modification via "taking" elements. -/// -/// This class guarantees that the items pointer is non-NULL for any valid list (even when len=0). -pub(crate) trait CList: Sized { - type Element; - - /// Create a new CList from the given items, len, and capacity. - /// - /// # Safety - /// - /// The arguments must either: - /// - be NULL, 0, and 0, respectively; or - /// - be valid for Vec::from_raw_parts - unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self; - - /// Return a mutable slice representing the elements in this list. - fn slice(&mut self) -> &mut [Self::Element]; - - /// Get the items, len, and capacity (in that order) for this instance. These must be - /// precisely the same values passed tearlier to `from_raw_parts`. - fn into_raw_parts(self) -> (*mut Self::Element, usize, usize); - - /// Generate a NULL value. By default this is a NULL items pointer with zero length and - /// capacity. - fn null_value() -> Self { - // SAFETY: - // - satisfies the first case in from_raw_parts' safety documentation - unsafe { Self::from_raw_parts(std::ptr::null_mut(), 0, 0) } - } -} - -/// Given a CList containing pass-by-value values, drop all of the values and -/// the list. -/// -/// This is a convenience function for `tc_.._list_free` functions. -/// -/// # Safety -/// -/// - List must be non-NULL and point to a valid CL instance -/// - The caller must not use the value array points to after this function, as -/// it has been freed. It will be replaced with the null value. -pub(crate) unsafe fn drop_value_list(list: *mut CL) -where - CL: CList, - T: PassByValue, -{ - debug_assert!(!list.is_null()); - - // SAFETY: - // - *list is a valid CL (promised by caller) - let mut vec = unsafe { CL::take_val_from_arg(list, CL::null_value()) }; - - // first, drop each of the elements in turn - for e in vec.drain(..) { - // SAFETY: - // - e is a valid Element (promised by caller) - // - e is owned - drop(unsafe { PassByValue::val_from_arg(e) }); - } - // then drop the vector - drop(vec); -} - -/// Given a CList containing NonNull pointers, drop all of the pointed-to values and the list. -/// -/// This is a convenience function for `tc_.._list_free` functions. -/// -/// # Safety -/// -/// - List must be non-NULL and point to a valid CL instance -/// - The caller must not use the value array points to after this function, as -/// it has been freed. It will be replaced with the null value. -#[allow(dead_code)] // this was useful once, and might be again? -pub(crate) unsafe fn drop_pointer_list(list: *mut CL) -where - CL: CList>, - T: PassByPointer, -{ - debug_assert!(!list.is_null()); - // SAFETY: - // - *list is a valid CL (promised by caller) - let mut vec = unsafe { CL::take_val_from_arg(list, CL::null_value()) }; - - // first, drop each of the elements in turn - for e in vec.drain(..) { - // SAFETY: - // - e is a valid Element (promised by caller) - // - e is owned - drop(unsafe { PassByPointer::take_from_ptr_arg(e.as_ptr()) }); - } - // then drop the vector - drop(vec); -} - -/// Given a CList containing optional pointers, drop all of the non-null pointed-to values and the -/// list. -/// -/// This is a convenience function for `tc_.._list_free` functions, for lists from which items -/// can be taken. -/// -/// # Safety -/// -/// - List must be non-NULL and point to a valid CL instance -/// - The caller must not use the value array points to after this function, as -/// it has been freed. It will be replaced with the null value. -pub(crate) unsafe fn drop_optional_pointer_list(list: *mut CL) -where - CL: CList>>, - T: PassByPointer, -{ - debug_assert!(!list.is_null()); - // SAFETY: - // - *list is a valid CL (promised by caller) - let mut vec = unsafe { CL::take_val_from_arg(list, CL::null_value()) }; - - // first, drop each of the elements in turn - for e in vec.drain(..).flatten() { - // SAFETY: - // - e is a valid Element (promised by caller) - // - e is owned - drop(unsafe { PassByPointer::take_from_ptr_arg(e.as_ptr()) }); - } - // then drop the vector - drop(vec); -} - -/// Take a value from an optional pointer list, returning the value and replacing its array -/// element with NULL. -/// -/// This is a convenience function for `tc_.._list_take` functions, for lists from which items -/// can be taken. -/// -/// The returned value will be None if the element has already been taken, or if the index is -/// out of bounds. -/// -/// # Safety -/// -/// - List must be non-NULL and point to a valid CL instance -pub(crate) unsafe fn take_optional_pointer_list_item( - list: *mut CL, - index: usize, -) -> Option> -where - CL: CList>>, - T: PassByPointer, -{ - debug_assert!(!list.is_null()); - - // SAFETy: - // - list is properly aligned, dereferencable, and points to an initialized CL, since it is valid - // - the lifetime of the resulting reference is limited to this function, during which time - // nothing else refers to this memory. - let slice = unsafe { list.as_mut() }.unwrap().slice(); - if let Some(elt_ref) = slice.get_mut(index) { - let mut rv = None; - if let Some(elt) = elt_ref.as_mut() { - rv = Some(*elt); - *elt_ref = None; // clear out the array element - } - rv - } else { - None // index out of bounds - } -} - -impl PassByValue for A -where - A: CList, -{ - type RustType = Vec; - - unsafe fn from_ctype(self) -> Self::RustType { - let (items, len, cap) = self.into_raw_parts(); - debug_assert!(!items.is_null()); - // SAFETY: - // - CList::from_raw_parts requires that items, len, and cap be valid for - // Vec::from_raw_parts if not NULL, and they are not NULL (as promised by caller) - // - CList::into_raw_parts returns precisely the values passed to from_raw_parts. - // - those parts are passed to Vec::from_raw_parts here. - unsafe { Vec::from_raw_parts(items as *mut _, len, cap) } - } - - fn as_ctype(arg: Self::RustType) -> Self { - let (items, len, cap) = vec_into_raw_parts(arg); - // SAFETY: - // - satisfies the second case in from_raw_parts' safety documentation - unsafe { Self::from_raw_parts(items, len, cap) } - } -} diff --git a/src/tc/lib/src/uda.rs b/src/tc/lib/src/uda.rs deleted file mode 100644 index 3994bfc82..000000000 --- a/src/tc/lib/src/uda.rs +++ /dev/null @@ -1,188 +0,0 @@ -use crate::traits::*; -use crate::types::*; - -#[ffizz_header::item] -#[ffizz(order = 500)] -/// ***** TCUda ***** -/// -/// TCUda contains the details of a UDA. -/// -/// ```c -/// typedef struct TCUda { -/// // Namespace of the UDA. For legacy UDAs, this may have a NULL ptr field. -/// struct TCString ns; -/// // UDA key. Must not be NULL. -/// struct TCString key; -/// // Content of the UDA. Must not be NULL. -/// struct TCString value; -/// } TCUda; -/// ``` -#[repr(C)] -#[derive(Default)] -pub struct TCUda { - pub ns: TCString, - pub key: TCString, - pub value: TCString, -} - -pub(crate) struct Uda { - pub ns: Option>, - pub key: RustString<'static>, - pub value: RustString<'static>, -} - -impl PassByValue for TCUda { - type RustType = Uda; - - unsafe fn from_ctype(self) -> Self::RustType { - Uda { - ns: if self.ns.is_null() { - None - } else { - // SAFETY: - // - self is owned, so we can take ownership of this TCString - // - self.ns is a valid, non-null TCString (NULL just checked) - Some(unsafe { TCString::val_from_arg(self.ns) }) - }, - // SAFETY: - // - self is owned, so we can take ownership of this TCString - // - self.key is a valid, non-null TCString (see type docstring) - key: unsafe { TCString::val_from_arg(self.key) }, - // SAFETY: - // - self is owned, so we can take ownership of this TCString - // - self.value is a valid, non-null TCString (see type docstring) - value: unsafe { TCString::val_from_arg(self.value) }, - } - } - - fn as_ctype(uda: Uda) -> Self { - TCUda { - // SAFETY: caller assumes ownership of this value - ns: if let Some(ns) = uda.ns { - unsafe { TCString::return_val(ns) } - } else { - TCString::default() - }, - // SAFETY: caller assumes ownership of this value - key: unsafe { TCString::return_val(uda.key) }, - // SAFETY: caller assumes ownership of this value - value: unsafe { TCString::return_val(uda.value) }, - } - } -} - -#[ffizz_header::item] -#[ffizz(order = 510)] -/// ***** TCUdaList ***** -/// -/// TCUdaList represents a list of UDAs. -/// -/// The content of this struct must be treated as read-only. -/// -/// ```c -/// typedef struct TCUdaList { -/// // number of UDAs in items -/// size_t len; -/// // reserved -/// size_t _u1; -/// // Array of UDAs. These remain owned by the TCUdaList instance and will be freed by -/// // tc_uda_list_free. This pointer is never NULL for a valid TCUdaList. -/// struct TCUda *items; -/// } TCUdaList; -/// ``` -#[repr(C)] -pub struct TCUdaList { - /// number of UDAs in items - len: libc::size_t, - - /// total size of items (internal use only) - _capacity: libc::size_t, - - /// Array of UDAs. These remain owned by the TCUdaList instance and will be freed by - /// tc_uda_list_free. This pointer is never NULL for a valid TCUdaList. - items: *mut TCUda, -} - -impl CList for TCUdaList { - type Element = TCUda; - - unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self { - TCUdaList { - len, - _capacity: cap, - items, - } - } - - fn slice(&mut self) -> &mut [Self::Element] { - // SAFETY: - // - because we have &mut self, we have read/write access to items[0..len] - // - all items are properly initialized Element's - // - return value lifetime is equal to &mmut self's, so access is exclusive - // - items and len came from Vec, so total size is < isize::MAX - unsafe { std::slice::from_raw_parts_mut(self.items, self.len) } - } - - fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) { - (self.items, self.len, self._capacity) - } -} - -#[ffizz_header::item] -#[ffizz(order = 501)] -/// Free a TCUda instance. The instance, and the TCStrings it contains, must not be used -/// after this call. -/// -/// ```c -/// EXTERN_C void tc_uda_free(struct TCUda *tcuda); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_uda_free(tcuda: *mut TCUda) { - debug_assert!(!tcuda.is_null()); - // SAFETY: - // - *tcuda is a valid TCUda (caller promises to treat it as read-only) - let uda = unsafe { TCUda::take_val_from_arg(tcuda, TCUda::default()) }; - drop(uda); -} - -#[ffizz_header::item] -#[ffizz(order = 511)] -/// Free a TCUdaList instance. The instance, and all TCUdas it contains, must not be used after -/// this call. -/// -/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCUdaList. -/// -/// ```c -/// EXTERN_C void tc_uda_list_free(struct TCUdaList *tcudas); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_uda_list_free(tcudas: *mut TCUdaList) { - // SAFETY: - // - tcudas is not NULL and points to a valid TCUdaList (caller is not allowed to - // modify the list) - // - caller promises not to use the value after return - unsafe { drop_value_list(tcudas) } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn empty_list_has_non_null_pointer() { - let tcudas = unsafe { TCUdaList::return_val(Vec::new()) }; - assert!(!tcudas.items.is_null()); - assert_eq!(tcudas.len, 0); - assert_eq!(tcudas._capacity, 0); - } - - #[test] - fn free_sets_null_pointer() { - let mut tcudas = unsafe { TCUdaList::return_val(Vec::new()) }; - // SAFETY: testing expected behavior - unsafe { tc_uda_list_free(&mut tcudas) }; - assert!(tcudas.items.is_null()); - assert_eq!(tcudas.len, 0); - assert_eq!(tcudas._capacity, 0); - } -} diff --git a/src/tc/lib/src/util.rs b/src/tc/lib/src/util.rs deleted file mode 100644 index 61223e6bf..000000000 --- a/src/tc/lib/src/util.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::string::RustString; - -pub(crate) fn err_to_ruststring(e: anyhow::Error) -> RustString<'static> { - // The default `to_string` representation of `anyhow::Error` only shows the "outermost" - // context, e.g., "Could not connect to server", and omits the juicy details about what - // actually went wrong. So, join all of those contexts with `: ` for presentation to the C++ - // layer. - let entire_msg = e - .chain() - .skip(1) - .fold(e.to_string(), |a, b| format!("{}: {}", a, b)); - RustString::from(entire_msg) -} - -/// An implementation of Vec::into_raw_parts, which is still unstable. Returns ptr, len, cap. -pub(crate) fn vec_into_raw_parts(vec: Vec) -> (*mut T, usize, usize) { - // emulate Vec::into_raw_parts(): - // - disable dropping the Vec with ManuallyDrop - // - extract ptr, len, and capacity using those methods - let mut vec = std::mem::ManuallyDrop::new(vec); - (vec.as_mut_ptr(), vec.len(), vec.capacity()) -} - -/// An implementation of String::into_raw_parts, which is still unstable. Returns ptr, len, cap. -pub(crate) fn string_into_raw_parts(string: String) -> (*mut u8, usize, usize) { - // emulate String::into_raw_parts(): - // - disable dropping the String with ManuallyDrop - // - extract ptr, len, and capacity using those methods - let mut string = std::mem::ManuallyDrop::new(string); - (string.as_mut_ptr(), string.len(), string.capacity()) -} diff --git a/src/tc/lib/src/uuid.rs b/src/tc/lib/src/uuid.rs deleted file mode 100644 index c979074ce..000000000 --- a/src/tc/lib/src/uuid.rs +++ /dev/null @@ -1,239 +0,0 @@ -use crate::traits::*; -use crate::types::*; -use libc; -use taskchampion::Uuid; - -#[ffizz_header::item] -#[ffizz(order = 300)] -/// ***** TCUuid ***** -/// -/// TCUuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. -/// Uuids are typically treated as opaque, but the bytes are available in big-endian format. -/// -/// ```c -/// typedef struct TCUuid { -/// uint8_t bytes[16]; -/// } TCUuid; -/// ``` -#[repr(C)] -#[derive(Debug, Default)] -pub struct TCUuid([u8; 16]); - -impl PassByValue for TCUuid { - type RustType = Uuid; - - unsafe fn from_ctype(self) -> Self::RustType { - // SAFETY: - // - any 16-byte value is a valid Uuid - Uuid::from_bytes(self.0) - } - - fn as_ctype(arg: Uuid) -> Self { - TCUuid(*arg.as_bytes()) - } -} - -#[ffizz_header::item] -#[ffizz(order = 301)] -/// Length, in bytes, of the string representation of a UUID (without NUL terminator) -/// -/// ```c -/// #define TC_UUID_STRING_BYTES 36 -/// ``` -// TODO: debug_assert or static_assert this somewhere? -pub const TC_UUID_STRING_BYTES: usize = 36; - -#[ffizz_header::item] -#[ffizz(order = 310)] -/// TCUuidList represents a list of uuids. -/// -/// The content of this struct must be treated as read-only. -/// -/// ```c -/// typedef struct TCUuidList { -/// // number of uuids in items -/// size_t len; -/// // reserved -/// size_t _u1; -/// // Array of uuids. This pointer is never NULL for a valid TCUuidList. -/// struct TCUuid *items; -/// } TCUuidList; -/// ``` -#[repr(C)] -pub struct TCUuidList { - /// number of uuids in items - len: libc::size_t, - - /// total size of items (internal use only) - capacity: libc::size_t, - - /// Array of uuids. This pointer is never NULL for a valid TCUuidList. - items: *mut TCUuid, -} - -impl CList for TCUuidList { - type Element = TCUuid; - - unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self { - TCUuidList { - len, - capacity: cap, - items, - } - } - - fn slice(&mut self) -> &mut [Self::Element] { - // SAFETY: - // - because we have &mut self, we have read/write access to items[0..len] - // - all items are properly initialized Element's - // - return value lifetime is equal to &mmut self's, so access is exclusive - // - items and len came from Vec, so total size is < isize::MAX - unsafe { std::slice::from_raw_parts_mut(self.items, self.len) } - } - - fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) { - (self.items, self.len, self.capacity) - } -} - -#[ffizz_header::item] -#[ffizz(order = 302)] -/// Create a new, randomly-generated UUID. -/// -/// ```c -/// EXTERN_C struct TCUuid tc_uuid_new_v4(void); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_uuid_new_v4() -> TCUuid { - // SAFETY: - // - value is not allocated - unsafe { TCUuid::return_val(Uuid::new_v4()) } -} - -#[ffizz_header::item] -#[ffizz(order = 302)] -/// Create a new UUID with the nil value. -/// -/// ```c -/// EXTERN_C struct TCUuid tc_uuid_nil(void); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_uuid_nil() -> TCUuid { - // SAFETY: - // - value is not allocated - unsafe { TCUuid::return_val(Uuid::nil()) } -} - -#[ffizz_header::item] -#[ffizz(order = 302)] -/// Write the string representation of a TCUuid into the given buffer, which must be -/// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. -/// -/// ```c -/// EXTERN_C void tc_uuid_to_buf(struct TCUuid tcuuid, char *buf); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_uuid_to_buf(tcuuid: TCUuid, buf: *mut libc::c_char) { - debug_assert!(!buf.is_null()); - // SAFETY: - // - buf is valid for len bytes (by C convention) - // - (no alignment requirements for a byte slice) - // - content of buf will not be mutated during the lifetime of this slice (lifetime - // does not outlive this function call) - // - the length of the buffer is less than isize::MAX (promised by caller) - let buf: &mut [u8] = - unsafe { std::slice::from_raw_parts_mut(buf as *mut u8, TC_UUID_STRING_BYTES) }; - // SAFETY: - // - tcuuid is a valid TCUuid (all byte patterns are valid) - let uuid: Uuid = unsafe { TCUuid::val_from_arg(tcuuid) }; - uuid.as_hyphenated().encode_lower(buf); -} - -#[ffizz_header::item] -#[ffizz(order = 302)] -/// Return the hyphenated string representation of a TCUuid. The returned string -/// must be freed with tc_string_free. -/// -/// ```c -/// EXTERN_C struct TCString tc_uuid_to_str(struct TCUuid tcuuid); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_uuid_to_str(tcuuid: TCUuid) -> TCString { - // SAFETY: - // - tcuuid is a valid TCUuid (all byte patterns are valid) - let uuid: Uuid = unsafe { TCUuid::val_from_arg(tcuuid) }; - let s = uuid.to_string(); - // SAFETY: - // - caller promises to free this value. - unsafe { TCString::return_val(s.into()) } -} - -#[ffizz_header::item] -#[ffizz(order = 302)] -/// Parse the given string as a UUID. Returns TC_RESULT_ERROR on parse failure or if the given -/// string is not valid. -/// -/// ```c -/// EXTERN_C TCResult tc_uuid_from_str(struct TCString s, struct TCUuid *uuid_out); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_uuid_from_str(s: TCString, uuid_out: *mut TCUuid) -> TCResult { - debug_assert!(!s.is_null()); - debug_assert!(!uuid_out.is_null()); - // SAFETY: - // - s is valid (promised by caller) - // - caller will not use s after this call (convention) - let mut s = unsafe { TCString::val_from_arg(s) }; - if let Ok(s) = s.as_str() { - if let Ok(u) = Uuid::parse_str(s) { - // SAFETY: - // - uuid_out is not NULL (promised by caller) - // - alignment is not required - unsafe { TCUuid::val_to_arg_out(u, uuid_out) }; - return TCResult::Ok; - } - } - TCResult::Error -} - -#[ffizz_header::item] -#[ffizz(order = 312)] -/// Free a TCUuidList instance. The instance, and all TCUuids it contains, must not be used after -/// this call. -/// -/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCUuidList. -/// -/// ```c -/// EXTERN_C void tc_uuid_list_free(struct TCUuidList *tcuuids); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_uuid_list_free(tcuuids: *mut TCUuidList) { - // SAFETY: - // - tcuuids is not NULL and points to a valid TCUuidList (caller is not allowed to - // modify the list) - // - caller promises not to use the value after return - unsafe { drop_value_list(tcuuids) }; -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn empty_list_has_non_null_pointer() { - let tcuuids = unsafe { TCUuidList::return_val(Vec::new()) }; - assert!(!tcuuids.items.is_null()); - assert_eq!(tcuuids.len, 0); - assert_eq!(tcuuids.capacity, 0); - } - - #[test] - fn free_sets_null_pointer() { - let mut tcuuids = unsafe { TCUuidList::return_val(Vec::new()) }; - // SAFETY: testing expected behavior - unsafe { tc_uuid_list_free(&mut tcuuids) }; - assert!(tcuuids.items.is_null()); - assert_eq!(tcuuids.len, 0); - assert_eq!(tcuuids.capacity, 0); - } -} diff --git a/src/tc/lib/src/workingset.rs b/src/tc/lib/src/workingset.rs deleted file mode 100644 index ef9ceaf99..000000000 --- a/src/tc/lib/src/workingset.rs +++ /dev/null @@ -1,141 +0,0 @@ -use crate::traits::*; -use crate::types::*; -use taskchampion::{Uuid, WorkingSet}; - -#[ffizz_header::item] -#[ffizz(order = 1100)] -/// ***** TCWorkingSet ***** -/// -/// A TCWorkingSet represents a snapshot of the working set for a replica. It is not automatically -/// updated based on changes in the replica. Its lifetime is independent of the replica and it can -/// be freed at any time. -/// -/// To iterate over a working set, search indexes 1 through largest_index. -/// -/// # Safety -/// -/// The `*TCWorkingSet` returned from `tc_replica_working_set` is owned by the caller and -/// must later be freed to avoid a memory leak. Its lifetime is independent of the replica -/// from which it was generated. -/// -/// Any function taking a `*TCWorkingSet` requires: -/// - the pointer must not be NUL; -/// - the pointer must be one previously returned from `tc_replica_working_set` -/// - the memory referenced by the pointer must never be accessed by C code; and -/// - except for `tc_replica_free`, ownership of a `*TCWorkingSet` remains with the caller. -/// -/// Once passed to `tc_replica_free`, a `*TCWorkingSet` becomes invalid and must not be used again. -/// -/// TCWorkingSet is not threadsafe. -/// -/// ```c -/// typedef struct TCWorkingSet TCWorkingSet; -/// ``` -pub struct TCWorkingSet(WorkingSet); - -impl PassByPointer for TCWorkingSet {} - -impl From for TCWorkingSet { - fn from(ws: WorkingSet) -> TCWorkingSet { - TCWorkingSet(ws) - } -} - -/// Utility function to get a shared reference to the underlying WorkingSet. -fn wrap(ws: *mut TCWorkingSet, f: F) -> T -where - F: FnOnce(&WorkingSet) -> T, -{ - // SAFETY: - // - ws is not null (promised by caller) - // - ws outlives 'a (promised by caller) - let tcws: &TCWorkingSet = unsafe { TCWorkingSet::from_ptr_arg_ref(ws) }; - f(&tcws.0) -} - -#[ffizz_header::item] -#[ffizz(order = 1101)] -/// Get the working set's length, or the number of UUIDs it contains. -/// -/// ```c -/// EXTERN_C size_t tc_working_set_len(struct TCWorkingSet *ws); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_working_set_len(ws: *mut TCWorkingSet) -> usize { - wrap(ws, |ws| ws.len()) -} - -#[ffizz_header::item] -#[ffizz(order = 1101)] -/// Get the working set's largest index. -/// -/// ```c -/// EXTERN_C size_t tc_working_set_largest_index(struct TCWorkingSet *ws); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_working_set_largest_index(ws: *mut TCWorkingSet) -> usize { - wrap(ws, |ws| ws.largest_index()) -} - -#[ffizz_header::item] -#[ffizz(order = 1101)] -/// Get the UUID for the task at the given index. Returns true if the UUID exists in the working -/// set. If not, returns false and does not change uuid_out. -/// -/// ```c -/// EXTERN_C bool tc_working_set_by_index(struct TCWorkingSet *ws, size_t index, struct TCUuid *uuid_out); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_working_set_by_index( - ws: *mut TCWorkingSet, - index: usize, - uuid_out: *mut TCUuid, -) -> bool { - debug_assert!(!uuid_out.is_null()); - wrap(ws, |ws| { - if let Some(uuid) = ws.by_index(index) { - // SAFETY: - // - uuid_out is not NULL (promised by caller) - // - alignment is not required - unsafe { TCUuid::val_to_arg_out(uuid, uuid_out) }; - true - } else { - false - } - }) -} - -#[ffizz_header::item] -#[ffizz(order = 1101)] -/// Get the working set index for the task with the given UUID. Returns 0 if the task is not in -/// the working set. -/// -/// ```c -/// EXTERN_C size_t tc_working_set_by_uuid(struct TCWorkingSet *ws, struct TCUuid uuid); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_working_set_by_uuid(ws: *mut TCWorkingSet, uuid: TCUuid) -> usize { - wrap(ws, |ws| { - // SAFETY: - // - tcuuid is a valid TCUuid (all byte patterns are valid) - let uuid: Uuid = unsafe { TCUuid::val_from_arg(uuid) }; - ws.by_uuid(uuid).unwrap_or(0) - }) -} - -#[ffizz_header::item] -#[ffizz(order = 1102)] -/// Free a TCWorkingSet. The given value must not be NULL. The value must not be used after this -/// function returns, and must not be freed more than once. -/// -/// ```c -/// EXTERN_C void tc_working_set_free(struct TCWorkingSet *ws); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn tc_working_set_free(ws: *mut TCWorkingSet) { - // SAFETY: - // - rep is not NULL (promised by caller) - // - caller will not use the TCWorkingSet after this (promised by caller) - let ws = unsafe { TCWorkingSet::take_from_ptr_arg(ws) }; - drop(ws); -} diff --git a/src/tc/lib/taskchampion.h b/src/tc/lib/taskchampion.h deleted file mode 100644 index 3d0cf5e9a..000000000 --- a/src/tc/lib/taskchampion.h +++ /dev/null @@ -1,937 +0,0 @@ -// TaskChampion -// -// This file defines the C interface to libtaskchampion. This is a thin wrapper around the Rust -// `taskchampion` crate. Refer to the documentation for that crate at -// https://docs.rs/taskchampion/latest/taskchampion/ for API details. The comments in this file -// focus mostly on the low-level details of passing values to and from TaskChampion. -// -// # Overview -// -// This library defines four major types used to interact with the API, that map directly to Rust -// types. -// -// * TCReplica - see https://docs.rs/taskchampion/latest/taskchampion/struct.Replica.html -// * TCTask - see https://docs.rs/taskchampion/latest/taskchampion/struct.Task.html -// * TCServer - see https://docs.rs/taskchampion/latest/taskchampion/trait.Server.html -// * TCWorkingSet - see https://docs.rs/taskchampion/latest/taskchampion/struct.WorkingSet.html -// -// It also defines a few utility types: -// -// * TCString - a wrapper around both C (NUL-terminated) and Rust (always utf-8) strings. -// * TC…List - a list of objects represented as a C array -// * see below for the remainder -// -// # Safety -// -// Each type contains specific instructions to ensure memory safety. The general rules are as -// follows. -// -// No types in this library are threadsafe. All values should be used in only one thread for their -// entire lifetime. It is safe to use unrelated values in different threads (for example, -// different threads may use different TCReplica values concurrently). -// -// ## Pass by Pointer -// -// Several types such as TCReplica and TCString are "opaque" types and always handled as pointers -// in C. The bytes these pointers address are private to the Rust implementation and must not be -// accessed from C. -// -// Pass-by-pointer values have exactly one owner, and that owner is responsible for freeing the -// value (using a `tc_…_free` function), or transferring ownership elsewhere. Except where -// documented otherwise, when a value is passed to C, ownership passes to C as well. When a value -// is passed to Rust, ownership stays with the C code. The exception is TCString, ownership of -// which passes to Rust when it is used as a function argument. -// -// The limited circumstances where one value must not outlive another, due to pointer references -// between them, are documented below. -// -// ## Pass by Value -// -// Types such as TCUuid and TC…List are passed by value, and contain fields that are accessible -// from C. C code is free to access the content of these types in a _read_only_ fashion. -// -// Pass-by-value values that contain pointers also have exactly one owner, responsible for freeing -// the value or transferring ownership. The tc_…_free functions for these types will replace the -// pointers with NULL to guard against use-after-free errors. The interior pointers in such values -// should never be freed directly (for example, `tc_string_free(tcuda.value)` is an error). -// -// TCUuid is a special case, because it does not contain pointers. It can be freely copied and -// need not be freed. -// -// ## Lists -// -// Lists are a special kind of pass-by-value type. Each contains `len` and `items`, where `items` -// is an array of length `len`. Lists, and the values in the `items` array, must be treated as -// read-only. On return from an API function, a list's ownership is with the C caller, which must -// eventually free the list. List data must be freed with the `tc_…_list_free` function. It is an -// error to free any value in the `items` array of a list. - -#ifndef TASKCHAMPION_H -#define TASKCHAMPION_H - -#include -#include -#include - -#ifdef __cplusplus -#define EXTERN_C extern "C" -#else -#define EXTERN_C -#endif // __cplusplus - -// ***** TCResult ***** -// -// A result from a TC operation. Typically if this value is TC_RESULT_ERROR, -// the associated object's `tc_.._error` method will return an error message. -enum TCResult -#ifdef __cplusplus - : int32_t -#endif // __cplusplus -{ - TC_RESULT_ERROR = -1, - TC_RESULT_OK = 0, -}; -#ifndef __cplusplus -typedef int32_t TCResult; -#endif // __cplusplus - -// ***** TCString ***** -// -// TCString supports passing strings into and out of the TaskChampion API. -// -// # Rust Strings and C Strings -// -// A Rust string can contain embedded NUL characters, while C considers such a character to mark -// the end of a string. Strings containing embedded NULs cannot be represented as a "C string" -// and must be accessed using `tc_string_content_and_len` and `tc_string_clone_with_len`. In -// general, these two functions should be used for handling arbitrary data, while more convenient -// forms may be used where embedded NUL characters are impossible, such as in static strings. -// -// # UTF-8 -// -// TaskChampion expects all strings to be valid UTF-8. `tc_string_…` functions will fail if given -// a `*TCString` containing invalid UTF-8. -// -// # Safety -// -// The `ptr` field may be checked for NULL, where documentation indicates this is possible. All -// other fields in a TCString are private and must not be used from C. They exist in the struct -// to ensure proper allocation and alignment. -// -// When a `TCString` appears as a return value or output argument, ownership is passed to the -// caller. The caller must pass that ownership back to another function or free the string. -// -// Any function taking a `TCString` requires: -// - the pointer must not be NUL; -// - the pointer must be one previously returned from a tc_… function; and -// - the memory referenced by the pointer must never be modified by C code. -// -// Unless specified otherwise, TaskChampion functions take ownership of a `TCString` when it is -// given as a function argument, and the caller must not use or free TCStrings after passing them -// to such API functions. -// -// A TCString with a NULL `ptr` field need not be freed, although tc_free_string will not fail -// for such a value. -// -// TCString is not threadsafe. -typedef struct TCString { - void *ptr; // opaque, but may be checked for NULL - size_t _u1; // reserved - size_t _u2; // reserved - uint8_t _u3; // reserved -} TCString; - -// 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 -// static string. -// -// NOTE: this function does _not_ take responsibility for freeing the given C string. The -// given string can be freed once the TCString referencing it has been freed. -// -// For example: -// -// ```text -// char *url = get_item_url(..); // dynamically allocate C string -// tc_task_annotate(task, tc_string_borrow(url)); // TCString created, passed, and freed -// free(url); // string is no longer referenced and can be freed -// ``` -EXTERN_C struct TCString tc_string_borrow(const char *cstr); - -// Create a new TCString by cloning the content of the given C string. The resulting TCString -// is independent of the given string, which can be freed or overwritten immediately. -EXTERN_C struct TCString tc_string_clone(const char *cstr); - -// Create a new TCString containing the given string with the given length. This allows creation -// of strings containing embedded NUL characters. As with `tc_string_clone`, the resulting -// TCString is independent of the passed buffer, which may be reused or freed immediately. -// -// The length should _not_ include any trailing NUL. -// -// The given length must be less than half the maximum value of usize. -EXTERN_C struct TCString tc_string_clone_with_len(const char *buf, size_t len); - -// Get the content of the string as a regular C string. The given string must be valid. The -// returned value is NULL if the string contains NUL bytes or (in some cases) invalid UTF-8. The -// returned C string is valid until the TCString is freed or passed to another TC API function. -// -// In general, prefer [`tc_string_content_with_len`] except when it's certain that the string is -// valid and NUL-free. -// -// This function takes the TCString by pointer because it may be modified in-place to add a NUL -// terminator. The pointer must not be NULL. -// -// This function does _not_ take ownership of the TCString. -EXTERN_C const char *tc_string_content(const struct TCString *tcstring); - -// Get the content of the string as a pointer and length. The given string must not be NULL. -// This function can return any string, even one including NUL bytes or invalid UTF-8. The -// returned buffer is valid until the TCString is freed or passed to another TaskChampio -// function. -// -// This function takes the TCString by pointer because it may be modified in-place to add a NUL -// terminator. The pointer must not be NULL. -// -// This function does _not_ take ownership of the TCString. -EXTERN_C const char *tc_string_content_with_len(const struct TCString *tcstring, size_t *len_out); - -// Free a TCString. The given string must not be NULL. The string must not be used -// after this function returns, and must not be freed more than once. -EXTERN_C void tc_string_free(struct TCString *tcstring); - -// ***** TCStringList ***** -// -// TCStringList represents a list of strings. -// -// The content of this struct must be treated as read-only. -typedef struct TCStringList { - // number of strings in items - size_t len; - // reserved - size_t _u1; - // TCStringList representing each string. These remain owned by the TCStringList instance and will - // be freed by tc_string_list_free. This pointer is never NULL for a valid TCStringList, and the - // *TCStringList at indexes 0..len-1 are not NULL. - struct TCString *items; -} TCStringList; - -// Free a TCStringList instance. The instance, and all TCStringList it contains, must not be used -// after this call. -// -// When this call returns, the `items` pointer will be NULL, signalling an invalid TCStringList. -EXTERN_C void tc_string_list_free(struct TCStringList *tcstrings); - -// ***** TCUuid ***** -// -// TCUuid is used as a task identifier. Uuids do not contain any pointers and need not be freed. -// Uuids are typically treated as opaque, but the bytes are available in big-endian format. -typedef struct TCUuid { - uint8_t bytes[16]; -} TCUuid; - -// Length, in bytes, of the string representation of a UUID (without NUL terminator) -#define TC_UUID_STRING_BYTES 36 - -// Parse the given string as a UUID. Returns TC_RESULT_ERROR on parse failure or if the given -// string is not valid. -EXTERN_C TCResult tc_uuid_from_str(struct TCString s, struct TCUuid *uuid_out); - -// Create a new, randomly-generated UUID. -EXTERN_C struct TCUuid tc_uuid_new_v4(void); - -// Create a new UUID with the nil value. -EXTERN_C struct TCUuid tc_uuid_nil(void); - -// Write the string representation of a TCUuid into the given buffer, which must be -// at least TC_UUID_STRING_BYTES long. No NUL terminator is added. -EXTERN_C void tc_uuid_to_buf(struct TCUuid tcuuid, char *buf); - -// Return the hyphenated string representation of a TCUuid. The returned string -// must be freed with tc_string_free. -EXTERN_C struct TCString tc_uuid_to_str(struct TCUuid tcuuid); - -// TCUuidList represents a list of uuids. -// -// The content of this struct must be treated as read-only. -typedef struct TCUuidList { - // number of uuids in items - size_t len; - // reserved - size_t _u1; - // Array of uuids. This pointer is never NULL for a valid TCUuidList. - struct TCUuid *items; -} TCUuidList; - -// Free a TCUuidList instance. The instance, and all TCUuids it contains, must not be used after -// this call. -// -// When this call returns, the `items` pointer will be NULL, signalling an invalid TCUuidList. -EXTERN_C void tc_uuid_list_free(struct TCUuidList *tcuuids); - -// ***** TCAnnotation ***** -// -// TCAnnotation contains the details of an annotation. -// -// # Safety -// -// An annotation must be initialized from a tc_.. function, and later freed -// with `tc_annotation_free` or `tc_annotation_list_free`. -// -// Any function taking a `*TCAnnotation` requires: -// - the pointer must not be NUL; -// - the pointer must be one previously returned from a tc_… function; -// - the memory referenced by the pointer must never be modified by C code; and -// - ownership transfers to the called function, and the value must not be used -// after the call returns. In fact, the value will be zeroed out to ensure this. -// -// TCAnnotations are not threadsafe. -typedef struct TCAnnotation { - // Time the annotation was made. Must be nonzero. - time_t entry; - // Content of the annotation. Must not be NULL. - TCString description; -} TCAnnotation; - -// Free a TCAnnotation instance. The instance, and the TCString it contains, must not be used -// after this call. -EXTERN_C void tc_annotation_free(struct TCAnnotation *tcann); - -// ***** TCAnnotationList ***** -// -// TCAnnotationList represents a list of annotations. -// -// The content of this struct must be treated as read-only. -typedef struct TCAnnotationList { - // number of annotations in items - size_t len; - // reserved - size_t _u1; - // Array of annotations. These remain owned by the TCAnnotationList instance and will be freed by - // tc_annotation_list_free. This pointer is never NULL for a valid TCAnnotationList. - struct TCAnnotation *items; -} TCAnnotationList; - -// Free a TCAnnotationList instance. The instance, and all TCAnnotations it contains, must not be -// used after this call. -// -// When this call returns, the `items` pointer will be NULL, signalling an invalid TCAnnotationList. -EXTERN_C void tc_annotation_list_free(struct TCAnnotationList *tcanns); - -// ***** TCUda ***** -// -// TCUda contains the details of a UDA. -typedef struct TCUda { - // Namespace of the UDA. For legacy UDAs, this may have a NULL ptr field. - struct TCString ns; - // UDA key. Must not be NULL. - struct TCString key; - // Content of the UDA. Must not be NULL. - struct TCString value; -} TCUda; - -// Free a TCUda instance. The instance, and the TCStrings it contains, must not be used -// after this call. -EXTERN_C void tc_uda_free(struct TCUda *tcuda); - -// ***** TCUdaList ***** -// -// TCUdaList represents a list of UDAs. -// -// The content of this struct must be treated as read-only. -typedef struct TCUdaList { - // number of UDAs in items - size_t len; - // reserved - size_t _u1; - // Array of UDAs. These remain owned by the TCUdaList instance and will be freed by - // tc_uda_list_free. This pointer is never NULL for a valid TCUdaList. - struct TCUda *items; -} TCUdaList; - -// Free a TCUdaList instance. The instance, and all TCUdas it contains, must not be used after -// this call. -// -// When this call returns, the `items` pointer will be NULL, signalling an invalid TCUdaList. -EXTERN_C void tc_uda_list_free(struct TCUdaList *tcudas); - -// ***** TCKV ***** -// -// TCKV contains a key/value pair that is part of a task. -// -// Neither key nor value are ever NULL. They remain owned by the TCKV and -// will be freed when it is freed with tc_kv_list_free. -typedef struct TCKV { - struct TCString key; - struct TCString value; -} TCKV; - -// ***** TCKVList ***** -// -// TCKVList represents a list of key/value pairs. -// -// The content of this struct must be treated as read-only. -typedef struct TCKVList { - // number of key/value pairs in items - size_t len; - // reserved - size_t _u1; - // Array of TCKV's. These remain owned by the TCKVList instance and will be freed by - // tc_kv_list_free. This pointer is never NULL for a valid TCKVList. - struct TCKV *items; -} TCKVList; - -// Free a TCKVList instance. The instance, and all TCKVs it contains, must not be used after -// this call. -// -// When this call returns, the `items` pointer will be NULL, signalling an invalid TCKVList. -EXTERN_C void tc_kv_list_free(struct TCKVList *tckvs); - -// ***** TCStatus ***** -// -// The status of a task, as defined by the task data model. -#ifdef __cplusplus -typedef enum TCStatus : int32_t { -#else // __cplusplus -typedef int32_t TCStatus; -enum TCStatus { -#endif // __cplusplus - TC_STATUS_PENDING = 0, - TC_STATUS_COMPLETED = 1, - TC_STATUS_DELETED = 2, - TC_STATUS_RECURRING = 3, - // Unknown signifies a status in the task DB that was not - // recognized. - TC_STATUS_UNKNOWN = -1, -#ifdef __cplusplus -} TCStatus; -#else // __cplusplus -}; -#endif // __cplusplus - -// ***** TCServer ***** -// -// 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; - -// 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. -EXTERN_C 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. -EXTERN_C struct TCServer *tc_server_new_sync(struct TCString url, struct TCUuid client_id, - struct TCString encryption_secret, - struct TCString *error_out); - -// Create a new TCServer that connects to the Google Cloud Platform. 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. -EXTERN_C struct TCServer *tc_server_new_gcp(struct TCString bucket, struct TCString credential_path, - 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. -EXTERN_C void tc_server_free(struct TCServer *server); - -// ***** TCReplica ***** -// -// A replica represents an instance of a user's task data, providing an easy interface -// for querying and modifying that data. -// -// # Error Handling -// -// When a `tc_replica_..` function that returns a TCResult returns TC_RESULT_ERROR, then -// `tc_replica_error` will return the error message. -// -// # Safety -// -// The `*TCReplica` returned from `tc_replica_new…` functions is owned by the caller and -// must later be freed to avoid a memory leak. -// -// Any function taking a `*TCReplica` requires: -// - the pointer must not be NUL; -// - the pointer must be one previously returned from a tc_… function; -// - the memory referenced by the pointer must never be modified by C code; and -// - except for `tc_replica_free`, ownership of a `*TCReplica` remains with the caller. -// -// Once passed to `tc_replica_free`, a `*TCReplica` becomes invalid and must not be used again. -// -// TCReplicas are not threadsafe. -typedef struct TCReplica TCReplica; - -// ***** TCReplicaOpType ***** -enum TCReplicaOpType -#ifdef __cplusplus - : uint32_t -#endif // __cplusplus -{ - Create = 0, - Delete = 1, - Update = 2, - UndoPoint = 3, -}; -#ifndef __cplusplus -typedef uint32_t TCReplicaOpType; -#endif // __cplusplus - -// ***** TCReplicaOp ***** -struct TCReplicaOp { - TCReplicaOpType operation_type; - void *inner; -}; - -typedef struct TCReplicaOp TCReplicaOp; - -// ***** TCReplicaOpList ***** -struct TCReplicaOpList { - struct TCReplicaOp *items; - size_t len; - size_t capacity; -}; - -typedef struct TCReplicaOpList TCReplicaOpList; - -// Create a new TCReplica with an in-memory database. The contents of the database will be -// lost when it is freed with tc_replica_free. -EXTERN_C 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. The caller -// must free this string. -EXTERN_C struct TCReplica *tc_replica_new_on_disk(struct TCString path, bool create_if_missing, - struct TCString *error_out); - -// Add an UndoPoint, if one has not already been added by this Replica. This occurs automatically -// when a change is made. The `force` flag allows forcing a new UndoPoint even if one has already -// been created by this Replica, and may be useful when a Replica instance is held for a long time -// and used to apply more than one user-visible change. -EXTERN_C TCResult tc_replica_add_undo_point(struct TCReplica *rep, bool force); - -// Get a list of all uuids for tasks in the replica. -// -// Returns a TCUuidList with a NULL items field on error. -// -// The caller must free the UUID list with `tc_uuid_list_free`. -EXTERN_C struct TCUuidList tc_replica_all_task_uuids(struct TCReplica *rep); - -// Get a list of all tasks in the replica. -// -// Returns a TCTaskList with a NULL items field on error. -EXTERN_C struct TCTaskList tc_replica_all_tasks(struct TCReplica *rep); - -// Undo local operations in storage. -// -// If undone_out is not NULL, then on success it is set to 1 if operations were undone, or 0 if -// there are no operations that can be done. -EXTERN_C TCResult tc_replica_commit_undo_ops(struct TCReplica *rep, TCReplicaOpList tc_undo_ops, - int32_t *undone_out); - -// Delete a task. The task must exist. Note that this is different from setting status to -// Deleted; this is the final purge of the task. -// -// Deletion may interact poorly with modifications to the same task on other replicas. For -// example, if a task is deleted on replica 1 and its description modified on replica 1, then -// after both replicas have fully synced, the resulting task will only have a `description` -// property. -EXTERN_C TCResult tc_replica_delete_task(struct TCReplica *rep, struct TCUuid tcuuid); - -// Get the latest error for a replica, or a string with NULL ptr if no error exists. Subsequent -// calls to this function will return NULL. The rep pointer must not be NULL. The caller must -// free the returned string. -EXTERN_C struct TCString tc_replica_error(struct TCReplica *rep); - -// Expire old, deleted tasks. -// -// Expiration entails removal of tasks from the replica. Any modifications that occur after -// the deletion (such as operations synchronized from other replicas) will do nothing. -// -// Tasks are eligible for expiration when they have status Deleted and have not been modified -// for 180 days (about six months). Note that completed tasks are not eligible. -EXTERN_C TCResult tc_replica_expire_tasks(struct TCReplica *rep); - -// Get an existing task by its UUID. -// -// Returns NULL when the task does not exist, and on error. Consult tc_replica_error -// to distinguish the two conditions. -EXTERN_C struct TCTask *tc_replica_get_task(struct TCReplica *rep, struct TCUuid tcuuid); - -// Return undo local operations until the most recent UndoPoint. -EXTERN_C TCReplicaOpList tc_replica_get_undo_ops(struct TCReplica *rep); - -// Create a new task. The task must not already exist. -// -// Returns the task, or NULL on error. -EXTERN_C struct TCTask *tc_replica_import_task_with_uuid(struct TCReplica *rep, - struct TCUuid tcuuid); - -// Create a new task. The task must not already exist. -// -// Returns the task, or NULL on error. -EXTERN_C struct TCTask *tc_replica_new_task(struct TCReplica *rep, enum TCStatus status, - struct TCString description); - -// Get the number of local, un-synchronized operations (not including undo points), or -1 on -// error. -EXTERN_C int64_t tc_replica_num_local_operations(struct TCReplica *rep); - -// Get the number of undo points (number of undo calls possible), or -1 on error. -EXTERN_C int64_t tc_replica_num_undo_points(struct TCReplica *rep); - -// Rebuild this replica's working set, based on whether tasks are pending or not. If `renumber` -// is true, then existing tasks may be moved to new working-set indices; in any case, on -// completion all pending tasks are in the working set and all non- pending tasks are not. -EXTERN_C TCResult tc_replica_rebuild_working_set(struct TCReplica *rep, bool renumber); - -// Synchronize this replica with a server. -// -// The `server` argument remains owned by the caller, and must be freed explicitly. -EXTERN_C TCResult tc_replica_sync(struct TCReplica *rep, struct TCServer *server, - bool avoid_snapshots); - -// Get the current working set for this replica. The resulting value must be freed -// with tc_working_set_free. -// -// Returns NULL on error. -EXTERN_C struct TCWorkingSet *tc_replica_working_set(struct TCReplica *rep); - -// Free a replica. The replica may not be used after this function returns and must not be freed -// more than once. -EXTERN_C void tc_replica_free(struct TCReplica *rep); - -// Return description field of old task field of ReplicaOp. -EXTERN_C struct TCString tc_replica_op_get_old_task_description(struct TCReplicaOp *op); - -// Return old value field of ReplicaOp. -EXTERN_C struct TCString tc_replica_op_get_old_value(struct TCReplicaOp *op); - -// Return property field of ReplicaOp. -EXTERN_C struct TCString tc_replica_op_get_property(struct TCReplicaOp *op); - -// Return timestamp field of ReplicaOp. -EXTERN_C struct TCString tc_replica_op_get_timestamp(struct TCReplicaOp *op); - -// Return uuid field of ReplicaOp. -EXTERN_C struct TCString tc_replica_op_get_uuid(struct TCReplicaOp *op); - -// Return value field of ReplicaOp. -EXTERN_C struct TCString tc_replica_op_get_value(struct TCReplicaOp *op); - -// Free a vector of ReplicaOp. The vector may not be used after this function returns and must not -// be freed more than once. -EXTERN_C void tc_replica_op_list_free(struct TCReplicaOpList *oplist); - -// ***** TCTask ***** -// -// A task, as publicly exposed by this library. -// -// A task begins in "immutable" mode. It must be converted to "mutable" mode -// to make any changes, and doing so requires exclusive access to the replica -// until the task is freed or converted back to immutable mode. -// -// An immutable task carries no reference to the replica that created it, and can be used until it -// is freed or converted to a TaskMut. A mutable task carries a reference to the replica and -// must be freed or made immutable before the replica is freed. -// -// All `tc_task_..` functions taking a task as an argument require that it not be NULL. -// -// When a `tc_task_..` function that returns a TCResult returns TC_RESULT_ERROR, then -// `tc_task_error` will return the error message. -// -// # Safety -// -// A task is an owned object, and must be freed with tc_task_free (or, if part of a list, -// with tc_task_list_free). -// -// Any function taking a `*TCTask` requires: -// - the pointer must not be NUL; -// - the pointer must be one previously returned from a tc_… function; -// - the memory referenced by the pointer must never be modified by C code; and -// - except for `tc_{task,task_list}_free`, ownership of a `*TCTask` remains with the caller. -// -// Once passed to tc_task_free, a `*TCTask` becomes invalid and must not be used again. -// -// TCTasks are not threadsafe. -typedef struct TCTask TCTask; - -// Get the annotations for the task. -// -// The caller must free the returned TCAnnotationList instance. The TCStringList instance does not -// reference the task and the two may be freed in any order. -EXTERN_C struct TCAnnotationList tc_task_get_annotations(struct TCTask *task); - -// Get all dependencies for a task. -EXTERN_C struct TCUuidList tc_task_get_dependencies(struct TCTask *task); - -// Get a task's description. -EXTERN_C struct TCString tc_task_get_description(struct TCTask *task); - -// Get the entry timestamp for a task (when it was created), or 0 if not set. -EXTERN_C time_t tc_task_get_entry(struct TCTask *task); - -// Get the named legacy UDA from the task. -// -// Returns NULL if the UDA does not exist. -EXTERN_C struct TCString tc_task_get_legacy_uda(struct TCTask *task, struct TCString key); - -// Get all UDAs for this task. -// -// All TCUdas in this list have a NULL ns field. The entire UDA key is -// included in the key field. The caller must free the returned list. -EXTERN_C struct TCUdaList tc_task_get_legacy_udas(struct TCTask *task); - -// Get the modified timestamp for a task, or 0 if not set. -EXTERN_C time_t tc_task_get_modified(struct TCTask *task); - -// Get a task's status. -EXTERN_C enum TCStatus tc_task_get_status(struct TCTask *task); - -// Get the tags for the task. -// -// The caller must free the returned TCStringList instance. The TCStringList instance does not -// reference the task and the two may be freed in any order. -EXTERN_C struct TCStringList tc_task_get_tags(struct TCTask *task); - -// Get the underlying key/value pairs for this task. The returned TCKVList is -// a "snapshot" of the task and will not be updated if the task is subsequently -// modified. It is the caller's responsibility to free the TCKVList. -EXTERN_C struct TCKVList tc_task_get_taskmap(struct TCTask *task); - -// Get the named UDA from the task. -// -// Returns a TCString with NULL ptr field if the UDA does not exist. -EXTERN_C struct TCString tc_task_get_uda(struct TCTask *task, struct TCString ns, - struct TCString key); - -// Get all UDAs for this task. -// -// Legacy UDAs are represented with an empty string in the ns field. -EXTERN_C struct TCUdaList tc_task_get_udas(struct TCTask *task); - -// Get a task's UUID. -EXTERN_C struct TCUuid tc_task_get_uuid(struct TCTask *task); - -// Get a task property's value, or NULL if the task has no such property, (including if the -// property name is not valid utf-8). -EXTERN_C struct TCString tc_task_get_value(struct TCTask *task, struct TCString property); - -// Get the wait timestamp for a task, or 0 if not set. -EXTERN_C time_t tc_task_get_wait(struct TCTask *task); - -// Check if a task has the given tag. If the tag is invalid, this function will return false, as -// that (invalid) tag is not present. No error will be reported via `tc_task_error`. -EXTERN_C bool tc_task_has_tag(struct TCTask *task, struct TCString tag); - -// Check if a task is active (started and not stopped). -EXTERN_C bool tc_task_is_active(struct TCTask *task); - -// Check if a task is blocked (depends on at least one other task). -EXTERN_C bool tc_task_is_blocked(struct TCTask *task); - -// Check if a task is blocking (at least one other task depends on it). -EXTERN_C bool tc_task_is_blocking(struct TCTask *task); - -// Check if a task is waiting. -EXTERN_C bool tc_task_is_waiting(struct TCTask *task); - -// Convert an immutable task into a mutable task. -// -// The task must not be NULL. It is modified in-place, and becomes mutable. -// -// The replica must not be NULL. After this function returns, the replica _cannot be used at all_ -// until this task is made immutable again. This implies that it is not allowed for more than one -// task associated with a replica to be mutable at any time. -// -// Typically mutation of tasks is bracketed with `tc_task_to_mut` and `tc_task_to_immut`: -// -// ```text -// tc_task_to_mut(task, rep); -// success = tc_task_done(task); -// tc_task_to_immut(task, rep); -// if (!success) { ... } -// ``` -EXTERN_C void tc_task_to_mut(struct TCTask *task, struct TCReplica *tcreplica); - -// Add an annotation to a mutable task. This call takes ownership of the -// passed annotation, which must not be used after the call returns. -EXTERN_C TCResult tc_task_add_annotation(struct TCTask *task, struct TCAnnotation *annotation); - -// Add a dependency. -EXTERN_C TCResult tc_task_add_dependency(struct TCTask *task, struct TCUuid dep); - -// Add a tag to a mutable task. -EXTERN_C TCResult tc_task_add_tag(struct TCTask *task, struct TCString tag); - -// Mark a task as deleted. -EXTERN_C TCResult tc_task_delete(struct TCTask *task); - -// Mark a task as done. -EXTERN_C TCResult tc_task_done(struct TCTask *task); - -// Remove an annotation from a mutable task. -EXTERN_C TCResult tc_task_remove_annotation(struct TCTask *task, int64_t entry); - -// Remove a dependency. -EXTERN_C TCResult tc_task_remove_dependency(struct TCTask *task, struct TCUuid dep); - -// Remove a UDA fraom a mutable task. -EXTERN_C TCResult tc_task_remove_legacy_uda(struct TCTask *task, struct TCString key); - -// Remove a tag from a mutable task. -EXTERN_C TCResult tc_task_remove_tag(struct TCTask *task, struct TCString tag); - -// Remove a UDA fraom a mutable task. -EXTERN_C TCResult tc_task_remove_uda(struct TCTask *task, struct TCString ns, struct TCString key); - -// Set a mutable task's description. -EXTERN_C TCResult tc_task_set_description(struct TCTask *task, struct TCString description); - -// Set a mutable task's entry (creation time). Pass entry=0 to unset -// the entry field. -EXTERN_C TCResult tc_task_set_entry(struct TCTask *task, time_t entry); - -// Set a legacy UDA on a mutable task. -EXTERN_C TCResult tc_task_set_legacy_uda(struct TCTask *task, struct TCString key, - struct TCString value); - -// Set a mutable task's modified timestamp. The value cannot be zero. -EXTERN_C TCResult tc_task_set_modified(struct TCTask *task, time_t modified); - -// Set a mutable task's status. -EXTERN_C TCResult tc_task_set_status(struct TCTask *task, enum TCStatus status); - -// Set a UDA on a mutable task. -EXTERN_C TCResult tc_task_set_uda(struct TCTask *task, struct TCString ns, struct TCString key, - struct TCString value); - -// Set a mutable task's property value by name. If value.ptr is NULL, the property is removed. -EXTERN_C TCResult tc_task_set_value(struct TCTask *task, struct TCString property, - struct TCString value); - -// Set a mutable task's wait timestamp. Pass wait=0 to unset the wait field. -EXTERN_C TCResult tc_task_set_wait(struct TCTask *task, time_t wait); - -// Start a task. -EXTERN_C TCResult tc_task_start(struct TCTask *task); - -// Stop a task. -EXTERN_C TCResult tc_task_stop(struct TCTask *task); - -// Convert a mutable task into an immutable task. -// -// The task must not be NULL. It is modified in-place, and becomes immutable. -// -// The replica passed to `tc_task_to_mut` may be used freely after this call. -EXTERN_C void tc_task_to_immut(struct TCTask *task); - -// Get the latest error for a task, or a string NULL ptr field if the last operation succeeded. -// Subsequent calls to this function will return NULL. The task pointer must not be NULL. The -// caller must free the returned string. -EXTERN_C struct TCString tc_task_error(struct TCTask *task); - -// Free a task. The given task must not be NULL. The task must not be used after this function -// returns, and must not be freed more than once. -// -// If the task is currently mutable, it will first be made immutable. -EXTERN_C void tc_task_free(struct TCTask *task); - -// ***** TCTaskList ***** -// -// TCTaskList represents a list of tasks. -// -// The content of this struct must be treated as read-only: no fields or anything they reference -// should be modified directly by C code. -// -// When an item is taken from this list, its pointer in `items` is set to NULL. -typedef struct TCTaskList { - // number of tasks in items - size_t len; - // reserved - size_t _u1; - // Array of pointers representing each task. These remain owned by the TCTaskList instance and - // will be freed by tc_task_list_free. This pointer is never NULL for a valid TCTaskList. - // Pointers in the array may be NULL after `tc_task_list_take`. - struct TCTask **items; -} TCTaskList; - -// Free a TCTaskList instance. The instance, and all TCTaskList it contains, must not be used after -// this call. -// -// When this call returns, the `items` pointer will be NULL, signalling an invalid TCTaskList. -EXTERN_C void tc_task_list_free(struct TCTaskList *tasks); - -// Take an item from a TCTaskList. After this call, the indexed item is no longer associated -// with the list and becomes the caller's responsibility, just as if it had been returned from -// `tc_replica_get_task`. -// -// The corresponding element in the `items` array will be set to NULL. If that field is already -// NULL (that is, if the item has already been taken), this function will return NULL. If the -// index is out of bounds, this function will also return NULL. -// -// The passed TCTaskList remains owned by the caller. -EXTERN_C struct TCTask *tc_task_list_take(struct TCTaskList *tasks, size_t index); - -// ***** TCWorkingSet ***** -// -// A TCWorkingSet represents a snapshot of the working set for a replica. It is not automatically -// updated based on changes in the replica. Its lifetime is independent of the replica and it can -// be freed at any time. -// -// To iterate over a working set, search indexes 1 through largest_index. -// -// # Safety -// -// The `*TCWorkingSet` returned from `tc_replica_working_set` is owned by the caller and -// must later be freed to avoid a memory leak. Its lifetime is independent of the replica -// from which it was generated. -// -// Any function taking a `*TCWorkingSet` requires: -// - the pointer must not be NUL; -// - the pointer must be one previously returned from `tc_replica_working_set` -// - the memory referenced by the pointer must never be accessed by C code; and -// - except for `tc_replica_free`, ownership of a `*TCWorkingSet` remains with the caller. -// -// Once passed to `tc_replica_free`, a `*TCWorkingSet` becomes invalid and must not be used again. -// -// TCWorkingSet is not threadsafe. -typedef struct TCWorkingSet TCWorkingSet; - -// Get the UUID for the task at the given index. Returns true if the UUID exists in the working -// set. If not, returns false and does not change uuid_out. -EXTERN_C bool tc_working_set_by_index(struct TCWorkingSet *ws, size_t index, - struct TCUuid *uuid_out); - -// Get the working set index for the task with the given UUID. Returns 0 if the task is not in -// the working set. -EXTERN_C size_t tc_working_set_by_uuid(struct TCWorkingSet *ws, struct TCUuid uuid); - -// Get the working set's largest index. -EXTERN_C size_t tc_working_set_largest_index(struct TCWorkingSet *ws); - -// Get the working set's length, or the number of UUIDs it contains. -EXTERN_C size_t tc_working_set_len(struct TCWorkingSet *ws); - -// Free a TCWorkingSet. The given value must not be NULL. The value must not be used after this -// function returns, and must not be freed more than once. -EXTERN_C void tc_working_set_free(struct TCWorkingSet *ws); - -#endif /* TASKCHAMPION_H */ diff --git a/src/tc/util.cpp b/src/tc/util.cpp deleted file mode 100644 index 3339615ce..000000000 --- a/src/tc/util.cpp +++ /dev/null @@ -1,79 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// -// Copyright 2022, Dustin J. Mitchell -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// https://www.opensource.org/licenses/mit-license.php -// -//////////////////////////////////////////////////////////////////////////////// - -#include -// cmake.h include header must come first - -#include -#include - -#include "tc/Replica.h" -#include "tc/Task.h" - -using namespace tc::ffi; - -namespace tc { -//////////////////////////////////////////////////////////////////////////////// -TCString string2tc(const std::string& str) { - return tc_string_clone_with_len(str.data(), str.size()); -} - -//////////////////////////////////////////////////////////////////////////////// -std::string tc2string_clone(const TCString& str) { - size_t len; - auto ptr = tc_string_content_with_len(&str, &len); - auto rv = std::string(ptr, len); - return rv; -} - -//////////////////////////////////////////////////////////////////////////////// -std::string tc2string(TCString& str) { - auto rv = tc2string_clone(str); - tc_string_free(&str); - return rv; -} - -//////////////////////////////////////////////////////////////////////////////// -TCUuid uuid2tc(const std::string& str) { - TCString tcstr = tc_string_borrow(str.c_str()); - TCUuid rv; - if (TC_RESULT_OK != tc_uuid_from_str(tcstr, &rv)) { - throw std::string("invalid UUID"); - } - return rv; -} - -//////////////////////////////////////////////////////////////////////////////// -std::string tc2uuid(TCUuid& uuid) { - char s[TC_UUID_STRING_BYTES]; - tc_uuid_to_buf(uuid, s); - std::string str; - str.assign(s, TC_UUID_STRING_BYTES); - return str; -} - -//////////////////////////////////////////////////////////////////////////////// -} // namespace tc diff --git a/src/tc/util.h b/src/tc/util.h deleted file mode 100644 index 87d54cd16..000000000 --- a/src/tc/util.h +++ /dev/null @@ -1,52 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// -// Copyright 2022, Dustin J. Mitchell -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// https://www.opensource.org/licenses/mit-license.php -// -//////////////////////////////////////////////////////////////////////////////// - -#ifndef INCLUDED_TC_UTIL -#define INCLUDED_TC_UTIL - -#include - -#include "tc/ffi.h" - -namespace tc { -// convert a std::string into a TCString, copying the contained data -tc::ffi::TCString string2tc(const std::string&); - -// convert a TCString into a std::string, leaving the TCString as-is -std::string tc2string_clone(const tc::ffi::TCString&); - -// convert a TCString into a std::string, freeing the TCString -std::string tc2string(tc::ffi::TCString&); - -// convert a TCUuid into a std::string -std::string tc2uuid(tc::ffi::TCUuid&); - -// parse a std::string into a TCUuid (throwing if parse fails) -tc::ffi::TCUuid uuid2tc(const std::string&); -} // namespace tc - -#endif -//////////////////////////////////////////////////////////////////////////////// diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 5af30aba0..d8de86d9f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -2,7 +2,6 @@ cmake_minimum_required (VERSION 3.22) include_directories (${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/src - ${CMAKE_SOURCE_DIR}/src/tc/lib ${CMAKE_SOURCE_DIR}/src/commands ${CMAKE_SOURCE_DIR}/src/columns ${CMAKE_SOURCE_DIR}/src/libshared/src @@ -48,7 +47,7 @@ add_custom_target (build_tests DEPENDS ${test_SRCS} foreach (src_FILE ${test_SRCS}) add_executable (${src_FILE} ${src_FILE} test.cpp) - target_link_libraries (${src_FILE} task tc commands columns libshared task tc commands columns libshared task commands columns libshared ${TASK_LIBRARIES}) + target_link_libraries (${src_FILE} task commands columns libshared task commands columns libshared task commands columns libshared ${TASK_LIBRARIES}) add_dependencies (${src_FILE} task_executable) if (DARWIN) target_link_libraries (${src_FILE} "-framework CoreFoundation -framework Security -framework SystemConfiguration") diff --git a/test/feature.559.test.py b/test/feature.559.test.py index c0c5afa8f..f637e104e 100755 --- a/test/feature.559.test.py +++ b/test/feature.559.test.py @@ -69,7 +69,11 @@ class TestFeature559(TestCase): self.assertNotIn("footask", out) self.assertNotIn("Error", out) self.assertRegex( - err, re.compile("Could not.+unable to open database file", re.DOTALL) + err, + re.compile( + "unable to open database file:.*Unable to open the database file", + re.DOTALL, + ), ) diff --git a/test/tc.test.cpp b/test/tc.test.cpp index e3351e101..747c8e628 100644 --- a/test/tc.test.cpp +++ b/test/tc.test.cpp @@ -27,89 +27,130 @@ #include // cmake.h include header must come first +#include #include +#include +#include #include #include -#include "tc/Replica.h" -#include "tc/Task.h" -#include "tc/WorkingSet.h" -#include "tc/util.h" -#include "test.h" +std::string uuid2str(tc::Uuid uuid) { return static_cast(uuid.to_string()); } //////////////////////////////////////////////////////////////////////////////// -int main(int, char**) { - UnitTest t(23); +// Tests for the basic cxxbridge functionality. This focuses on the methods with +// complex cxxbridge implementations, rather than those with complex Rust +// implementations but simple APIs, like sync. +int main(int, char **) { + UnitTest t; + std::string str; - // This function contains unit tests for the various bits of the wrappers for - // taskchampion-lib (that is, for `src/tc/*.cpp`). + auto replica = tc::new_replica_in_memory(); + auto uuid = tc::uuid_v4(); + auto uuid2 = tc::uuid_v4(); + t.is(uuid2str(uuid).size(), (size_t)36, "uuid string is the right length"); - //// util + rust::Vec ops; + auto task = tc::create_task(uuid, ops); + t.is(uuid2str(task->get_uuid()), uuid2str(uuid), "new task has correct uuid"); + task->update("status", "pending", ops); + task->update("description", "a task", ops); + task->update("description", "a cool task", ops); + tc::add_undo_point(ops); + task->delete_task(ops); - { - auto s1 = std::string("a\0string!"); - auto stc = tc::string2tc(s1); - auto s2 = tc::tc2string(stc); - t.is(s1, s2, "round-trip to tc string and back (containing an embedded NUL)"); + t.is(ops[0].is_create(), true, "ops[0] is create"); + t.is(uuid2str(ops[0].get_uuid()), uuid2str(uuid), "ops[0] has correct uuid"); + + t.is(ops[1].is_update(), true, "ops[1] is update"); + t.is(uuid2str(ops[1].get_uuid()), uuid2str(uuid), "ops[1] has correct uuid"); + ops[1].get_property(str); + t.is(str, "status", "ops[1] property is 'status'"); + t.ok(ops[1].get_value(str), "get_value succeeds"); + t.is(str, "pending", "ops[1] value is 'pending'"); + t.ok(!ops[1].get_old_value(str), "get_old_value has no old value"); + + t.is(ops[2].is_update(), true, "ops[2] is update"); + t.is(uuid2str(ops[2].get_uuid()), uuid2str(uuid), "ops[2] has correct uuid"); + ops[2].get_property(str); + t.is(str, "description", "ops[2] property is 'description'"); + t.ok(ops[2].get_value(str), "get_value succeeds"); + t.is(str, "a task", "ops[2] value is 'a task'"); + t.ok(!ops[2].get_old_value(str), "get_old_value has no old value"); + + t.is(ops[3].is_update(), true, "ops[3] is update"); + t.is(uuid2str(ops[3].get_uuid()), uuid2str(uuid), "ops[3] has correct uuid"); + ops[3].get_property(str); + t.is(str, "description", "ops[3] property is 'description'"); + t.ok(ops[3].get_value(str), "get_value succeeds"); + t.is(str, "a cool task", "ops[3] value is 'a cool task'"); + t.ok(ops[3].get_old_value(str), "get_old_value succeeds"); + t.is(str, "a task", "ops[3] old value is 'a task'"); + + t.is(ops[4].is_undo_point(), true, "ops[4] is undo_point"); + + t.is(ops[5].is_delete(), true, "ops[5] is delete"); + t.is(uuid2str(ops[5].get_uuid()), uuid2str(uuid), "ops[5] has correct uuid"); + auto old_task = ops[5].get_old_task(); + // old_task is in arbitrary order, so just check that status is in there. + bool found = false; + for (auto &pv : old_task) { + std::string p = static_cast(pv.prop); + if (p == "status") { + std::string v = static_cast(pv.value); + t.is(v, "pending", "old_task has status:pending"); + found = true; + } + } + t.ok(found, "found the status property in ops[5].old_task"); + + replica->commit_operations(std::move(ops)); + auto maybe_task2 = replica->get_task_data(tc::uuid_v4()); + t.ok(maybe_task2.is_none(), "looking up a random uuid gets nothing"); + + // The last operation deleted the task, but we want to see the task, so undo it.. + auto undo_ops = replica->get_undo_operations(); + t.ok(replica->commit_reversed_operations(std::move(undo_ops)), "undo committed successfully"); + + auto maybe_task3 = replica->get_task_data(uuid); + t.ok(maybe_task3.is_some(), "looking up the original uuid get TaskData"); + rust::Box task3 = maybe_task3.take(); + t.is(uuid2str(task3->get_uuid()), uuid2str(uuid), "reloaded task has correct uuid"); + t.ok(task3->get("description", str), "reloaded task has a description"); + t.is(str, "a cool task", "reloaded task has correct description"); + t.ok(task3->get("status", str), "reloaded task has a status"); + t.is(str, "pending", "reloaded task has correct status"); + + t.is(task3->properties().size(), (size_t)2, "task has 2 properties"); + t.is(task3->items().size(), (size_t)2, "task has 2 items"); + + rust::Vec ops2; + auto task4 = tc::create_task(uuid2, ops2); + task4->update("description", "another", ops2); + replica->commit_operations(std::move(ops2)); + + auto all_tasks = replica->all_task_data(); + t.is(all_tasks.size(), (size_t)2, "now there are 2 tasks"); + for (auto &maybe_task : all_tasks) { + t.ok(maybe_task.is_some(), "all_tasks is fully populated"); + auto task = maybe_task.take(); + if (task->get_uuid() == uuid) { + t.ok(task->get("description", str), "get_value succeeds"); + t.is(str, "a cool task", "description is 'a cool task'"); + } } - { - auto s1 = std::string("62123ec9-c443-4f7e-919a-35362a8bef8d"); - auto tcuuid = tc::uuid2tc(s1); - auto s2 = tc::tc2uuid(tcuuid); - t.is(s1, s2, "round-trip to TCUuid and back"); + // Check exception formatting. + try { + replica->sync_to_local("/does/not/exist", false); + // tc::new_replica_on_disk("/does/not/exist", false); + } catch (rust::Error &err) { + t.is(err.what(), + "unable to open database file: /does/not/exist/taskchampion-local-sync-server.sqlite3: " + "Error code 14: Unable to open the database file", + "error message has full context"); } - //// Replica - - auto rep = tc::Replica(); - t.pass("replica constructed"); - - auto maybe_task = rep.get_task("24478a28-4609-4257-bc19-44ec51391431"); - t.notok(maybe_task.has_value(), "task with fixed uuid does not exist"); - - auto task = rep.new_task(tc::Status::Pending, "a test"); - t.pass("new task constructed"); - t.is(task.get_description(), std::string("a test"), "task description round-trip"); - t.is(task.get_status(), tc::Status::Pending, "task status round-trip"); - - auto uuid = task.get_uuid(); - - auto maybe_task2 = rep.get_task(uuid); - t.ok(maybe_task2.has_value(), "task lookup by uuid finds task"); - t.is((*maybe_task2).get_description(), std::string("a test"), "task description round-trip"); - - rep.rebuild_working_set(true); - t.pass("rebuild_working_set"); - - auto tasks = rep.all_tasks(); - t.is((int)tasks.size(), 1, "all_tasks returns one task"); - - //// Task - - task = std::move(tasks[0]); - - t.is(task.get_uuid(), uuid, "returned task has correct uuid"); - t.is(task.get_status(), tc::Status::Pending, "returned task is pending"); - auto map = task.get_taskmap(); - t.is(map["description"], "a test", "task description in taskmap"); - t.is(task.get_description(), "a test", "returned task has correct description"); - t.is(task.is_waiting(), false, "task is not waiting"); - t.is(task.is_active(), false, "task is not active"); - - //// WorkingSet - - auto ws = rep.working_set(); - - t.is(ws.len(), (size_t)1, "WorkingSet::len"); - t.is(ws.largest_index(), (size_t)1, "WorkingSet::largest_index"); - t.is(ws.by_index(1).value(), uuid, "WorkingSet::by_index"); - t.is(ws.by_index(2).has_value(), false, "WorkingSet::by_index for unknown index"); - t.is(ws.by_uuid(uuid).value(), (size_t)1, "WorkingSet::by_uuid"); - t.is(ws.by_uuid("3e18a306-e3a8-4a53-a85c-fa7c057759a2").has_value(), false, - "WorkingSet::by_uuid for unknown uuid"); - return 0; } diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml deleted file mode 100644 index ffcbbe838..000000000 --- a/xtask/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "xtask" -version = "0.4.1" -edition = "2021" - -[dependencies] -anyhow.workspace = true -taskchampion-lib = { path = "../src/tc/lib" } -regex.workspace = true diff --git a/xtask/src/main.rs b/xtask/src/main.rs deleted file mode 100644 index 003844ea2..000000000 --- a/xtask/src/main.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! This executable defines the `cargo xtask` subcommands. -//! -//! At the moment it is very simple, but if this grows more subcommands then -//! it will be sensible to use `clap` or another similar library. - -use std::env; -use std::fs::File; -use std::io::Write; -use std::path::{Path, PathBuf}; - -pub fn main() -> anyhow::Result<()> { - let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); - let workspace_dir = manifest_dir.parent().unwrap(); - let arguments: Vec = env::args().collect(); - - if arguments.len() < 2 { - anyhow::bail!("xtask: Valid arguments are: `codegen`"); - } - - match arguments[1].as_str() { - "codegen" => codegen(workspace_dir), - _ => anyhow::bail!("xtask: unknown xtask"), - } -} - -/// `cargo xtask codegen` -/// -/// This uses ffizz-header to generate `lib/taskchampion.h`. -fn codegen(workspace_dir: &Path) -> anyhow::Result<()> { - let lib_crate_dir = workspace_dir.join("src/tc/lib"); - let mut file = File::create(lib_crate_dir.join("taskchampion.h")).unwrap(); - write!(&mut file, "{}", ::taskchampion_lib::generate_header()).unwrap(); - - Ok(()) -}