Compare commits

...

45 Commits

Author SHA1 Message Date
Dustin J. Mitchell
7a092bea03 Release v3.2.0 (#3679) 2024-11-12 14:52:22 -05:00
Dustin J. Mitchell
54a94bd18c include bubblegum-256.theme in default .taskrc (#3673) 2024-11-08 13:10:04 +01:00
Dustin J. Mitchell
a2f9b92d6c Better undo output (and remove undo.style config) (#3672) 2024-11-07 14:56:34 -05:00
Dustin J. Mitchell
dcc8a8cdde bump libshared for bold 256color support (#3670)
In particular, commit 47a750c385.
2024-11-06 07:40:16 -05:00
Dustin J. Mitchell
c9967c20e2 Restore support for task info journal (#3671)
This support was removed before Taskwarrior-3.x, and is now restored,
including the original tests removed in
ddd367232e
2024-11-06 07:39:39 -05:00
Dustin J. Mitchell
7da23aee1c Run cargo test and fix it (#3663)
run cargo test and fix it
2024-11-05 08:55:10 -05:00
Denis Zh.
5b1be95f7d Add color.calendar.scheduled to no-color.theme (#3666)
* Add scheduled color setting for calendar report
* Add default color.calendar.scheduled to all themes
2024-11-05 08:54:49 -05:00
Denis Zh.
0ff7844732 Fix missing line in man task-color (#3665)
Escape leading single quote to prevent groff misinterpretation as a
control character.
2024-11-05 08:00:43 -05:00
pre-commit-ci[bot]
023e7958c9 [pre-commit.ci] pre-commit autoupdate (#3664)
updates:
- [github.com/pre-commit/mirrors-clang-format: v19.1.2 → v19.1.3](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.2...v19.1.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-11-04 16:42:35 -05:00
Dustin J. Mitchell
8184226319 Support ENABLE_TLS_NATIVE_ROOTS to use system TLS CAs (#3660) 2024-11-02 11:16:23 +01:00
Dustin J. Mitchell
94c95563ab Upgrade to TaskChampion 0.9.0 (#3662)
See https://github.com/GothenburgBitFactory/taskchampion/releases/tag/v0.9.0
2024-10-31 12:59:49 +00:00
Dustin J. Mitchell
6ff900f3fc Use Replica::pending_tasks (#3661)
This replaces a loop over _all_ tasks with one that fetches only pending
tasks, as determined by the working set.

This should be faster for task DB's with large numbers of completed
tasks, although on my medium-sized installation (~5000 total tasks) the
difference is negligible.
2024-10-30 21:49:04 -04:00
dependabot[bot]
8bad3cdcbc Bump docker/build-push-action from 6.8.0 to 6.9.0 (#3642)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.8.0 to 6.9.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.8.0...v6.9.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-27 15:22:38 -04:00
pre-commit-ci[bot]
0bb32d188c [pre-commit.ci] pre-commit autoupdate (#3657)
updates:
- [github.com/pre-commit/mirrors-clang-format: v19.1.1 → v19.1.2](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.1...v19.1.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-27 15:22:29 -04:00
dependabot[bot]
c3b850898f Bump sigstore/cosign-installer from 3.6.0 to 3.7.0 (#3641)
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.6.0 to 3.7.0.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.6.0...v3.7.0)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-27 15:22:12 -04:00
Fredrik Lanker
af8c5d58c8 Limit the allowed epoch timestamps (#3651)
The code for parsing epoch timestamps when displaying tasks only
supports values between year 1980 and 9999. Previous to this change, it
was possible to set e.g., the due timestamp to a value outside of these
limits, which would make it impossible to later on show the task.

With this change, we only allow setting values within the same limits
used by the code for displaying tasks.
2024-10-23 19:18:21 -04:00
Dustin J. Mitchell
2db373d631 Update to TaskChampion 0.8.0 (#3648)
* Update to TaskChampion 0.8.0

* Cargo update
2024-10-22 19:37:47 -04:00
Dustin J. Mitchell
96c72f3e06 Issue warnings instead of errors for 'weird' tasks (#3646)
* Issue warnings instead of errors for 'weird' tasks

* Support more comprehensive checks when adding a task
2024-10-22 15:15:51 -04:00
Thomas Lauf
4bf6144daf Add SECURITY.md (#3655) 2024-10-21 15:16:25 -04:00
Scott Mcdermott
3e20ad6f6f Pass rc.weekstart to libshared for ISO8601 weeknum parsing if "monday" (#3654)
* libshared: bump for weekstart, epoch defines, eopww fix

mainly those visible changes, and miscellaneous others

see GothenburgBitFactory/taskwarrior#3623 (weekstart)
see GothenburgBitFactory/taskwarrior#3651 (epoch limit defines)
see GothenburgBitFactory/libshared#73 (eopww fix)

* Initialize libshared's weekstart from user's rc.weekstart config

This enables use of newer libshared code that can parse week numbers
according to ISO8601 instead of existing code which is always using
Sunday-based weeks.  To get ISO behavior, set rc.weekstart=monday.
Default is still Sunday / old algorithm, as before, since Sunday is in
the hardcoded default rcfile.

Weekstart does not yet fix week-relative shortcuts, which will still
always use Monday.

See #3623 for further details.
2024-10-19 16:00:50 -04:00
Dustin J. Mitchell
7bd3d1b892 Install uuid-dev in GitHub action (#3647) 2024-10-14 17:48:41 -04:00
pre-commit-ci[bot]
0bd3989bab [pre-commit.ci] pre-commit autoupdate (#3650)
updates:
- [github.com/psf/black: 24.8.0 → 24.10.0](https://github.com/psf/black/compare/24.8.0...24.10.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-14 17:47:39 -04:00
Dustin J. Mitchell
26c383d615 Restore 'load' timer (#3635)
* Restore 'load' timer

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-10 07:33:02 +02:00
pre-commit-ci[bot]
a8b4bcdda8 [pre-commit.ci] pre-commit autoupdate (#3638)
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0)
- [github.com/pre-commit/mirrors-clang-format: v18.1.8 → v19.1.1](https://github.com/pre-commit/mirrors-clang-format/compare/v18.1.8...v19.1.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-07 14:36:44 -04:00
Dustin J. Mitchell
28628e5dca Add newline in sync error message (#3603) 2024-10-03 18:32:13 -04:00
dependabot[bot]
ff2b1cb888 Bump docker/build-push-action from 6.7.0 to 6.8.0 (#3637)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.7.0 to 6.8.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.7.0...v6.8.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 08:59:26 -04:00
dependabot[bot]
cfe92ce845 Bump threeal/gcovr-action from 1.0.0 to 1.1.0 (#3636)
Bumps [threeal/gcovr-action](https://github.com/threeal/gcovr-action) from 1.0.0 to 1.1.0.
- [Release notes](https://github.com/threeal/gcovr-action/releases)
- [Commits](https://github.com/threeal/gcovr-action/compare/v1.0.0...v1.1.0)

---
updated-dependencies:
- dependency-name: threeal/gcovr-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 08:06:38 -04:00
Dustin J. Mitchell
c95dc9d149 Ignore SIGPIPE (#3627)
This replicates what the Rust runtime does, and matches what Rust code
expects, for example when writing to a socket which is no longer
connected to the remote end.
2024-09-22 18:57:46 -04:00
Gagan Nagaraj
d75ef7f197 Check if end date is being set to a pending task (#3622)
check if end date is being set to a pending task

-throw error if end date is being set to a pending task
- add test for the bug
2024-09-13 12:16:20 -04:00
Gagan Nagaraj
c00c0e941b Throw error when task config write is unsuccessfully (#3620) 2024-09-11 10:20:22 -04:00
Gagan Nagaraj
6a24510473 Exclude attributes starting with tag_ (#3619)
* Exclude attributes starting with tag_

* Check only for tag_*
2024-09-09 08:12:19 -04:00
Tobias Predel
72f9cd91a5 Refine INSTALL file (#3615)
Update INSTALL file

CMake can also abstract the install procedure from the underlying Makefile
2024-09-02 18:22:33 -04:00
Dustin J. Mitchell
44d443a8d6 Update INSTALL file (#3606) 2024-09-02 12:53:50 +00:00
Dustin J. Mitchell
2e3badbf99 Add some instructions to the MSRV (#3604)
There is no easy way to determine the MSRV for TaskChampion, other than
somehow pulling the right version of the source and grepping for it. In
practice, if we update the `taskchampion` dependency to one that has a
higher MSRV, we'll get a build error and find this comment. And if we
get an error building Taskwarrior due to an old MSRV (for example if
something changes on `crates.io`) then we will also find this comment.

This also removes some superfluous dependency versions from the root
workspace. `src/taskchampion-cpp/Cargo.toml` specifies versions
directly.
2024-08-26 21:45:19 -04:00
dependabot[bot]
6cfbb16966 Bump docker/build-push-action from 6.6.1 to 6.7.0 (#3602)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.6.1 to 6.7.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.6.1...v6.7.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 09:22:41 -04:00
Dustin J. Mitchell
70632b088e Do not count undo operations in the 'would be reverted..' message (#3598) 2024-08-14 08:35:34 -04:00
dependabot[bot]
d46e5eca58 Bump docker/build-push-action from 6.5.0 to 6.6.1 (#3595)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.5.0 to 6.6.1.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.5.0...v6.6.1)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-12 08:23:45 -04:00
dependabot[bot]
05da133eb6 Bump sigstore/cosign-installer from 3.5.0 to 3.6.0 (#3594)
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.5.0 to 3.6.0.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.5.0...v3.6.0)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-12 08:22:36 -04:00
Dustin J. Mitchell
c719cce4f1 Run all C++ tests from a single executable (#3582) 2024-08-12 00:20:17 +00:00
Dustin J. Mitchell
4ff63a7960 Use TaskChampion 0.7.0, now via cxx instead of hand-rolled FFI (#3588)
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<T>` (https://github.com/dtolnay/cxx/issues/87)
 - no support for `Vec<Box<..>>`

As a result, some creativity is required in writing the bridge, for
example returning a `Vec<OptionTaskData>` 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`.
2024-08-11 02:06:00 +00:00
Jan Christian Grünhage
0f96fd31bf Update google-cloud-auth to drop ring@0.16.20 (#3591)
* Update google-cloud-auth to drop ring@0.16.20

ring@0.16.20 doesn't build on ppc and risc-v, and updating
google-cloud-auth pulls in a newer version of jsonwebtoken,
which in turn depends on a newer version of ring that we depend on
already either way.

This necessitated an MSRV bump to 1.73.0
2024-08-09 21:24:12 -04:00
Jan Christian Grünhage
3d30f2ac46 Optionally use system provided corrosion (#3590) 2024-08-09 13:05:19 -04:00
Dustin J. Mitchell
49e09a9783 Remove accidentally-included sqlite3 file (#3589) 2024-08-08 03:11:56 +00:00
Dustin J. Mitchell
17889a3f25 Actually run shell tests (#3583)
Two of these used EXPFAIL which, because nothing is interpreting the TAP
output, does not work. So, that functionality is removed, and the
expected-to-fail bits are commented out or removed.

There was a conditional on the filename in `bash_tap.sh` which caused it
to not actually do anything and just run the test as a simple shell
script. That, too, has been removed.
2024-08-07 00:44:12 +00:00
pre-commit-ci[bot]
c0b708d1f3 [pre-commit.ci] pre-commit autoupdate (#3587)
updates:
- [github.com/psf/black: 24.4.2 → 24.8.0](https://github.com/psf/black/compare/24.4.2...24.8.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-05 14:20:35 -04:00
149 changed files with 2749 additions and 8458 deletions

View File

@@ -1 +0,0 @@
../config.toml

View File

@@ -29,7 +29,10 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
toolchain: "1.70.0" # MSRV
# If this version is old enough to cause errors, or older than the
# TaskChampion MSRV, bump it to the MSRV of the currently-required
# TaskChampion package; if necessary, bump that version as well.
toolchain: "1.73.0" # MSRV
override: true
- uses: actions-rs/cargo@v1.0.3
@@ -61,39 +64,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

View File

@@ -29,7 +29,7 @@ jobs:
submodules: "recursive"
- name: Install cosign
uses: sigstore/cosign-installer@v3.5.0
uses: sigstore/cosign-installer@v3.7.0
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3.3.0
@@ -40,7 +40,7 @@ jobs:
- name: Build and push Taskwarrior Docker image
id: build-and-push
uses: docker/build-push-action@v6.5.0
uses: docker/build-push-action@v6.9.0
with:
context: .
file: "./docker/task.dockerfile"

View File

@@ -17,6 +17,9 @@ jobs:
toolchain: "stable"
override: true
- name: Install uuid-dev
run: sudo apt install uuid-dev
- name: make a release tarball and build from it
run: |
cmake -S. -Bbuild &&

View File

@@ -17,13 +17,13 @@ jobs:
run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS=--coverage
- name: Build project
run: cmake --build build --target build_tests
run: cmake --build build --target test_runner --target task_executable
- name: Test project
run: ctest --test-dir build -j 8 --output-on-failure
- name: Generate a code coverage report
uses: threeal/gcovr-action@v1.0.0
uses: threeal/gcovr-action@v1.1.0
with:
coveralls-out: coverage.coveralls.json
excludes: |
@@ -94,6 +94,37 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
cargo-test:
runs-on: ubuntu-latest
name: "Cargo Test"
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:
# If this version is old enough to cause errors, or older than the
# TaskChampion MSRV, bump it to the MSRV of the currently-required
# TaskChampion package; if necessary, bump that version as well.
toolchain: "1.73.0" # MSRV
override: true
- uses: actions-rs/cargo@v1.0.3
with:
command: test
tests:
needs: coverage
strategy:

4
.gitmodules vendored
View File

@@ -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

View File

@@ -2,18 +2,18 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v18.1.8
rev: v19.1.3
hooks:
- id: clang-format
types_or: [c++, c]
- repo: https://github.com/psf/black
rev: 24.4.2
rev: 24.10.0
hooks:
- id: black

View File

@@ -4,7 +4,7 @@ enable_testing()
set (CMAKE_EXPORT_COMPILE_COMMANDS ON)
project (task
VERSION 3.1.0
VERSION 3.2.0
DESCRIPTION "Taskwarrior - a command-line TODO list manager"
HOMEPAGE_URL https://taskwarrior.org/)
@@ -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)

787
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,7 @@
[workspace]
members = [
"src/tc/lib",
"xtask",
"src/taskchampion-cpp",
]
resolver = "2"
# All Rust dependencies are defined here, and then referenced by the
# Cargo.toml's in the members with `foo.workspace = true`.
[workspace.dependencies]
anyhow = "1.0"
ffizz-header = "0.5"
libc = "0.2.136"
pretty_assertions = "1"
regex = "^1.10.2"
taskchampion = "0.6"

View File

@@ -1,5 +1,30 @@
------ current release ---------------------------
3.2.0 -
- Support for the journal in `task info` has been restored (#3671) and the
task info output no longer contains `tag_` values (#3619).
- The `rc.weekstart` value now affects calculation of week numbers in
expressions like `2013-W49` (#2654).
- Build-time flag `ENABLE_TLS_NATIVE_ROOTS` will cause `task sync` to use the
system TLS roots instead of its built-in roots to authenticate the server (#3660).
- The output from `task undo` is now more human-readable. The `undo.style`
configuraiton option, which has had no effect since 3.0.0, is now removed (3672).
- Fetching pending tasks is now more efficient (#3661).
Thanks to the following people for contributions to this release:
- Denis Zh.
- Dustin J. Mitchell
- Fredrik Lanker
- Gagan Nagaraj
- Jan Christian Grünhage
- Scott Mcdermott
- Thomas Lauf
- Tobias Predel
------ old releases ------------------------------
3.1.0 -
- Support for `task purge` has been restored, and new support added for automatically
@@ -35,8 +60,6 @@ Thanks to the following people for contributions to this release:
- Steve Dondley
- Will R S Hansen
------ old releases ------------------------------
3.0.2 -
- Fix an accidentally-included debug print which polluted output of

14
INSTALL
View File

@@ -21,18 +21,20 @@ You will need a C++ compiler that supports full C++17, which includes:
You will need the following libraries:
- libuuid (not needed for OSX)
You will need a Rust toolchain of the Minimum Supported Rust Version (MSRV):
- rust 1.73.0
Basic Installation
------------------
Briefly, these shell commands will unpack, build and install Taskwarrior:
$ tar xzf task-X.Y.Z.tar.gz [1]
$ cd task-X.Y.Z [2]
$ cmake -DCMAKE_BUILD_TYPE=release . [3]
$ make [4]
$ sudo make install [5]
$ cd .. ; rm -r task-X.Y.Z [6]
$ tar xzf task-X.Y.Z.tar.gz [1]
$ cd task-X.Y.Z [2]
$ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release . [3]
$ cmake --build build [4]
$ sudo cmake --install build [5]
$ cd .. ; rm -r task-X.Y.Z [6]
These commands are explained below:

13
SECURITY.md Normal file
View File

@@ -0,0 +1,13 @@
# Security
To report a vulnerability, please contact [dustin@cs.uchicago.edu](mailto:dustin@cs.uchicago.edu), you may use GPG public-key D8097934A92E4B4210368102FF8B7AC6154E3226 which is available [here](https://keybase.io/djmitche/pgp_keys.asc?fingerprint=d8097934a92e4b4210368102ff8b7ac6154e3226).
Initial response is expected within ~48h.
We kindly ask to follow the responsible disclosure model and refrain from sharing information until:
1. Vulnerabilities are patched in Taskwarrior + 60 days to coordinate with distributions.
2. 90 days since the vulnerability is disclosed to us.
We recognise the legitimacy of public interest and accept that security researchers can publish information after 90-days deadline unilaterally.
We will assist with obtaining CVE and acknowledge the vulnerabilities reported.

View File

@@ -1,2 +0,0 @@
[alias]
xtask = "run --package xtask --"

View File

@@ -52,9 +52,9 @@ cmake --build build-clang
## Run the Test Suite:
For running the test suite [ctest](https://cmake.org/cmake/help/latest/manual/ctest.1.html) is used.
Before one can run the test suite the `task_executable` must be built.
After that also the `build_tests` target must be build, which can be done over:
After that also the `test_runner` target must be build, which can be done over:
```sh
cmake --build build --target build_tests
cmake --build build --target test_runner
```
Again you may also use the `-j <number-of-jobs>` option for parallel builds.

View File

@@ -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.

View File

@@ -278,7 +278,7 @@ The keyword rule shown here as 'keyword.' corresponds to a wildcard pattern,
meaning 'color.keyword.*', or in other words all the keyword rules.
There is also 'color.project.none', 'color.tag.none' and
'color.uda.priority.none' to specifically represent missing data.
\[aq]color.uda.priority.none' to specifically represent missing data.
.SH THEMES
Taskwarrior supports themes. What this really means is that with the ability to

View File

@@ -508,15 +508,6 @@ weekly recurring task is added with a due date of tomorrow, and recurrence.limit
is set to 2, then a report will list 2 pending recurring tasks, one for tomorrow,
and one for a week from tomorrow.
.TP
.B undo.style=side
When the 'undo' command is run, Taskwarrior presents a before and after
comparison of the data. This can be in either the 'side' style, which compares
values side-by-side in a table, or 'diff' style, which uses a format similar to
the 'diff' command.
Currently not supported.
.TP
.B abbreviation.minimum=2
Minimum length of any abbreviated command/value. This means that "ve", "ver",

View File

@@ -84,6 +84,7 @@ color.calendar.due=color0 on rgb325
color.calendar.due.today=color0 on rgb404
color.calendar.holiday=color15 on rgb102
color.calendar.overdue=color0 on color5
color.calendar.scheduled=
color.calendar.today=color15 on rgb103
color.calendar.weekend=gray12 on gray3
color.calendar.weeknumber=rgb104

View File

@@ -86,6 +86,7 @@ color.calendar.due=white on red
color.calendar.due.today=bold white on red
color.calendar.holiday=black on bright yellow
color.calendar.overdue=black on bright red
color.calendar.scheduled=
color.calendar.today=bold white on bright blue
color.calendar.weekend=white on bright black
color.calendar.weeknumber=bold blue

View File

@@ -82,6 +82,7 @@ color.summary.bar=black on rgb141
color.calendar.due.today=color15 on color1
color.calendar.due=color0 on color1
color.calendar.holiday=color0 on color11
color.calendar.scheduled=
color.calendar.overdue=color0 on color9
color.calendar.today=color15 on rgb013
color.calendar.weekend=on color235

View File

@@ -83,6 +83,7 @@ color.calendar.due.today=color0 on color252
color.calendar.due=color0 on color249
color.calendar.holiday=color255 on rgb013
color.calendar.overdue=color0 on color255
color.calendar.scheduled=
color.calendar.today=color0 on rgb115
color.calendar.weekend=on color235
color.calendar.weeknumber=rgb015

View File

@@ -83,6 +83,7 @@ color.calendar.due=on gray8
color.calendar.due.today=black on gray15
color.calendar.holiday=black on gray20
color.calendar.overdue=gray2 on gray10
color.calendar.scheduled=
color.calendar.today=bold white
color.calendar.weekend=on gray2
color.calendar.weeknumber=gray6

View File

@@ -83,6 +83,7 @@ color.calendar.due=color0 on gray10
color.calendar.due.today=color0 on gray15
color.calendar.holiday=color15 on rgb005
color.calendar.overdue=color0 on gray20
color.calendar.scheduled=
color.calendar.today=underline black on color15
color.calendar.weekend=on gray4
color.calendar.weeknumber=gray10

View File

@@ -83,6 +83,7 @@ color.calendar.due.today=color0 on color225
color.calendar.due=color0 on color249
color.calendar.holiday=rgb151 on rgb020
color.calendar.overdue=color0 on color255
color.calendar.scheduled=
color.calendar.today=color0 on rgb151
color.calendar.weekend=on color235
color.calendar.weeknumber=rgb010

View File

@@ -83,6 +83,7 @@ color.calendar.due.today=color0 on color252
color.calendar.due=color0 on color249
color.calendar.holiday=rgb522 on rgb300
color.calendar.overdue=color0 on color255
color.calendar.scheduled=
color.calendar.today=color0 on rgb511
color.calendar.weekend=on color235
color.calendar.weeknumber=rgb100

View File

@@ -83,6 +83,7 @@ color.calendar.due=color0 on rgb325
color.calendar.due.today=color0 on rgb404
color.calendar.holiday=color15 on rgb102
color.calendar.overdue=color0 on color5
color.calendar.scheduled=
color.calendar.today=color15 on rgb103
color.calendar.weekend=gray12 on gray3
color.calendar.weeknumber=rgb104

View File

@@ -83,6 +83,7 @@ color.calendar.due=color0 on rgb440
color.calendar.due.today=color0 on rgb430
color.calendar.holiday=rgb151 on rgb020
color.calendar.overdue=color0 on rgb420
color.calendar.scheduled=
color.calendar.today=color15 on rgb110
color.calendar.weekend=on color235
color.calendar.weeknumber=rgb110

View File

@@ -83,6 +83,7 @@ color.calendar.due=on bright green
color.calendar.due.today=blue on bright yellow
color.calendar.holiday=on yellow
color.calendar.overdue=on bright red
color.calendar.scheduled=
color.calendar.today=blue
color.calendar.weekend=on white
color.calendar.weeknumber=blue

View File

@@ -83,6 +83,7 @@ color.calendar.due=on rgb343
color.calendar.due.today=on rgb353
color.calendar.holiday=color0 on rgb530
color.calendar.overdue=on rgb533
color.calendar.scheduled=
color.calendar.today=rgb005
color.calendar.weekend=on gray21
color.calendar.weeknumber=gray16

View File

@@ -86,6 +86,7 @@ color.calendar.due=
color.calendar.due.today=
color.calendar.holiday=
color.calendar.overdue=
color.calendar.scheduled=
color.calendar.today=
color.calendar.weekend=
color.calendar.weeknumber=

View File

@@ -100,6 +100,7 @@ color.calendar.due=color0 on color9
color.calendar.due.today=color0 on color1
color.calendar.holiday=color0 on color3
color.calendar.overdue=color0 on color5
color.calendar.scheduled=
color.calendar.today=color0 on color4
color.calendar.weekend=on color0
color.calendar.weeknumber=color4

View File

@@ -100,6 +100,7 @@ color.calendar.due=color7 on color9
color.calendar.due.today=color7 on color1
color.calendar.holiday=color7 on color3
color.calendar.overdue=color7 on color5
color.calendar.scheduled=
color.calendar.today=color7 on color4
color.calendar.weekend=on color7
color.calendar.weeknumber=color14

View File

@@ -168,7 +168,6 @@ syn match taskrcGoodKey '^\s*\Vsugar='he=e-1
syn match taskrcGoodKey '^\s*\Vsync.\(server.\(url\|origin\|client_id\|encryption_secret\)\|local.server_dir\)='he=e-1
syn match taskrcGoodKey '^\s*\Vtag.indicator='he=e-1
syn match taskrcGoodKey '^\s*\Vuda.\S\{-}.\(default\|type\|label\|values\|indicator\)='he=e-1
syn match taskrcGoodKey '^\s*\Vundo.style='he=e-1
syn match taskrcGoodKey '^\s*\Vurgency.active.coefficient='he=e-1
syn match taskrcGoodKey '^\s*\Vurgency.age.coefficient='he=e-1
syn match taskrcGoodKey '^\s*\Vurgency.age.max='he=e-1

View File

@@ -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
@@ -15,6 +13,7 @@ add_library (task STATIC CLI2.cpp CLI2.h
Filter.cpp Filter.h
Hooks.cpp Hooks.h
Lexer.cpp Lexer.h
Operation.cpp Operation.h
TDB2.cpp TDB2.h
Task.cpp Task.h
Variant.cpp Variant.h
@@ -28,6 +27,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 +52,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")

View File

@@ -37,9 +37,11 @@
#include <assert.h>
#include <format.h>
#include <main.h>
#include <rust/cxx.h>
#include <shared.h>
#include <stdlib.h>
#include <string.h>
#include <taskchampion-cpp/lib.h>
#include <unistd.h>
#include <algorithm>
@@ -125,7 +127,6 @@ std::string configurationDefaults =
"dependency.indicator=D # What to show as a dependency indicator\n"
"recurrence.indicator=R # What to show as a task recurrence indicator\n"
"recurrence.limit=1 # Number of future recurring pending tasks\n"
"undo.style=side # Undo style - can be 'side', or 'diff'\n"
"regex=1 # Assume all search/filter strings are "
"regexes\n"
"xterm.title=0 # Sets xterm title for some commands\n"
@@ -681,6 +682,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 +695,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 +778,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;
@@ -1093,6 +1104,11 @@ void Context::staticInitialization() {
Task::regex = Variant::searchUsingRegex = config.getBoolean("regex");
Lexer::dateFormat = Variant::dateFormat = config.get("dateformat");
auto weekStart = Datetime::dayOfWeek(config.get("weekstart"));
if (weekStart != 0 && weekStart != 1)
throw std::string(
"The 'weekstart' configuration variable may only contain 'Sunday' or 'Monday'.");
Datetime::weekstart = weekStart;
Datetime::isoEnabled = config.getBoolean("date.iso");
Datetime::standaloneDateEnabled = false;
Datetime::standaloneTimeEnabled = false;
@@ -1155,6 +1171,7 @@ void Context::createDefaultConfig() {
<< "\n# Color theme (uncomment one to use)\n"
<< "#include light-16.theme\n"
<< "#include light-256.theme\n"
<< "#include bubblegum-256.theme\n"
<< "#include dark-16.theme\n"
<< "#include dark-256.theme\n"
<< "#include dark-red-256.theme\n"

View File

@@ -1,6 +1,6 @@
////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2022, Dustin J. Mitchell
// Copyright 2006 - 2024, 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
@@ -27,53 +27,47 @@
#include <cmake.h>
// cmake.h include header must come first
#include <assert.h>
#include <format.h>
#include <Operation.h>
#include <taskchampion-cpp/lib.h>
#include "tc/Replica.h"
#include "tc/Task.h"
#include <vector>
using namespace tc::ffi;
namespace tc {
////////////////////////////////////////////////////////////////////////////////
TCString string2tc(const std::string& str) {
return tc_string_clone_with_len(str.data(), str.size());
Operation::Operation(const tc::Operation& op) : op(&op) {}
////////////////////////////////////////////////////////////////////////////////
std::vector<Operation> Operation::operations(const rust::Vec<tc::Operation>& operations) {
return {operations.begin(), operations.end()};
}
////////////////////////////////////////////////////////////////////////////////
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;
Operation& Operation::operator=(const Operation& other) {
op = other.op;
return *this;
}
////////////////////////////////////////////////////////////////////////////////
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");
bool Operation::operator<(Operation& other) const {
if (is_create()) {
return !other.is_create();
} else if (is_update()) {
if (other.is_create()) {
return false;
} else if (other.is_update()) {
return get_timestamp() < other.get_timestamp();
} else {
return true;
}
} else if (is_delete()) {
if (other.is_create() || other.is_update() || other.is_delete()) {
return false;
} else {
return true;
}
} else if (is_undo_point()) {
return !other.is_undo_point();
}
return rv;
return false; // not reachable
}
////////////////////////////////////////////////////////////////////////////////
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

88
src/Operation.h Normal file
View File

@@ -0,0 +1,88 @@
////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2006 - 2024, 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_OPERATIOn
#define INCLUDED_OPERATIOn
#include <taskchampion-cpp/lib.h>
#include <optional>
#include <vector>
// Representation of a TaskChampion operation.
//
// This class wraps `tc::Operation&` and thus cannot outlive that underlying
// type.
class Operation {
public:
explicit Operation(const tc::Operation &);
Operation(const Operation &other) = default;
Operation &operator=(const Operation &other);
// Create a vector of Operations given the result of `Replica::get_undo_operations` or
// `Replica::get_task_operations`. The resulting vector must not outlive the input `rust::Vec`.
static std::vector<Operation> operations(const rust::Vec<tc::Operation> &);
// Methods from the underlying `tc::Operation`.
bool is_create() const { return op->is_create(); }
bool is_update() const { return op->is_update(); }
bool is_delete() const { return op->is_delete(); }
bool is_undo_point() const { return op->is_undo_point(); }
std::string get_uuid() const { return std::string(op->get_uuid().to_string()); }
::rust::Vec<::tc::PropValuePair> get_old_task() const { return op->get_old_task(); };
std::string get_property() const {
std::string value;
op->get_property(value);
return value;
}
std::optional<std::string> get_value() const {
std::optional<std::string> value{std::string()};
if (!op->get_value(value.value())) {
value = std::nullopt;
}
return value;
}
std::optional<std::string> get_old_value() const {
std::optional<std::string> old_value{std::string()};
if (!op->get_old_value(old_value.value())) {
old_value = std::nullopt;
}
return old_value;
}
time_t get_timestamp() const { return static_cast<time_t>(op->get_timestamp()); }
// Define a partial order on Operations:
// - Create < Update < Delete < UndoPoint
// - Given two updates, sort by timestamp
bool operator<(Operation &other) const;
private:
const tc::Operation *op;
};
#endif
////////////////////////////////////////////////////////////////////////////////

View File

@@ -46,79 +46,55 @@
#include <unordered_set>
#include <vector>
#include "tc/Server.h"
#include "tc/util.h"
bool TDB2::debug_mode = false;
static void dependency_scan(std::vector<Task>&);
////////////////////////////////////////////////////////////////////////////////
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);
}
////////////////////////////////////////////////////////////////////////////////
// Add the new task to the replica.
void TDB2::add(Task& task) {
// Validate a task for addition. This is stricter than `task.validate`, as any
// inconsistency is probably user error.
task.validate_add();
// Ensure the task is consistent, and provide defaults if necessary.
// bool argument to validate() is "applyDefault", to apply default values for
// properties not otherwise given.
task.validate(true);
std::string uuid = task.get("uuid");
rust::Vec<tc::Operation> ops;
maybe_add_undo_point(ops);
auto uuid = task.get("uuid");
changes[uuid] = task;
tc::Uuid tcuuid = tc::uuid_from_string(uuid);
// run hooks for this new task
Context::getContext().hooks.onAdd(task);
auto innertask = replica.import_task_with_uuid(uuid);
auto taskdata = tc::create_task(tcuuid, ops);
{
auto guard = replica.mutate_task(innertask);
// add the task attributes
for (auto& attr : task.all()) {
// TaskChampion does not store uuid or id in the taskmap
if (attr == "uuid" || attr == "id") {
continue;
}
// Use `set_status` for the task status, to get expected behavior
// with respect to the working set.
else if (attr == "status") {
innertask.set_status(Task::status2tc(Task::textToStatus(task.get(attr))));
}
// use `set_modified` to set the modified timestamp, avoiding automatic
// updates to this field by TaskChampion.
else if (attr == "modified") {
auto mod = (time_t)std::stoi(task.get(attr));
innertask.set_modified(mod);
}
// otherwise, just set the k/v map value
else {
innertask.set_value(attr, std::make_optional(task.get(attr)));
}
// add the task attributes
for (auto& attr : task.all()) {
// TaskChampion does not store uuid or id in the task data
if (attr == "uuid" || attr == "id") {
continue;
}
}
auto ws = replica.working_set();
taskdata->update(attr, task.get(attr), ops);
}
replica()->commit_operations(std::move(ops));
invalidate_cached_info();
// get the ID that was assigned to this task
auto id = ws.by_uuid(uuid);
// update the cached working set with the new information
_working_set = std::make_optional(std::move(ws));
if (id.has_value()) {
task.id = id.value();
auto id = working_set()->by_uuid(tcuuid);
if (id > 0) {
task.id = id;
}
}
@@ -144,6 +120,9 @@ void TDB2::modify(Task& task) {
task.validate(false);
auto uuid = task.get("uuid");
rust::Vec<tc::Operation> ops;
maybe_add_undo_point(ops);
changes[uuid] = task;
// invoke the hook and allow it to modify the task before updating
@@ -151,14 +130,15 @@ void TDB2::modify(Task& task) {
get(uuid, original);
Context::getContext().hooks.onModify(original, task);
auto maybe_tctask = replica.get_task(uuid);
if (!maybe_tctask.has_value()) {
tc::Uuid tcuuid = tc::uuid_from_string(uuid);
auto maybe_tctask = replica()->get_task_data(tcuuid);
if (maybe_tctask.is_none()) {
throw std::string("task no longer exists");
}
auto tctask = std::move(maybe_tctask.value());
auto guard = replica.mutate_task(tctask);
auto tctask_map = tctask.get_taskmap();
auto tctask = maybe_tctask.take();
// Perform the necessary `update` operations to set all keys in `tctask`
// equal to those in `task`.
std::unordered_set<std::string> seen;
for (auto k : task.all()) {
// ignore task keys that aren't stored
@@ -168,45 +148,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<std::string>(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<tc::Operation> 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<tc::Replica>& 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<tc::WorkingSet>& 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<tc::Operation>& 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<Task>& changes) {
std::map<std::string, Task>& changes_map = this->changes;
@@ -215,168 +226,106 @@ void TDB2::get_changes(std::vector<Task>& changes) {
[](const auto& kv) { return kv.second; });
}
////////////////////////////////////////////////////////////////////////////////
void TDB2::revert() {
auto undo_ops = replica.get_undo_ops();
if (undo_ops.len == 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);
}
replica.rebuild_working_set(false);
}
////////////////////////////////////////////////////////////////////////////////
bool TDB2::confirm_revert(struct tc::ffi::TCReplicaOpList 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 << "- ";
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::cout << "\n";
}
return !Context::getContext().config.getBoolean("confirmation") ||
confirm(
"The undo command is not reversible. Are you sure you want to revert to the previous "
"state?");
}
////////////////////////////////////////////////////////////////////////////////
std::string TDB2::option_string(std::string input) { return input == "" ? "<empty>" : input; }
////////////////////////////////////////////////////////////////////////////////
void TDB2::show_diff(const std::string& current, const std::string& prior,
const std::string& when) {
Datetime lastChange(strtoll(when.c_str(), nullptr, 10));
// Set the colors.
Color color_red(
Context::getContext().color() ? Context::getContext().config.get("color.undo.before") : "");
Color color_green(
Context::getContext().color() ? Context::getContext().config.get("color.undo.after") : "");
auto before = prior == "" ? Task() : Task(prior);
auto after = Task(current);
if (Context::getContext().config.get("undo.style") == "side") {
Table view = before.diffForUndoSide(after);
std::cout << '\n'
<< format("The last modification was made {1}", lastChange.toString()) << '\n'
<< '\n'
<< view.render() << '\n';
}
else if (Context::getContext().config.get("undo.style") == "diff") {
Table view = before.diffForUndoPatch(after, lastChange);
std::cout << '\n' << view.render() << '\n';
}
}
////////////////////////////////////////////////////////////////////////////////
void TDB2::gc() {
Timer timer;
// 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<Task> TDB2::all_tasks() {
auto all_tctasks = replica.all_tasks();
Timer timer;
auto all_tctasks = replica()->all_task_data();
std::vector<Task> 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);
Context::getContext().time_load_us += timer.total_us();
return all;
}
////////////////////////////////////////////////////////////////////////////////
const std::vector<Task> TDB2::pending_tasks() {
const tc::WorkingSet& ws = working_set();
auto largest_index = ws.largest_index();
if (!_pending_tasks) {
Timer timer;
std::vector<Task> 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())));
}
auto pending_tctasks = replica()->pending_task_data();
std::vector<Task> result;
for (auto& maybe_tctask : pending_tctasks) {
auto tctask = maybe_tctask.take();
result.push_back(Task(std::move(tctask)));
}
dependency_scan(result);
Context::getContext().time_load_us += timer.total_us();
_pending_tasks = result;
}
dependency_scan(result);
return result;
return *_pending_tasks;
}
////////////////////////////////////////////////////////////////////////////////
const std::vector<Task> TDB2::completed_tasks() {
auto all_tctasks = replica.all_tasks();
const tc::WorkingSet& ws = working_set();
if (!_completed_tasks) {
auto all_tctasks = replica()->all_task_data();
auto& ws = working_set();
std::vector<Task> result;
for (auto& tctask : all_tctasks) {
// if this task is _not_ in the working set, return it.
if (!ws.by_uuid(tctask.get_uuid())) {
result.push_back(Task(std::move(tctask)));
std::vector<Task> 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<std::string>(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<Task> pending = pending_tasks();
for (auto& pending_task : pending) {
if (pending_task.get("uuid") == uuid) {
task = pending_task;
return true;
}
}
}
@@ -386,22 +335,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<Task> 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<std::string>(tctask->get_uuid().to_string());
if (closeEnough(tctask_uuid, uuid, uuid.length())) {
task = Task{std::move(tctask)};
return true;
}
@@ -446,63 +396,59 @@ const std::vector<Task> TDB2::children(Task& parent) {
std::vector<Task> 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<std::string>(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() {

View File

@@ -30,32 +30,27 @@
#include <FS.h>
#include <Task.h>
#include <stdio.h>
#include <tc/Replica.h>
#include <tc/WorkingSet.h>
#include <taskchampion-cpp/lib.h>
#include <map>
#include <optional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
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 &);
void modify(Task &);
void purge(Task &);
void get_changes(std::vector<Task> &);
void revert();
void gc();
void expire_tasks();
int latest_id();
@@ -79,19 +74,22 @@ class TDB2 {
void dump();
void sync(tc::Server server, bool avoid_snapshots);
bool confirm_revert(struct tc::ffi::TCReplicaOpList);
rust::Box<tc::Replica> &replica();
private:
tc::Replica replica;
std::optional<tc::WorkingSet> _working_set;
std::optional<rust::Box<tc::Replica>> _replica;
// Cached information from the replica
std::optional<rust::Box<tc::WorkingSet>> _working_set;
std::optional<std::vector<Task>> _pending_tasks;
std::optional<std::vector<Task>> _completed_tasks;
void invalidate_cached_info();
// UUID -> Task containing all tasks modified in this invocation.
std::map<std::string, Task> changes;
const tc::WorkingSet &working_set();
static std::string option_string(std::string input);
static void show_diff(const std::string &, const std::string &, const std::string &);
const rust::Box<tc::WorkingSet> &working_set();
void maybe_add_undo_point(rust::Vec<tc::Operation> &);
};
#endif

View File

@@ -138,7 +138,7 @@ Task::Task(const json::object* obj) {
}
////////////////////////////////////////////////////////////////////////////////
Task::Task(tc::Task obj) {
Task::Task(rust::Box<tc::TaskData> 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<tc::TaskData> task) {
auto items = task->items();
data.clear();
for (auto& item : items) {
data[static_cast<std::string>(item.prop)] = static_cast<std::string>(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<std::string>(task->get_uuid().to_string());
id = Context::getContext().tdb2.id(data["uuid"]);
is_blocking = task.is_blocking();
is_blocked = task.is_blocked();
}
////////////////////////////////////////////////////////////////////////////////
@@ -1242,14 +1243,14 @@ void Task::fixTagsAttribute() {
bool Task::isTagAttr(const std::string& attr) { return attr.compare(0, 4, "tag_") == 0; }
////////////////////////////////////////////////////////////////////////////////
const std::string Task::tag2Attr(const std::string& tag) const {
std::string Task::tag2Attr(const std::string& tag) {
std::stringstream tag_attr;
tag_attr << "tag_" << tag;
return tag_attr.str();
}
////////////////////////////////////////////////////////////////////////////////
const std::string Task::attr2Tag(const std::string& attr) const {
std::string Task::attr2Tag(const std::string& attr) {
assert(isTagAttr(attr));
return attr.substr(4);
}
@@ -1270,14 +1271,14 @@ void Task::fixDependsAttribute() {
bool Task::isDepAttr(const std::string& attr) { return attr.compare(0, 4, "dep_") == 0; }
////////////////////////////////////////////////////////////////////////////////
const std::string Task::dep2Attr(const std::string& tag) const {
std::string Task::dep2Attr(const std::string& tag) {
std::stringstream tag_attr;
tag_attr << "dep_" << tag;
return tag_attr.str();
}
////////////////////////////////////////////////////////////////////////////////
const std::string Task::attr2Dep(const std::string& attr) const {
std::string Task::attr2Dep(const std::string& attr) {
assert(isDepAttr(attr));
return attr.substr(4);
}
@@ -1407,13 +1408,29 @@ void Task::substitute(const std::string& from, const std::string& to, const std:
}
#endif
////////////////////////////////////////////////////////////////////////////////
// Validate a task for addition, raising user-visible errors for inconsistent or
// incorrect inputs. This is called before `Task::validate`.
void Task::validate_add() {
// There is no fixing a missing description.
if (!has("description"))
throw std::string("A task must have a description.");
else if (get("description") == "")
throw std::string("Cannot add a task that is blank.");
// Cannot have an old-style recur frequency with no due date - when would it recur?
if (has("recur") && (!has("due") || get("due") == ""))
throw std::string("A recurring task must also have a 'due' date.");
}
////////////////////////////////////////////////////////////////////////////////
// The purpose of Task::validate is three-fold:
// 1) To provide missing attributes where possible
// 2) To provide suitable warnings about odd states
// 3) To generate errors when the inconsistencies are not fixable
// 4) To update status depending on other attributes
// 3) To update status depending on other attributes
//
// As required by TaskChampion, no combination of properties and values is an
// error. This function will try to make sensible defaults and resolve inconsistencies.
// Critically, note that despite the name this is not a read-only function.
//
void Task::validate(bool applyDefault /* = true */) {
@@ -1427,6 +1444,8 @@ void Task::validate(bool applyDefault /* = true */) {
Lexer lex(uid);
std::string token;
Lexer::Type type;
// `uuid` is not a property in the TaskChampion model, so an invalid UUID is
// actually an error.
if (!lex.isUUID(token, type, true)) throw format("Not a valid UUID '{1}'.", uid);
} else
set("uuid", uuid());
@@ -1542,27 +1561,27 @@ void Task::validate(bool applyDefault /* = true */) {
validate_before("scheduled", "due");
validate_before("scheduled", "end");
// 3) To generate errors when the inconsistencies are not fixable
if (!has("description") || get("description") == "")
Context::getContext().footnote(format("Warning: task has no description."));
// There is no fixing a missing description.
if (!has("description"))
throw std::string("A task must have a description.");
else if (get("description") == "")
throw std::string("Cannot add a task that is blank.");
// Cannot have an old-style recur frequency with no due date - when would it recur?
if (has("recur") && (!has("due") || get("due") == "")) {
Context::getContext().footnote(format("Warning: recurring task has no due date."));
remove("recur");
}
// Cannot have a recur frequency with no due date - when would it recur?
if (has("recur") && (!has("due") || get("due") == ""))
throw std::string("A recurring task must also have a 'due' date.");
// Recur durations must be valid.
// Old-style recur durations must be valid.
if (has("recur")) {
std::string value = get("recur");
if (value != "") {
Duration p;
std::string::size_type i = 0;
if (!p.parse(value, i))
if (!p.parse(value, i)) {
// TODO Ideal location to map unsupported old recurrence periods to supported values.
throw format("The recurrence value '{1}' is not valid.", value);
Context::getContext().footnote(
format("Warning: The recurrence value '{1}' is not valid.", value));
remove("recur");
}
}
}
}
@@ -1602,40 +1621,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:
@@ -2167,274 +2152,3 @@ std::string Task::diff(const Task& after) const {
}
////////////////////////////////////////////////////////////////////////////////
// Similar to diff, but formatted for inclusion in the output of the info command
std::string Task::diffForInfo(const Task& after, const std::string& dateformat,
long& last_timestamp, const long current_timestamp) const {
// Attributes are all there is, so figure the different attribute names
// between before and after.
std::vector<std::string> beforeAtts;
for (auto& att : data) beforeAtts.push_back(att.first);
std::vector<std::string> afterAtts;
for (auto& att : after.data) afterAtts.push_back(att.first);
std::vector<std::string> beforeOnly;
std::vector<std::string> afterOnly;
listDiff(beforeAtts, afterAtts, beforeOnly, afterOnly);
// Now start generating a description of the differences.
std::stringstream out;
for (auto& name : beforeOnly) {
if (isAnnotationAttr(name)) {
out << format("Annotation '{1}' deleted.\n", get(name));
} else if (isTagAttr(name)) {
out << format("Tag '{1}' deleted.\n", attr2Tag(name));
} else if (isDepAttr(name)) {
out << format("Dependency on '{1}' deleted.\n", attr2Dep(name));
} else if (name == "depends" || name == "tags") {
// do nothing for legacy attributes
} else if (name == "start") {
Datetime started(get("start"));
Datetime stopped;
if (after.has("end"))
// Task was marked as finished, use end time
stopped = Datetime(after.get("end"));
else
// Start attribute was removed, use modification time
stopped = Datetime(current_timestamp);
out << format("{1} deleted (duration: {2}).", Lexer::ucFirst(name),
Duration(stopped - started).format())
<< "\n";
} else {
out << format("{1} deleted.\n", Lexer::ucFirst(name));
}
}
for (auto& name : afterOnly) {
if (isAnnotationAttr(name)) {
out << format("Annotation of '{1}' added.\n", after.get(name));
} else if (isTagAttr(name)) {
out << format("Tag '{1}' added.\n", attr2Tag(name));
} else if (isDepAttr(name)) {
out << format("Dependency on '{1}' added.\n", attr2Dep(name));
} else if (name == "depends" || name == "tags") {
// do nothing for legacy attributes
} else {
if (name == "start") last_timestamp = current_timestamp;
out << format("{1} set to '{2}'.", Lexer::ucFirst(name),
renderAttribute(name, after.get(name), dateformat))
<< "\n";
}
}
for (auto& name : beforeAtts)
if (name != "uuid" && name != "modified" && get(name) != after.get(name) && get(name) != "" &&
after.get(name) != "") {
if (name == "depends" || name == "tags") {
// do nothing for legacy attributes
} else if (isTagAttr(name) || isDepAttr(name)) {
// ignore new attributes
} else if (isAnnotationAttr(name)) {
out << format("Annotation changed to '{1}'.\n", after.get(name));
} else
out << format("{1} changed from '{2}' to '{3}'.", Lexer::ucFirst(name),
renderAttribute(name, get(name), dateformat),
renderAttribute(name, after.get(name), dateformat))
<< "\n";
}
// Shouldn't just say nothing.
if (out.str().length() == 0) out << "No changes made.\n";
return out.str();
}
////////////////////////////////////////////////////////////////////////////////
// Similar to diff, but formatted as a side-by-side table for an Undo preview
Table Task::diffForUndoSide(const Task& after) const {
// Set the colors.
Color color_red(
Context::getContext().color() ? Context::getContext().config.get("color.undo.before") : "");
Color color_green(
Context::getContext().color() ? Context::getContext().config.get("color.undo.after") : "");
// Attributes are all there is, so figure the different attribute names
// between before and after.
Table view;
view.width(Context::getContext().getWidth());
view.intraPadding(2);
view.add("");
view.add("Prior Values");
view.add("Current Values");
setHeaderUnderline(view);
if (!is_empty()) {
const Task& before = *this;
std::vector<std::string> beforeAtts;
for (auto& att : before.data) beforeAtts.push_back(att.first);
std::vector<std::string> afterAtts;
for (auto& att : after.data) afterAtts.push_back(att.first);
std::vector<std::string> beforeOnly;
std::vector<std::string> afterOnly;
listDiff(beforeAtts, afterAtts, beforeOnly, afterOnly);
int row;
for (auto& name : beforeOnly) {
row = view.addRow();
view.set(row, 0, name);
view.set(row, 1, renderAttribute(name, before.get(name)), color_red);
}
for (auto& att : before.data) {
std::string priorValue = before.get(att.first);
std::string currentValue = after.get(att.first);
if (currentValue != "") {
row = view.addRow();
view.set(row, 0, att.first);
view.set(row, 1, renderAttribute(att.first, priorValue),
(priorValue != currentValue ? color_red : Color()));
view.set(row, 2, renderAttribute(att.first, currentValue),
(priorValue != currentValue ? color_green : Color()));
}
}
for (auto& name : afterOnly) {
row = view.addRow();
view.set(row, 0, name);
view.set(row, 2, renderAttribute(name, after.get(name)), color_green);
}
} else {
int row;
for (auto& att : after.data) {
row = view.addRow();
view.set(row, 0, att.first);
view.set(row, 2, renderAttribute(att.first, after.get(att.first)), color_green);
}
}
return view;
}
////////////////////////////////////////////////////////////////////////////////
// Similar to diff, but formatted as a diff for an Undo preview
Table Task::diffForUndoPatch(const Task& after, const Datetime& lastChange) const {
// This style looks like this:
// --- before 2009-07-04 00:00:25.000000000 +0200
// +++ after 2009-07-04 00:00:45.000000000 +0200
//
// - name: old // att deleted
// + name:
//
// - name: old // att changed
// + name: new
//
// - name:
// + name: new // att added
//
// Set the colors.
Color color_red(
Context::getContext().color() ? Context::getContext().config.get("color.undo.before") : "");
Color color_green(
Context::getContext().color() ? Context::getContext().config.get("color.undo.after") : "");
const Task& before = *this;
// Generate table header.
Table view;
view.width(Context::getContext().getWidth());
view.intraPadding(2);
view.add("");
view.add("");
int row = view.addRow();
view.set(row, 0, "--- previous state", color_red);
view.set(row, 1, "Undo will restore this state", color_red);
row = view.addRow();
view.set(row, 0, "+++ current state ", color_green);
view.set(row, 1,
format("Change made {1}",
lastChange.toString(Context::getContext().config.get("dateformat"))),
color_green);
view.addRow();
// Add rows to table showing diffs.
std::vector<std::string> all = Context::getContext().getColumns();
// Now factor in the annotation attributes.
for (auto& it : before.data)
if (it.first.substr(0, 11) == "annotation_") all.push_back(it.first);
for (auto& it : after.data)
if (it.first.substr(0, 11) == "annotation_") all.push_back(it.first);
// Now render all the attributes.
std::sort(all.begin(), all.end());
std::string before_att;
std::string after_att;
std::string last_att;
for (auto& a : all) {
if (a != last_att) // Skip duplicates.
{
last_att = a;
before_att = before.get(a);
after_att = after.get(a);
// Don't report different uuid.
// Show nothing if values are the unchanged.
if (a == "uuid" || before_att == after_att) {
// Show nothing - no point displaying that which did not change.
// row = view.addRow ();
// view.set (row, 0, *a + ":");
// view.set (row, 1, before_att);
}
// Attribute deleted.
else if (before_att != "" && after_att == "") {
row = view.addRow();
view.set(row, 0, '-' + a + ':', color_red);
view.set(row, 1, before_att, color_red);
row = view.addRow();
view.set(row, 0, '+' + a + ':', color_green);
}
// Attribute added.
else if (before_att == "" && after_att != "") {
row = view.addRow();
view.set(row, 0, '-' + a + ':', color_red);
row = view.addRow();
view.set(row, 0, '+' + a + ':', color_green);
view.set(row, 1, after_att, color_green);
}
// Attribute changed.
else {
row = view.addRow();
view.set(row, 0, '-' + a + ':', color_red);
view.set(row, 1, before_att, color_red);
row = view.addRow();
view.set(row, 0, '+' + a + ':', color_green);
view.set(row, 1, after_att, color_green);
}
}
}
return view;
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -31,7 +31,7 @@
#include <JSON.h>
#include <Table.h>
#include <stdio.h>
#include <tc/Task.h>
#include <taskchampion-cpp/lib.h>
#include <time.h>
#include <map>
@@ -66,7 +66,7 @@ class Task {
bool operator!=(const Task&);
Task(const std::string&);
Task(const json::object*);
Task(tc::Task);
Task(rust::Box<tc::TaskData>);
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;
@@ -166,6 +164,12 @@ class Task {
void substitute(const std::string&, const std::string&, const std::string&);
#endif
static std::string tag2Attr(const std::string&);
static std::string attr2Tag(const std::string&);
static std::string dep2Attr(const std::string&);
static std::string attr2Dep(const std::string&);
void validate_add();
void validate(bool applyDefault = true);
float urgency_c() const;
@@ -177,24 +181,16 @@ class Task {
#endif
std::string diff(const Task& after) const;
std::string diffForInfo(const Task& after, const std::string& dateformat, long& last_timestamp,
const long current_timestamp) const;
Table diffForUndoSide(const Task& after) const;
Table diffForUndoPatch(const Task& after, const Datetime& lastChange) const;
private:
int determineVersion(const std::string&);
void parseJSON(const std::string&);
void parseJSON(const json::object*);
void parseTC(const tc::Task&);
void parseTC(rust::Box<tc::TaskData>);
void parseLegacy(const std::string&);
void validate_before(const std::string&, const std::string&);
const std::string encode(const std::string&) const;
const std::string decode(const std::string&) const;
const std::string tag2Attr(const std::string&) const;
const std::string attr2Tag(const std::string&) const;
const std::string dep2Attr(const std::string&) const;
const std::string attr2Dep(const std::string&) const;
void fixDependsAttribute();
void fixTagsAttribute();

View File

@@ -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")

View File

@@ -190,7 +190,11 @@ void ColumnTypeDate::modify(Task& task, const std::string& value) {
if (value != "" && evaluatedValue.get_date() == 0)
throw format("'{1}' is not a valid date in the '{2}' format.", value, Variant::dateFormat);
task.set(_name, evaluatedValue.get_date());
time_t epoch = evaluatedValue.get_date();
if (epoch < EPOCH_MIN_VALUE || epoch >= EPOCH_MAX_VALUE) {
throw format("'{1}' is not a valid date.", value);
}
task.set(_name, epoch);
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -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")

View File

@@ -404,12 +404,7 @@ int CmdCalendar::execute(std::string& output) {
std::string CmdCalendar::renderMonths(int firstMonth, int firstYear, const Datetime& today,
std::vector<Task>& all, int monthsPerLine) {
auto& config = Context::getContext().config;
// What day of the week does the user consider the first?
auto weekStart = Datetime::dayOfWeek(config.get("weekstart"));
if (weekStart != 0 && weekStart != 1)
throw std::string(
"The 'weekstart' configuration variable may only contain 'Sunday' or 'Monday'.");
auto weekStart = Datetime::weekstart;
// Build table for the number of months to be displayed.
Table view;

View File

@@ -95,7 +95,9 @@ bool CmdConfig::setConfigVariable(const std::string& name, const std::string& va
change = true;
}
if (change) File::write(Context::getContext().config.file(), contents);
if (change)
if (!File::write(Context::getContext().config.file(), contents))
throw format("Could not write to '{1}'.", Context::getContext().config.file());
return change;
}
@@ -133,7 +135,9 @@ int CmdConfig::unsetConfigVariable(const std::string& name, bool confirmation /*
if (!lineDeleted) line++;
}
if (change) File::write(Context::getContext().config.file(), contents);
if (change)
if (!File::write(Context::getContext().config.file(), contents))
throw format("Could not write to '{1}'.", Context::getContext().config.file());
if (change && found)
return 0;

View File

@@ -174,6 +174,9 @@ void CmdImport::importSingleTask(json::object* obj) {
// Parse the whole thing, validate the data.
Task task(obj);
// An empty task is probably not intentional - at least a UUID should be included.
if (task.is_empty()) throw format("Cannot import an empty task.");
auto hasGeneratedEntry = not task.has("entry");
auto hasExplicitEnd = task.has("end");

View File

@@ -31,6 +31,7 @@
#include <JSON.h>
#include <string>
#include <unordered_map>
class CmdImport : public Command {
public:

View File

@@ -33,13 +33,16 @@
#include <Duration.h>
#include <Filter.h>
#include <Lexer.h>
#include <Operation.h>
#include <format.h>
#include <main.h>
#include <math.h>
#include <shared.h>
#include <stdlib.h>
#include <taskchampion-cpp/lib.h>
#include <util.h>
#include <algorithm>
#include <sstream>
////////////////////////////////////////////////////////////////////////////////
@@ -373,7 +376,7 @@ int CmdInfo::execute(std::string& output) {
// Show any orphaned UDAs, which are identified by not being represented in
// the context.columns map.
for (auto& att : all) {
if (att.substr(0, 11) != "annotation_" && att.substr(0, 5) != "tags_" &&
if (att.substr(0, 11) != "annotation_" && att.substr(0, 4) != "tag_" &&
att.substr(0, 4) != "dep_" &&
Context::getContext().columns.find(att) == Context::getContext().columns.end()) {
row = view.addRow();
@@ -477,9 +480,68 @@ int CmdInfo::execute(std::string& output) {
urgencyDetails.set(row, 5, rightJustify(format(task.urgency(), 4, 4), 6));
}
// Create a third table, containing undo log change details.
Table journal;
setHeaderUnderline(journal);
if (Context::getContext().config.getBoolean("obfuscate")) journal.obfuscate();
if (Context::getContext().config.getBoolean("color")) journal.forceColor();
journal.width(Context::getContext().getWidth());
journal.add("Date");
journal.add("Modification");
if (Context::getContext().config.getBoolean("journal.info")) {
auto& replica = Context::getContext().tdb2.replica();
tc::Uuid tcuuid = tc::uuid_from_string(uuid);
auto tcoperations = replica->get_task_operations(tcuuid);
auto operations = Operation::operations(tcoperations);
// Sort by type (Create < Update < Delete < UndoPoint) and then by timestamp.
std::sort(operations.begin(), operations.end());
long last_timestamp = 0;
for (size_t i = 0; i < operations.size(); i++) {
auto& op = operations[i];
// Only display updates -- creation and deletion aren't interesting.
if (!op.is_update()) {
continue;
}
// Group operations that occur within 1s of this one. This is a heuristic
// for operations performed in the same `task` invocation, and allows e.g.,
// `task done end:-2h` to take the updated `end` value into account. It also
// groups these events into a single "row" of the table for better layout.
size_t group_start = i;
for (i++; i < operations.size(); i++) {
auto& op2 = operations[i];
if (!op2.is_update() || op2.get_timestamp() - op.get_timestamp() > 1) {
break;
}
}
size_t group_end = i;
i--;
std::optional<std::string> msg =
formatForInfo(operations, group_start, group_end, dateformat, last_timestamp);
if (!msg) {
continue;
}
int row = journal.addRow();
Datetime timestamp(op.get_timestamp());
journal.set(row, 0, timestamp.toString(dateformat));
journal.set(row, 1, *msg);
}
}
out << optionalBlankLine() << view.render() << '\n';
if (urgencyDetails.rows() > 0) out << urgencyDetails.render() << '\n';
if (journal.rows() > 0) out << journal.render() << '\n';
}
output = out.str();
@@ -502,3 +564,105 @@ void CmdInfo::urgencyTerm(Table& view, const std::string& label, float measure,
}
////////////////////////////////////////////////////////////////////////////////
std::optional<std::string> CmdInfo::formatForInfo(const std::vector<Operation>& operations,
size_t group_start, size_t group_end,
const std::string& dateformat, long& last_start) {
std::stringstream out;
for (auto i = group_start; i < group_end; i++) {
auto& operation = operations[i];
assert(operation.is_update());
// Extract the parts of the Update operation.
std::string prop = operation.get_property();
std::optional<std::string> value = operation.get_value();
std::optional<std::string> old_value = operation.get_old_value();
Datetime timestamp(operation.get_timestamp());
// Never care about modifying the modification time, or the legacy properties `depends` and
// `tags`.
if (prop == "modified" || prop == "depends" || prop == "tags") {
continue;
}
// Handle property deletions
if (!value && old_value) {
if (Task::isAnnotationAttr(prop)) {
out << format("Annotation '{1}' deleted.\n", *old_value);
} else if (Task::isTagAttr(prop)) {
out << format("Tag '{1}' deleted.\n", Task::attr2Tag(prop));
} else if (Task::isDepAttr(prop)) {
out << format("Dependency on '{1}' deleted.\n", Task::attr2Dep(prop));
} else if (prop == "start") {
Datetime started(last_start);
Datetime stopped = timestamp;
// If any update in this group sets the `end` property, use that instead of the
// timestamp deleting the `start` property as the stop time.
// See https://github.com/GothenburgBitFactory/taskwarrior/issues/2514
for (auto i = group_start; i < group_end; i++) {
auto& op = operations[i];
assert(op.is_update());
if (op.get_property() == "end") {
try {
stopped = op.get_value().value();
} catch (std::string) {
// Fall back to the 'start' timestamp if its value is un-parseable.
stopped = op.get_timestamp();
}
}
}
out << format("{1} deleted (duration: {2}).", Lexer::ucFirst(prop),
Duration(stopped - started).format())
<< "\n";
} else {
out << format("{1} deleted.\n", Lexer::ucFirst(prop));
}
}
// Handle property additions.
if (value && !old_value) {
if (Task::isAnnotationAttr(prop)) {
out << format("Annotation of '{1}' added.\n", *value);
} else if (Task::isTagAttr(prop)) {
out << format("Tag '{1}' added.\n", Task::attr2Tag(prop));
} else if (Task::isDepAttr(prop)) {
out << format("Dependency on '{1}' added.\n", Task::attr2Dep(prop));
} else {
// Record the last start time for later duration calculation.
if (prop == "start") {
last_start = Datetime(value.value()).toEpoch();
}
out << format("{1} set to '{2}'.", Lexer::ucFirst(prop),
renderAttribute(prop, *value, dateformat))
<< "\n";
}
}
// Handle property changes.
if (value && old_value) {
if (Task::isTagAttr(prop) || Task::isDepAttr(prop)) {
// Dependencies and tags do not have meaningful values.
} else if (Task::isAnnotationAttr(prop)) {
out << format("Annotation changed to '{1}'.\n", *value);
} else {
// Record the last start time for later duration calculation.
if (prop == "start") {
last_start = Datetime(value.value()).toEpoch();
}
out << format("{1} changed from '{2}' to '{3}'.", Lexer::ucFirst(prop),
renderAttribute(prop, *old_value, dateformat),
renderAttribute(prop, *value, dateformat))
<< "\n";
}
}
}
if (out.str().length() == 0) return std::nullopt;
return out.str();
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -28,9 +28,12 @@
#define INCLUDED_CMDINFO
#include <Command.h>
#include <Operation.h>
#include <Table.h>
#include <taskchampion-cpp/lib.h>
#include <string>
#include <vector>
class CmdInfo : public Command {
public:
@@ -39,6 +42,10 @@ class CmdInfo : public Command {
private:
void urgencyTerm(Table&, const std::string&, float, float) const;
// Format a group of update operations for display in `task info`.
std::optional<std::string> formatForInfo(const std::vector<Operation>& operations,
size_t group_start, size_t group_end,
const std::string& dateformat, long& last_start);
};
#endif

View File

@@ -116,6 +116,11 @@ void CmdModify::checkConsistency(Task &before, Task &after) {
if (before.has("recur") && (!after.has("recur") || after.get("recur") == ""))
throw std::string("You cannot remove the recurrence from a recurring task.");
if ((before.getStatus() == Task::pending) && (after.getStatus() == Task::pending) &&
(after.get("end") != ""))
throw format("Could not modify task {1}. You cannot set an end date on a pending task.",
before.identifier(true));
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -158,6 +158,7 @@ std::vector<NewsItem> NewsItem::all() {
version2_6_0(items);
version3_0_0(items);
version3_1_0(items);
version3_2_0(items);
return items;
}
@@ -500,6 +501,20 @@ void NewsItem::version3_1_0(std::vector<NewsItem>& items) {
items.push_back(news);
}
void NewsItem::version3_2_0(std::vector<NewsItem>& items) {
Version version("3.2.0");
NewsItem info{
version,
/*title=*/"`task info` Journal Restored",
/*bg_title=*/"",
/*background=*/"",
/*punchline=*/"",
/*update=*/
"Support for the \"journal\" output in `task info` has been restored. The command now\n"
"displays a list of changes made to the task, with timestamps.\n\n"};
items.push_back(info);
}
////////////////////////////////////////////////////////////////////////////////
int CmdNews::execute(std::string& output) {
auto words = Context::getContext().cli2.getWords();

View File

@@ -51,6 +51,7 @@ class NewsItem {
static void version2_6_0(std::vector<NewsItem>&);
static void version3_0_0(std::vector<NewsItem>&);
static void version3_1_0(std::vector<NewsItem>&);
static void version3_2_0(std::vector<NewsItem>&);
private:
NewsItem(Version, const std::string&, const std::string& = "", const std::string& = "",

View File

@@ -202,7 +202,6 @@ int CmdShow::execute(std::string& output) {
" sync.server.url"
" sync.server.origin"
" tag.indicator"
" undo.style"
" urgency.active.coefficient"
" urgency.scheduled.coefficient"
" urgency.annotations.coefficient"

View File

@@ -35,12 +35,11 @@
#include <inttypes.h>
#include <shared.h>
#include <signal.h>
#include <taskchampion-cpp/lib.h>
#include <util.h>
#include <sstream>
#include "tc/Server.h"
////////////////////////////////////////////////////////////////////////////////
CmdSync::CmdSync() {
_keyword = "synchronize";
@@ -60,56 +59,53 @@ CmdSync::CmdSync() {
int CmdSync::execute(std::string& output) {
int status = 0;
tc::Server server;
std::string server_ident;
Context& context = Context::getContext();
auto& replica = context.tdb2.replica();
std::stringstream out;
bool avoid_snapshots = false;
bool verbose = Context::getContext().verbose("sync");
// If no server is set up, quit.
std::string origin = Context::getContext().config.get("sync.server.origin");
std::string url = Context::getContext().config.get("sync.server.url");
std::string server_dir = Context::getContext().config.get("sync.local.server_dir");
std::string client_id = Context::getContext().config.get("sync.server.client_id");
std::string gcp_credential_path = Context::getContext().config.get("sync.gcp.credential_path");
std::string gcp_bucket = Context::getContext().config.get("sync.gcp.bucket");
std::string encryption_secret = Context::getContext().config.get("sync.encryption_secret");
// sync.server.origin is a deprecated synonym for sync.server.url
std::string server_url = url == "" ? origin : url;
if (server_dir != "") {
server = tc::Server::new_local(server_dir);
server_ident = server_dir;
} else if (gcp_bucket != "") {
if (encryption_secret == "") {
throw std::string("sync.encryption_secret is required");
}
server = tc::Server::new_gcp(gcp_bucket, gcp_credential_path, encryption_secret);
std::ostringstream os;
os << "GCP bucket " << gcp_bucket;
server_ident = os.str();
} else if (server_url != "") {
std::string client_id = Context::getContext().config.get("sync.server.client_id");
if (client_id == "" || encryption_secret == "") {
throw std::string("sync.server.client_id and sync.encryption_secret are required");
}
server = tc::Server::new_sync(server_url, client_id, encryption_secret);
std::ostringstream os;
os << "Sync server at " << server_url;
server_ident = os.str();
} else {
throw std::string("No sync.* settings are configured. See task-sync(5).");
}
std::stringstream out;
if (origin != "") {
out << "sync.server.origin is deprecated. Use sync.server.url instead.\n";
}
if (Context::getContext().verbose("sync")) {
out << format("Syncing with {1}", server_ident) << '\n';
if (server_dir != "") {
if (verbose) {
out << format("Syncing with {1}", server_dir) << '\n';
}
replica->sync_to_local(server_dir, avoid_snapshots);
} else if (gcp_bucket != "") {
if (encryption_secret == "") {
throw std::string("sync.encryption_secret is required");
}
if (verbose) {
out << format("Syncing with GCP bucket {1}", gcp_bucket) << '\n';
}
replica->sync_to_gcp(gcp_bucket, gcp_credential_path, encryption_secret, avoid_snapshots);
} else if (server_url != "") {
if (client_id == "" || encryption_secret == "") {
throw std::string("sync.server.client_id and sync.encryption_secret are required");
}
if (verbose) {
out << format("Syncing with sync server at {1}", server_url) << '\n';
}
replica->sync_to_remote(server_url, tc::uuid_from_string(client_id), encryption_secret,
avoid_snapshots);
} else {
throw std::string("No sync.* settings are configured. See task-sync(5).");
}
Context& context = Context::getContext();
context.tdb2.sync(std::move(server), false);
if (context.config.getBoolean("purge.on-sync")) {
context.tdb2.expire_tasks();
}

View File

@@ -29,6 +29,13 @@
#include <CmdUndo.h>
#include <Context.h>
#include <Operation.h>
#include <Task.h>
#include <iostream>
#include <sstream>
#include "shared.h"
////////////////////////////////////////////////////////////////////////////////
CmdUndo::CmdUndo() {
@@ -47,8 +54,94 @@ CmdUndo::CmdUndo() {
////////////////////////////////////////////////////////////////////////////////
int CmdUndo::execute(std::string&) {
Context::getContext().tdb2.revert();
auto& replica = Context::getContext().tdb2.replica();
rust::Vec<tc::Operation> undo_ops = replica->get_undo_operations();
if (confirm_revert(Operation::operations(undo_ops))) {
// Note that commit_reversed_operations rebuilds the working set, so that
// need not be done here.
if (!replica->commit_reversed_operations(std::move(undo_ops))) {
std::cout << "Could not undo: other operations have occurred.";
}
}
return 0;
}
////////////////////////////////////////////////////////////////////////////////
bool CmdUndo::confirm_revert(const std::vector<Operation>& undo_ops) {
// Count non-undo operations
int ops_count = 0;
for (auto& op : undo_ops) {
if (!op.is_undo_point()) {
ops_count++;
}
}
if (ops_count == 0) {
std::cout << "No operations to undo.\n";
return true;
}
std::cout << "The following " << ops_count << " operations would be reverted:\n";
Table view;
if (Context::getContext().config.getBoolean("obfuscate")) view.obfuscate();
view.width(Context::getContext().getWidth());
view.add("Uuid");
view.add("Modification");
std::string last_uuid;
std::stringstream mods;
for (auto& op : undo_ops) {
if (op.is_undo_point()) {
continue;
}
if (last_uuid != op.get_uuid()) {
if (last_uuid.size() != 0) {
int row = view.addRow();
view.set(row, 0, last_uuid);
view.set(row, 1, mods.str());
}
last_uuid = op.get_uuid();
mods.clear();
}
if (op.is_create()) {
mods << "Create task\n";
} else if (op.is_delete()) {
mods << "Delete (purge) task";
} else if (op.is_update()) {
auto property = op.get_property();
auto old_value = op.get_old_value();
auto value = op.get_value();
if (Task::isTagAttr(property)) {
if (value && *value == "x") {
mods << "Add tag '" << Task::attr2Tag(property) << "'\n";
continue;
} else if (!value && old_value && *old_value == "x") {
mods << "Remove tag '" << Task::attr2Tag(property) << "'\n";
continue;
}
}
if (old_value && value) {
mods << "Update property '" << property << "' from '" << *old_value << "' to '" << *value
<< "'\n";
} else if (old_value) {
mods << "Delete property '" << property << "' (was '" << *old_value << "')\n";
} else if (value) {
mods << "Add property '" << property << "' with value '" << *value << "'\n";
}
}
}
int row = view.addRow();
view.set(row, 0, last_uuid);
view.set(row, 1, mods.str());
std::cout << view.render() << "\n";
return !Context::getContext().config.getBoolean("confirmation") ||
confirm(
"The undo command is not reversible. Are you sure you want to revert to the previous "
"state?");
return true;
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -28,13 +28,18 @@
#define INCLUDED_CMDUNDO
#include <Command.h>
#include <Operation.h>
#include <string>
#include <vector>
class CmdUndo : public Command {
public:
CmdUndo();
int execute(std::string&);
int execute(std::string &);
private:
bool confirm_revert(const std::vector<Operation> &);
};
#endif

View File

@@ -28,6 +28,8 @@
// cmake.h include header must come first
#include <Context.h>
#include <rust/cxx.h>
#include <signal.h>
#include <cstring>
#include <iostream>
@@ -38,6 +40,10 @@
int main(int argc, const char** argv) {
int status{0};
// Ignore SIGPIPE from writes to network sockets after the remote end has hung
// up. Rust code expects this, and the Rust runtime ignores this signal at startup.
signal(SIGPIPE, SIG_IGN);
Context globalContext;
Context::setContext(&globalContext);
@@ -55,6 +61,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;

View File

@@ -0,0 +1,29 @@
cmake_minimum_required (VERSION 3.22)
OPTION(SYSTEM_CORROSION "Use system provided corrosion instead of vendored version" OFF)
if(SYSTEM_CORROSION)
find_package(Corrosion REQUIRED)
else()
add_subdirectory(${CMAKE_SOURCE_DIR}/src/taskchampion-cpp/corrosion)
endif()
OPTION (ENABLE_TLS_NATIVE_ROOTS "Use the system's TLS root certificates" OFF)
if (ENABLE_TLS_NATIVE_ROOTS)
message ("Enabling native TLS roots")
set(TASKCHAMPION_FEATURES "tls-native-roots")
endif (ENABLE_TLS_NATIVE_ROOTS)
# 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"
FEATURES "${TASKCHAMPION_FEATURES}")
# Set up `taskchampion-cpp`, the C++ side of the bridge.
corrosion_add_cxxbridge(taskchampion-cpp
CRATE taskchampion_lib
FILES lib.rs
)

View File

@@ -0,0 +1,19 @@
[package]
name = "taskchampion-lib"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["staticlib"]
[dependencies]
taskchampion = "0.9.0"
cxx = "1.0.124"
[features]
# use native CA roots, instead of bundled
tls-native-roots = ["taskchampion/tls-native-roots"]
[build-dependencies]
cxx-build = "1.0"

View File

@@ -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");
}

File diff suppressed because it is too large Load Diff

View File

@@ -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)

View File

@@ -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>
// cmake.h include header must come first
#include <format.h>
#include <iostream>
#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::Task> 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::Task> tc::Replica::all_tasks() {
TCTaskList tasks = tc_replica_all_tasks(&*inner);
if (!tasks.items) {
throw replica_error();
}
std::vector<Task> 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;
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -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 <functional>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#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<tc::ffi::TCReplica, std::function<void(tc::ffi::TCReplica *)>>;
// 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<tc::Task> all_tasks();
// TODO: struct TCUuidList tc_replica_all_task_uuids(struct TCReplica *rep);
tc::WorkingSet working_set();
std::optional<tc::Task> 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
////////////////////////////////////////////////////////////////////////////////

View File

@@ -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>
// cmake.h include header must come first
#include <format.h>
#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;
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -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 <functional>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#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<tc::ffi::TCServer, std::function<void(tc::ffi::TCServer *)>>;
// 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
////////////////////////////////////////////////////////////////////////////////

View File

@@ -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>
// cmake.h include header must come first
#include <assert.h>
#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<std::string, std::string> tc::Task::get_taskmap() const {
TCKVList kv = tc_task_get_taskmap(&*inner);
if (!kv.items) {
throw task_error();
}
std::map<std::string, std::string> 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<std::string> 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<std::string> 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;
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -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 <functional>
#include <map>
#include <memory>
#include <optional>
#include <string>
#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<tc::ffi::TCTask, std::function<void(tc::ffi::TCTask *)>>;
// 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<std::string, std::string> get_taskmap() const;
std::string get_description() const;
std::optional<std::string> 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<std::string> 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
////////////////////////////////////////////////////////////////////////////////

View File

@@ -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>
// cmake.h include header must come first
#include <format.h>
#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<std::string> 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<size_t> 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;
}
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -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 <functional>
#include <memory>
#include <optional>
#include <string>
#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<tc::ffi::TCWorkingSet, std::function<void(tc::ffi::TCWorkingSet *)>>;
// 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<std::string> by_index(size_t index) const noexcept; // tc_working_set_by_index
std::optional<size_t> by_uuid(const std::string &index) const noexcept; // tc_working_set_by_uuid
private:
unique_tcws_ptr inner;
};
} // namespace tc
#endif
////////////////////////////////////////////////////////////////////////////////

View File

@@ -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 <taskchampion.h>
}
#endif
////////////////////////////////////////////////////////////////////////////////

View File

@@ -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

View File

@@ -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<Utc>, 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);
}
}

View File

@@ -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<DateTime<Utc>> to a libc::time_t, or zero if not set.
impl PassByValue for libc::time_t {
type RustType = Option<DateTime<Utc>>;
unsafe fn from_ctype(self) -> Option<DateTime<Utc>> {
if self != 0 {
return Some(utc_timestamp(self));
}
None
}
fn as_ctype(arg: Option<DateTime<Utc>>) -> libc::time_t {
arg.map(|ts| ts.timestamp() as libc::time_t)
.unwrap_or(0 as libc::time_t)
}
}

View File

@@ -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);
}
}

View File

@@ -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 <stdbool.h>
/// #include <stdint.h>
/// #include <time.h>
///
/// #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()
}

View File

@@ -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<RustString<'static>>,
}
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<Replica> 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<T, F>(rep: *mut TCReplica, f: F, err_value: T) -> T
where
F: FnOnce(&mut Replica) -> anyhow::Result<T>,
{
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<T, F>(f: F, error_out: *mut TCString, err_value: T) -> T
where
F: FnOnce() -> anyhow::Result<T>,
{
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<ReplicaOp>,
}
impl From<ReplicaOp> 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<ReplicaOp> = 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);
}
}

View File

@@ -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,
}

View File

@@ -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<dyn Server>);
impl PassByPointer for TCServer {}
impl From<Box<dyn Server>> for TCServer {
fn from(server: Box<dyn Server>) -> TCServer {
TCServer(server)
}
}
impl AsMut<Box<dyn Server>> for TCServer {
fn as_mut(&mut self) -> &mut Box<dyn Server> {
&mut self.0
}
}
/// Utility function to allow using `?` notation to return an error value.
fn wrap<T, F>(f: F, error_out: *mut TCString, err_value: T) -> T
where
F: FnOnce() -> anyhow::Result<T>,
{
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);
}

View File

@@ -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<TCStatus> 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<Status> 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"));
}
}

View File

@@ -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<u8>),
}
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<String, std::str::Utf8Error> {
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<PathBuf, std::str::Utf8Error> {
#[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<String> 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<T, F>(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
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<Element>`.
///
/// The element type can be PassByValue or PassByPointer. If the latter, it should use either
/// `NonNull<T>` or `Option<NonNull<T>>` 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<CL, T>(list: *mut CL)
where
CL: CList<Element = T>,
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<CL, T>(list: *mut CL)
where
CL: CList<Element = NonNull<T>>,
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<CL, T>(list: *mut CL)
where
CL: CList<Element = Option<NonNull<T>>>,
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<CL, T>(
list: *mut CL,
index: usize,
) -> Option<NonNull<T>>
where
CL: CList<Element = Option<NonNull<T>>>,
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<A> PassByValue for A
where
A: CList,
{
type RustType = Vec<A::Element>;
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) }
}
}

View File

@@ -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<RustString<'static>>,
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);
}
}

View File

@@ -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<T>(vec: Vec<T>) -> (*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())
}

View File

@@ -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);
}
}

View File

@@ -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<WorkingSet> for TCWorkingSet {
fn from(ws: WorkingSet) -> TCWorkingSet {
TCWorkingSet(ws)
}
}
/// Utility function to get a shared reference to the underlying WorkingSet.
fn wrap<T, F>(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);
}

View File

@@ -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 <stdbool.h>
#include <stdint.h>
#include <time.h>
#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 */

View File

@@ -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 <string>
#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
////////////////////////////////////////////////////////////////////////////////

View File

@@ -1,67 +1,73 @@
cmake_minimum_required (VERSION 3.22)
# -- C++ tests
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
${CMAKE_SOURCE_DIR}/test
${TASK_INCLUDE_DIRS})
set (test_SRCS
col.test.cpp
dom.test.cpp
eval.test.cpp
lexer.test.cpp
t.test.cpp
tw-2689.test.cpp
tdb2.test.cpp
tc.test.cpp
util.test.cpp
variant_add.test.cpp
variant_and.test.cpp
variant_cast.test.cpp
variant_divide.test.cpp
variant_equal.test.cpp
variant_exp.test.cpp
variant_gt.test.cpp
variant_gte.test.cpp
variant_inequal.test.cpp
variant_lt.test.cpp
variant_lte.test.cpp
variant_match.test.cpp
variant_math.test.cpp
variant_modulo.test.cpp
variant_multiply.test.cpp
variant_nomatch.test.cpp
variant_not.test.cpp
variant_or.test.cpp
variant_partial.test.cpp
variant_subtract.test.cpp
variant_xor.test.cpp
view.test.cpp
# All C++ test files. Note that the portion before `.cpp` must be a valid,
# unique C++ identifier.
set(test_SRCS
col_test.cpp
dom_test.cpp
eval_test.cpp
lexer_test.cpp
t_test.cpp
tw_2689_test.cpp
tdb2_test.cpp
tc_cpp_test.cpp
util_test.cpp
variant_add_test.cpp
variant_and_test.cpp
variant_cast_test.cpp
variant_divide_test.cpp
variant_equal_test.cpp
variant_exp_test.cpp
variant_gt_test.cpp
variant_gte_test.cpp
variant_inequal_test.cpp
variant_lt_test.cpp
variant_lte_test.cpp
variant_match_test.cpp
variant_math_test.cpp
variant_modulo_test.cpp
variant_multiply_test.cpp
variant_nomatch_test.cpp
variant_not_test.cpp
variant_or_test.cpp
variant_partial_test.cpp
variant_subtract_test.cpp
variant_xor_test.cpp
view_test.cpp
)
add_custom_target (build_tests DEPENDS ${test_SRCS}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/test)
# Build `test_runner` containing all CPP tests, linked once.
create_test_sourcelist (cpp_test_SRCS cpp_tests.cpp ${test_SRCS})
add_executable(test_runner
test.cpp
${cpp_test_SRCS}
)
target_link_libraries (test_runner task commands columns libshared task commands columns libshared task commands columns libshared ${TASK_LIBRARIES})
if (DARWIN)
target_link_libraries (test_runner "-framework CoreFoundation -framework Security -framework SystemConfiguration")
endif (DARWIN)
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})
add_dependencies (${src_FILE} task_executable)
if (DARWIN)
target_link_libraries (${src_FILE} "-framework CoreFoundation -framework Security -framework SystemConfiguration")
endif (DARWIN)
add_test(NAME ${src_FILE}
COMMAND ${src_FILE}
foreach (test_FILE ${test_SRCS})
get_filename_component (test_NAME ${test_FILE} NAME_WE)
# Tell the source file what its own name is
set_source_files_properties(${test_FILE} PROPERTIES COMPILE_FLAGS -DTEST_NAME=${test_NAME})
add_test(NAME ${test_FILE}
COMMAND test_runner ${test_NAME}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
endforeach (src_FILE)
)
endforeach (test_FILE)
configure_file(bash_tap.sh bash_tap.sh COPYONLY)
configure_file(bash_tap_tw.sh bash_tap_tw.sh COPYONLY)
# -- Python tests
add_subdirectory(basetest)
add_subdirectory(simpletap)
@@ -168,6 +174,7 @@ set (pythonTests
timesheet.test.py
tw-1379.test.py
tw-1837.test.py
tw-1999.test.py
tw-20.test.py
tw-2575.test.py
tw-262.test.py
@@ -198,3 +205,40 @@ foreach (python_Test ${pythonTests})
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
endforeach(python_Test)
# -- Shell tests
set (shell_SRCS
tw-1637.test.sh
tw-1643.test.sh
tw-1688.test.sh
tw-1715.test.sh
tw-1718.test.sh
tw-1804.test.sh
tw-1883.test.sh
tw-1895.test.sh
tw-1938.test.sh
tw-2124.test.sh
tw-2189.test.sh
tw-2257.test.sh
tw-2386.test.sh
tw-2392.test.sh
tw-2429.test.sh
tw-2451.test.sh
tw-2514.test.sh
tw-2530.test.sh
tw-2550.test.sh
tw-2581.test.sh
tw-3102.test.sh
tw-3109.test.sh
)
configure_file(bash_tap.sh bash_tap.sh COPYONLY)
configure_file(bash_tap_tw.sh bash_tap_tw.sh COPYONLY)
foreach (shell_Test ${shell_SRCS})
add_test(NAME ${shell_Test}
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/${shell_Test}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
endforeach(shell_Test)

View File

@@ -1,7 +1,7 @@
## Running Tests
Do this to run all tests:
```shell
cmake --build build --target build_tests
cmake --build build --target test_runner --target task_executable
ctest --test-dir build
```

View File

@@ -8,13 +8,7 @@ function bashtap_on_error {
# $bashtap_line contains the last executed line, or an error.
echo -n "$bashtap_output"
# Determine if this failure was expected
if [[ ! -z "$EXPFAIL" ]]
then
todo_suffix=" # TODO"
fi
echo "not ok 1 - ${bashtap_line}${todo_suffix}"
echo "not ok 1 - ${bashtap_line}"
bashtap_clean_tmpdir
}
@@ -81,41 +75,35 @@ function bashtap_get_absolute_path {
bashtap_org_pwd=$(pwd)
bashtap_org_script=$(bashtap_get_absolute_path "$0")
if [ "${0:(-2)}" == ".t" ] || [ "$1" == "-t" ]; then
# Make sure any failing commands are caught.
set -e
set -o pipefail
# Make sure any failing commands are caught.
set -e
set -o pipefail
# TAP header. Hardcoded number of tests, 1.
echo "1..1"
# TAP header. Hardcoded number of tests, 1.
echo "1..1"
# Output TAP failure on early exit.
trap bashtap_on_error EXIT
# Output TAP failure on early exit.
trap bashtap_on_error EXIT
# The different calls to mktemp are necessary for OSX compatibility.
bashtap_tmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'bash_tap')
if [ ! -z "$bashtap_tmpdir" ]; then
cd "$bashtap_tmpdir"
else
bashtap_line="Unable to create temporary directory."
exit 1
fi
# Scripts sourced before bash_tap.sh may declare this function.
if declare -f bashtap_setup >/dev/null; then
bashtap_setup
fi
# Run test file interpreting failing commands as a test failure.
bashtap_run_testcase && echo "ok 1"
# Since we're in a sourced file and just ran the parent script,
# exit without running it a second time.
trap - EXIT
bashtap_clean_tmpdir
exit
# The different calls to mktemp are necessary for OSX compatibility.
bashtap_tmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'bash_tap')
if [ ! -z "$bashtap_tmpdir" ]; then
cd "$bashtap_tmpdir"
else
if declare -f bashtap_setup >/dev/null; then
bashtap_setup
fi
bashtap_line="Unable to create temporary directory."
exit 1
fi
# Scripts sourced before bash_tap.sh may declare this function.
if declare -f bashtap_setup >/dev/null; then
bashtap_setup
fi
# Run test file interpreting failing commands as a test failure.
bashtap_run_testcase && echo "ok 1"
# Since we're in a sourced file and just ran the parent script,
# exit without running it a second time.
trap - EXIT
bashtap_clean_tmpdir
exit

View File

@@ -13,7 +13,7 @@
function setup_taskrc {
# Configuration
for i in pending.data completed.data undo.data backlog.data taskrc; do
for i in taskchampion.sqlite3 taskrc; do
if [ -f "$i" ]; then
rm "$i" 2>&1 >/dev/null
fi
@@ -26,6 +26,7 @@ function setup_taskrc {
echo 'color.header=rgb025' >> taskrc
echo 'color.footer=rgb025' >> taskrc
echo 'color.error=bold white on red' >> taskrc
echo 'news.version=99.0.0' >> taskrc
}
function find_task_binary {

View File

@@ -33,7 +33,7 @@
#include <test.h>
////////////////////////////////////////////////////////////////////////////////
int main(int, char**) {
int TEST_NAME(int, char**) {
UnitTest test(12);
// Ensure environment has no influence.

View File

@@ -26,5 +26,5 @@ RUN cmake --install build
RUN task --version
# Setup tests
RUN cmake --build build --target build_tests -j 8
RUN cmake --build build --target test_runner -j 8
CMD ctest --test-dir build -j 8 --output-on-failure --rerun-failed

Some files were not shown because too many files have changed in this diff Show More