From 35074ccae9000a5823963b89318ee08065291f7d Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 18 Feb 2024 20:58:38 +0000 Subject: [PATCH] [WIP] Use TaskChampion 0.7.0, now via cxx instead of hand-rolled FFI TC 0.7.0 introduces a new `TaskData` type that maps to Taskwarrior's `Task` type more cleanly. It also introduces the idea of gathering lists of operations and "committing" them to a replica. A consequence of this change is that TaskChampion no longer automatically maintains dependency information, so Taskwarrior must do so, with its `TDB2::dependency_sync` method. This method does a very similar thing to what TaskChampion had been doing, so this is a shift of responsibility but not a major performance difference. Cxx is .. not great. It is missing a lot of useful things that make a general-purpose bridge impractical: - no support for trait objects - no support for `Option` (https://github.com/dtolnay/cxx/issues/87) - no support for `Vec>` As a result, some creativity is required in writing the bridge, for example returning a `Vec` from `all_task_data` to allow individual `TaskData` values to be "taken" from the vector. That said, Cxx is the current state-of-the-art, and does a good job of ensuring memory safety, at the cost of some slightly awkward APIs. Subsequent work can remove the "TDB2" layer and allow commands and other parts of Taskwarrior to interface directly with the `Replica`. TODO: - reference released `taskchampion 0.7.0` --- .cargo/config.toml | 1 - .github/workflows/checks.yml | 36 - .gitmodules | 4 +- CMakeLists.txt | 6 +- Cargo.lock | 235 ++-- Cargo.toml | 3 +- config.toml | 2 - doc/devel/contrib/rust-and-c++.md | 9 +- src/CMakeLists.txt | 10 +- src/Context.cpp | 14 +- src/TDB2.cpp | 363 ++++--- src/TDB2.h | 27 +- src/Task.cpp | 51 +- src/Task.h | 8 +- src/columns/CMakeLists.txt | 3 +- src/commands/CMakeLists.txt | 3 +- src/commands/CmdSync.cpp | 50 +- src/main.cpp | 6 + src/{tc => taskchampion-cpp}/.gitignore | 0 src/taskchampion-cpp/CMakeLists.txt | 16 + src/taskchampion-cpp/Cargo.toml | 15 + src/taskchampion-cpp/build.rs | 6 + src/{tc => taskchampion-cpp}/corrosion | 0 src/taskchampion-cpp/src/lib.rs | 973 +++++++++++++++++ src/tc/CMakeLists.txt | 30 - src/tc/Replica.cpp | 273 ----- src/tc/Replica.h | 127 --- src/tc/Server.cpp | 109 -- src/tc/Server.h | 85 -- src/tc/Task.cpp | 162 --- src/tc/Task.h | 130 --- src/tc/WorkingSet.cpp | 87 -- src/tc/WorkingSet.h | 74 -- src/tc/ffi.h | 36 - src/tc/lib/Cargo.toml | 18 - src/tc/lib/src/annotation.rs | 186 ---- src/tc/lib/src/atomic.rs | 34 - src/tc/lib/src/kv.rs | 155 --- src/tc/lib/src/lib.rs | 172 --- src/tc/lib/src/replica.rs | 958 ----------------- src/tc/lib/src/result.rs | 25 - src/tc/lib/src/server.rs | 234 ---- src/tc/lib/src/status.rs | 73 -- src/tc/lib/src/string.rs | 773 -------------- src/tc/lib/src/task.rs | 1304 ----------------------- src/tc/lib/src/traits.rs | 338 ------ src/tc/lib/src/uda.rs | 188 ---- src/tc/lib/src/util.rs | 31 - src/tc/lib/src/uuid.rs | 239 ----- src/tc/lib/src/workingset.rs | 141 --- src/tc/lib/taskchampion.h | 937 ---------------- src/tc/util.cpp | 79 -- src/tc/util.h | 52 - test/CMakeLists.txt | 3 +- test/feature.559.test.py | 6 +- test/tc.test.cpp | 185 ++-- xtask/Cargo.toml | 9 - xtask/src/main.rs | 35 - 58 files changed, 1518 insertions(+), 7611 deletions(-) delete mode 120000 .cargo/config.toml delete mode 100644 config.toml rename src/{tc => taskchampion-cpp}/.gitignore (100%) create mode 100644 src/taskchampion-cpp/CMakeLists.txt create mode 100644 src/taskchampion-cpp/Cargo.toml create mode 100644 src/taskchampion-cpp/build.rs rename src/{tc => taskchampion-cpp}/corrosion (100%) create mode 100644 src/taskchampion-cpp/src/lib.rs delete mode 100644 src/tc/CMakeLists.txt delete mode 100644 src/tc/Replica.cpp delete mode 100644 src/tc/Replica.h delete mode 100644 src/tc/Server.cpp delete mode 100644 src/tc/Server.h delete mode 100644 src/tc/Task.cpp delete mode 100644 src/tc/Task.h delete mode 100644 src/tc/WorkingSet.cpp delete mode 100644 src/tc/WorkingSet.h delete mode 100644 src/tc/ffi.h delete mode 100644 src/tc/lib/Cargo.toml delete mode 100644 src/tc/lib/src/annotation.rs delete mode 100644 src/tc/lib/src/atomic.rs delete mode 100644 src/tc/lib/src/kv.rs delete mode 100644 src/tc/lib/src/lib.rs delete mode 100644 src/tc/lib/src/replica.rs delete mode 100644 src/tc/lib/src/result.rs delete mode 100644 src/tc/lib/src/server.rs delete mode 100644 src/tc/lib/src/status.rs delete mode 100644 src/tc/lib/src/string.rs delete mode 100644 src/tc/lib/src/task.rs delete mode 100644 src/tc/lib/src/traits.rs delete mode 100644 src/tc/lib/src/uda.rs delete mode 100644 src/tc/lib/src/util.rs delete mode 100644 src/tc/lib/src/uuid.rs delete mode 100644 src/tc/lib/src/workingset.rs delete mode 100644 src/tc/lib/taskchampion.h delete mode 100644 src/tc/util.cpp delete mode 100644 src/tc/util.h delete mode 100644 xtask/Cargo.toml delete mode 100644 xtask/src/main.rs 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 15183b975..ecf049faa 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.70.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 d3d6f3b81..aaa8cbc2c 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]] @@ -209,6 +209,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" @@ -259,6 +269,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" @@ -280,12 +334,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" @@ -296,12 +344,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" @@ -329,29 +371,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" @@ -406,7 +425,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -724,15 +743,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" @@ -780,23 +790,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]] @@ -997,16 +996,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" @@ -1171,14 +1160,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.4" +version = "0.23.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" dependencies = [ "log", + "once_cell", "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.4", + "rustls-webpki 0.102.6", "subtle", "zeroize", ] @@ -1210,9 +1200,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 0.17.8", "rustls-pki-types", @@ -1237,6 +1227,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" @@ -1264,7 +1260,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1376,7 +1372,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.66", + "syn", ] [[package]] @@ -1385,17 +1381,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" @@ -1437,8 +1422,6 @@ dependencies = [ [[package]] name = "taskchampion" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55b167a2bea718f6f75f68c8d29f1550a6095d8917504d3b9c62626f4c4ef7cb" dependencies = [ "anyhow", "byteorder", @@ -1463,13 +1446,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" @@ -1487,7 +1477,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1562,7 +1552,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1613,7 +1603,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1667,6 +1657,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.7.1" @@ -1681,17 +1677,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", ] @@ -1715,9 +1710,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", @@ -1771,7 +1766,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "syn", "wasm-bindgen-shared", ] @@ -1805,7 +1800,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1870,6 +1865,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2034,21 +2038,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" @@ -2066,7 +2055,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..ab19bf729 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 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; - } + auto taskdata = tc::create_task(tcuuid, ops); - // 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; } + + taskdata->update(attr, task.get(attr), ops); } + replica()->commit_operations(std::move(ops)); - auto ws = replica.working_set(); + 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 = std::move(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,46 @@ 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); + if (!replica()->commit_reversed_operations(std::move(undo_ops))) { + std::cout << "Could not undo: other operations have occurred."; + } } - replica.rebuild_working_set(false); + // Note that commit_reversed_operations rebuilds the working set. } //////////////////////////////////////////////////////////////////////////////// -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 " << 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 +271,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 +309,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(); - - 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()))); + 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 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); + dependency_scan(result); - return result; + _pending_tasks = result; + } + + return *_pending_tasks; } //////////////////////////////////////////////////////////////////////////////// const std::vector TDB2::completed_tasks() { - auto all_tctasks = replica.all_tasks(); - const tc::WorkingSet& 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))); + if (!_completed_tasks) { + auto all_tctasks = replica()->all_task_data(); + auto& ws = working_set(); + + 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 +414,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 +475,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..ea7a0be18 100644 --- a/src/TDB2.h +++ b/src/TDB2.h @@ -30,8 +30,7 @@ #include #include #include -#include -#include +#include #include #include @@ -39,16 +38,12 @@ #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 +74,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/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 (origin != "") { + out << "sync.server.origin is deprecated. Use sync.server.url instead.\n"; + } if (server_dir != "") { - server = tc::Server::new_local(server_dir); - server_ident = 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"); } - server = tc::Server::new_gcp(gcp_bucket, gcp_credential_path, encryption_secret); - std::ostringstream os; - os << "GCP bucket " << gcp_bucket; - server_ident = os.str(); + 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 != "") { - 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(); + 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)."); } - 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'; - } - - 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..9b160bb1d --- /dev/null +++ b/src/taskchampion-cpp/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required (VERSION 3.22) + +add_subdirectory(${CMAKE_SOURCE_DIR}/src/taskchampion-cpp/corrosion) + +# 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..f9a3c364a --- /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 = { path = "../../../taskchampion/taskchampion", features = ["server-sync"] } +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..445ea2701 --- /dev/null +++ b/src/taskchampion-cpp/src/lib.rs @@ -0,0 +1,973 @@ +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, using ids as indices into the vector. Empty indices have + /// 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 + +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 ff19a08cd..000000000 --- a/src/tc/CMakeLists.txt +++ /dev/null @@ -1,30 +0,0 @@ -cmake_minimum_required (VERSION 3.22) - -add_subdirectory(${CMAKE_SOURCE_DIR}/src/tc/corrosion) - -# 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 57c81bf5f..378fbed30 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 @@ def test_exit_on_missing_db(self): 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,88 +27,129 @@ #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); - - // This function contains unit tests for the various bits of the wrappers for - // taskchampion-lib (that is, for `src/tc/*.cpp`). - - //// util - - { - 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)"); +// 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; + + 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"); + + 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); + + 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; + } } - - { - 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"); + 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'"); + } } - //// 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"); + // 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"); + } 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(()) -}