Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a092bea03 | ||
|
|
54a94bd18c | ||
|
|
a2f9b92d6c | ||
|
|
dcc8a8cdde | ||
|
|
c9967c20e2 | ||
|
|
7da23aee1c | ||
|
|
5b1be95f7d | ||
|
|
0ff7844732 | ||
|
|
023e7958c9 | ||
|
|
8184226319 | ||
|
|
94c95563ab | ||
|
|
6ff900f3fc | ||
|
|
8bad3cdcbc | ||
|
|
0bb32d188c | ||
|
|
c3b850898f | ||
|
|
af8c5d58c8 | ||
|
|
2db373d631 | ||
|
|
96c72f3e06 | ||
|
|
4bf6144daf | ||
|
|
3e20ad6f6f | ||
|
|
7bd3d1b892 | ||
|
|
0bd3989bab | ||
|
|
26c383d615 | ||
|
|
a8b4bcdda8 | ||
|
|
28628e5dca | ||
|
|
ff2b1cb888 | ||
|
|
cfe92ce845 | ||
|
|
c95dc9d149 | ||
|
|
d75ef7f197 | ||
|
|
c00c0e941b | ||
|
|
6a24510473 | ||
|
|
72f9cd91a5 | ||
|
|
44d443a8d6 | ||
|
|
2e3badbf99 | ||
|
|
6cfbb16966 | ||
|
|
70632b088e | ||
|
|
d46e5eca58 | ||
|
|
05da133eb6 | ||
|
|
c719cce4f1 | ||
|
|
4ff63a7960 | ||
|
|
0f96fd31bf | ||
|
|
3d30f2ac46 | ||
|
|
49e09a9783 | ||
|
|
17889a3f25 | ||
|
|
c0b708d1f3 |
@@ -1 +0,0 @@
|
||||
../config.toml
|
||||
41
.github/workflows/checks.yml
vendored
41
.github/workflows/checks.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/docker-image.yaml
vendored
4
.github/workflows/docker-image.yaml
vendored
@@ -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"
|
||||
|
||||
3
.github/workflows/release-check.yaml
vendored
3
.github/workflows/release-check.yaml
vendored
@@ -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 &&
|
||||
|
||||
35
.github/workflows/tests.yaml
vendored
35
.github/workflows/tests.yaml
vendored
@@ -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
4
.gitmodules
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
787
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
13
Cargo.toml
@@ -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"
|
||||
|
||||
27
ChangeLog
27
ChangeLog
@@ -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
14
INSTALL
@@ -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
13
SECURITY.md
Normal 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.
|
||||
@@ -1,2 +0,0 @@
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
88
src/Operation.h
Normal 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
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
386
src/TDB2.cpp
386
src/TDB2.cpp
@@ -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() {
|
||||
|
||||
28
src/TDB2.h
28
src/TDB2.h
@@ -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
|
||||
|
||||
378
src/Task.cpp
378
src/Task.cpp
@@ -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;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
22
src/Task.h
22
src/Task.h
@@ -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();
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <JSON.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
class CmdImport : public Command {
|
||||
public:
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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& = "",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -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
|
||||
|
||||
Submodule src/libshared updated: 47c3262fa9...47a750c385
11
src/main.cpp
11
src/main.cpp
@@ -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;
|
||||
|
||||
29
src/taskchampion-cpp/CMakeLists.txt
Normal file
29
src/taskchampion-cpp/CMakeLists.txt
Normal 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
|
||||
)
|
||||
19
src/taskchampion-cpp/Cargo.toml
Normal file
19
src/taskchampion-cpp/Cargo.toml
Normal 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"
|
||||
6
src/taskchampion-cpp/build.rs
Normal file
6
src/taskchampion-cpp/build.rs
Normal 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");
|
||||
}
|
||||
1017
src/taskchampion-cpp/src/lib.rs
Normal file
1017
src/taskchampion-cpp/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
127
src/tc/Replica.h
127
src/tc/Replica.h
@@ -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
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -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
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
162
src/tc/Task.cpp
162
src/tc/Task.cpp
@@ -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;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
130
src/tc/Task.h
130
src/tc/Task.h
@@ -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
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -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
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
36
src/tc/ffi.h
36
src/tc/ffi.h
@@ -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
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
#include <test.h>
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int main(int, char**) {
|
||||
int TEST_NAME(int, char**) {
|
||||
UnitTest test(12);
|
||||
|
||||
// Ensure environment has no influence.
|
||||
@@ -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
Reference in New Issue
Block a user