Compare commits

..

11 commits

Author SHA1 Message Date
Alex Good
8af437d335
fix the e2e 2022-09-26 17:51:05 -05:00
Orion Henry
85f5016193
fmt & clippy 2022-09-26 17:46:54 -05:00
Orion Henry
1b26647f66
update all tests 2022-09-26 17:46:53 -05:00
Orion Henry
bab4501671
implement increment 2022-09-26 17:46:53 -05:00
Orion Henry
6142c383b1
map and array insert, delete for apply() 2022-09-26 17:46:53 -05:00
Orion Henry
6cbf2737c0
move op observer into transaction 2022-09-26 17:46:50 -05:00
Alex Good
b946e8c1a6
Set optimization levels to 'Z' for release profile
This reduces the size of the WASM bundle which is generated to around
800kb. Unfortunately wasm-pack doesn't allow us to use arbitrary
profiles when building and the optimization level has to be set at the
workspace root - consequently this flag is set for all packages in the
workspace. This shouldn't be an issue really as all our dependents in
the Rust world will be setting their own optimization flags anyway.
2022-09-26 17:40:28 -05:00
Alex Good
8c0bb30530
Add an e2e testing tool for the JS packaging
JS packaging is complicated and testing it manually is irritating. Add a
tool in `automerge-js/e2e` which stands up a local NPM registry and
publishes the various packages to that registry for use in automated and
manual tests. Update the test script in `scripts/ci/js_tests` to run the
tests using this tool
2022-09-26 17:40:28 -05:00
Alex Good
ce250a7bcd
Add examples of using automerge with bundlers 2022-09-26 17:40:27 -05:00
Alex Good
7a6a33da98
Rename automerge-js to automerge
Now that automerge-js is ready to go we rename it to `automerge-js` and
set the version to `1.0.1-preview.8` - which is one preview after the
currently published preview of the `automerge` package.
2022-09-26 17:40:27 -05:00
Alex Good
748dc954be
Remove async automerge-wasm wrapper
By moving to wasm-bindgens `bundler` target rather than using the `web`
target we remove the need for an async initialization step on the
automerge-wasm package. This means that the automerge-js package can now
depend directly on automerge-wasm and perform initialization itself,
thus making automerge-js a drop in replacement for the `automerge` JS
package (hopefully).

We bump the versions of automerge-wasm and automerge-types.
2022-09-26 17:40:22 -05:00
510 changed files with 25076 additions and 45387 deletions

View file

@ -2,10 +2,10 @@ name: CI
on:
push:
branches:
- main
- main
pull_request:
branches:
- main
- main
jobs:
fmt:
runs-on: ubuntu-latest
@ -14,7 +14,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.60.0
default: true
components: rustfmt
- uses: Swatinem/rust-cache@v1
@ -28,7 +28,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.60.0
default: true
components: clippy
- uses: Swatinem/rust-cache@v1
@ -42,7 +42,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- name: Build rust docs
@ -51,6 +51,9 @@ jobs:
- name: Install doxygen
run: sudo apt-get install -y doxygen
shell: bash
- name: Build C docs
run: ./scripts/ci/cmake-docs
shell: bash
cargo-deny:
runs-on: ubuntu-latest
@ -64,50 +67,23 @@ jobs:
- uses: actions/checkout@v2
- uses: EmbarkStudios/cargo-deny-action@v1
with:
arguments: '--manifest-path ./rust/Cargo.toml'
command: check ${{ matrix.checks }}
wasm_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install wasm-bindgen-cli
run: cargo install wasm-bindgen-cli wasm-opt
- name: Install wasm32 target
run: rustup target add wasm32-unknown-unknown
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: run tests
run: ./scripts/ci/wasm_tests
deno_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- name: Install wasm-bindgen-cli
run: cargo install wasm-bindgen-cli wasm-opt
- name: Install wasm32 target
run: rustup target add wasm32-unknown-unknown
- name: run tests
run: ./scripts/ci/deno_tests
js_fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: install
run: yarn global add prettier
- name: format
run: prettier -c javascript/.prettierrc javascript
js_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install wasm-bindgen-cli
run: cargo install wasm-bindgen-cli wasm-opt
- name: Install wasm32 target
run: rustup target add wasm32-unknown-unknown
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: run tests
run: ./scripts/ci/js_tests
@ -118,7 +94,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2023-01-26
toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- name: Install CMocka
@ -127,8 +103,6 @@ jobs:
uses: jwlawson/actions-setup-cmake@v1.12
with:
cmake-version: latest
- name: Install rust-src
run: rustup component add rust-src
- name: Build and test C bindings
run: ./scripts/ci/cmake-build Release Static
shell: bash
@ -138,7 +112,9 @@ jobs:
strategy:
matrix:
toolchain:
- 1.67.0
- 1.60.0
- nightly
continue-on-error: ${{ matrix.toolchain == 'nightly' }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
@ -157,7 +133,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/ci/build-test
@ -170,7 +146,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/ci/build-test

View file

@ -30,16 +30,28 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clean
args: --manifest-path ./rust/Cargo.toml --doc
args: --doc
- name: Build Rust docs
uses: actions-rs/cargo@v1
with:
command: doc
args: --manifest-path ./rust/Cargo.toml --workspace --all-features --no-deps
args: --workspace --all-features --no-deps
- name: Move Rust docs
run: mkdir -p docs && mv rust/target/doc/* docs/.
run: mkdir -p docs && mv target/doc/* docs/.
shell: bash
- name: Install doxygen
run: sudo apt-get install -y doxygen
shell: bash
- name: Build C docs
run: ./scripts/ci/cmake-docs
shell: bash
- name: Move C docs
run: mkdir -p docs/automerge-c && mv automerge-c/build/src/html/* docs/automerge-c/.
shell: bash
- name: Configure root page

View file

@ -1,214 +0,0 @@
name: Release
on:
push:
branches:
- main
jobs:
check_if_wasm_version_upgraded:
name: Check if WASM version has been upgraded
runs-on: ubuntu-latest
outputs:
wasm_version: ${{ steps.version-updated.outputs.current-package-version }}
wasm_has_updated: ${{ steps.version-updated.outputs.has-updated }}
steps:
- uses: JiPaix/package-json-updated-action@v1.0.5
id: version-updated
with:
path: rust/automerge-wasm/package.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-wasm:
name: Publish WASM package
runs-on: ubuntu-latest
needs:
- check_if_wasm_version_upgraded
# We create release only if the version in the package.json has been upgraded
if: needs.check_if_wasm_version_upgraded.outputs.wasm_has_updated == 'true'
steps:
- uses: actions/setup-node@v3
with:
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
- uses: denoland/setup-deno@v1
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.ref }}
- name: Get rid of local github workflows
run: rm -r .github/workflows
- name: Remove tmp_branch if it exists
run: git push origin :tmp_branch || true
- run: git checkout -b tmp_branch
- name: Install wasm-bindgen-cli
run: cargo install wasm-bindgen-cli wasm-opt
- name: Install wasm32 target
run: rustup target add wasm32-unknown-unknown
- name: run wasm js tests
id: wasm_js_tests
run: ./scripts/ci/wasm_tests
- name: run wasm deno tests
id: wasm_deno_tests
run: ./scripts/ci/deno_tests
- name: build release
id: build_release
run: |
npm --prefix $GITHUB_WORKSPACE/rust/automerge-wasm run release
- name: Collate deno release files
if: steps.wasm_js_tests.outcome == 'success' && steps.wasm_deno_tests.outcome == 'success'
run: |
mkdir $GITHUB_WORKSPACE/deno_wasm_dist
cp $GITHUB_WORKSPACE/rust/automerge-wasm/deno/* $GITHUB_WORKSPACE/deno_wasm_dist
cp $GITHUB_WORKSPACE/rust/automerge-wasm/index.d.ts $GITHUB_WORKSPACE/deno_wasm_dist
cp $GITHUB_WORKSPACE/rust/automerge-wasm/README.md $GITHUB_WORKSPACE/deno_wasm_dist
cp $GITHUB_WORKSPACE/rust/automerge-wasm/LICENSE $GITHUB_WORKSPACE/deno_wasm_dist
sed -i '1i /// <reference types="./index.d.ts" />' $GITHUB_WORKSPACE/deno_wasm_dist/automerge_wasm.js
- name: Create npm release
if: steps.wasm_js_tests.outcome == 'success' && steps.wasm_deno_tests.outcome == 'success'
run: |
if [ "$(npm --prefix $GITHUB_WORKSPACE/rust/automerge-wasm show . version)" = "$VERSION" ]; then
echo "This version is already published"
exit 0
fi
EXTRA_ARGS="--access public"
if [[ $VERSION == *"alpha."* ]] || [[ $VERSION == *"beta."* ]] || [[ $VERSION == *"rc."* ]]; then
echo "Is pre-release version"
EXTRA_ARGS="$EXTRA_ARGS --tag next"
fi
if [ "$NODE_AUTH_TOKEN" = "" ]; then
echo "Can't publish on NPM, You need a NPM_TOKEN secret."
false
fi
npm publish $GITHUB_WORKSPACE/rust/automerge-wasm $EXTRA_ARGS
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
VERSION: ${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
- name: Commit wasm deno release files
run: |
git config --global user.name "actions"
git config --global user.email actions@github.com
git add $GITHUB_WORKSPACE/deno_wasm_dist
git commit -am "Add deno release files"
git push origin tmp_branch
- name: Tag wasm release
if: steps.wasm_js_tests.outcome == 'success' && steps.wasm_deno_tests.outcome == 'success'
uses: softprops/action-gh-release@v1
with:
name: Automerge Wasm v${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
tag_name: js/automerge-wasm-${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
target_commitish: tmp_branch
generate_release_notes: false
draft: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Remove tmp_branch
run: git push origin :tmp_branch
check_if_js_version_upgraded:
name: Check if JS version has been upgraded
runs-on: ubuntu-latest
outputs:
js_version: ${{ steps.version-updated.outputs.current-package-version }}
js_has_updated: ${{ steps.version-updated.outputs.has-updated }}
steps:
- uses: JiPaix/package-json-updated-action@v1.0.5
id: version-updated
with:
path: javascript/package.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-js:
name: Publish JS package
runs-on: ubuntu-latest
needs:
- check_if_js_version_upgraded
- check_if_wasm_version_upgraded
- publish-wasm
# We create release only if the version in the package.json has been upgraded and after the WASM release
if: |
(always() && ! cancelled()) &&
(needs.publish-wasm.result == 'success' || needs.publish-wasm.result == 'skipped') &&
needs.check_if_js_version_upgraded.outputs.js_has_updated == 'true'
steps:
- uses: actions/setup-node@v3
with:
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
- uses: denoland/setup-deno@v1
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.ref }}
- name: Get rid of local github workflows
run: rm -r .github/workflows
- name: Remove js_tmp_branch if it exists
run: git push origin :js_tmp_branch || true
- run: git checkout -b js_tmp_branch
- name: check js formatting
run: |
yarn global add prettier
prettier -c javascript/.prettierrc javascript
- name: run js tests
id: js_tests
run: |
cargo install wasm-bindgen-cli wasm-opt
rustup target add wasm32-unknown-unknown
./scripts/ci/js_tests
- name: build js release
id: build_release
run: |
npm --prefix $GITHUB_WORKSPACE/javascript run build
- name: build js deno release
id: build_deno_release
run: |
VERSION=$WASM_VERSION npm --prefix $GITHUB_WORKSPACE/javascript run deno:build
env:
WASM_VERSION: ${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
- name: run deno tests
id: deno_tests
run: |
npm --prefix $GITHUB_WORKSPACE/javascript run deno:test
- name: Collate deno release files
if: steps.js_tests.outcome == 'success' && steps.deno_tests.outcome == 'success'
run: |
mkdir $GITHUB_WORKSPACE/deno_js_dist
cp $GITHUB_WORKSPACE/javascript/deno_dist/* $GITHUB_WORKSPACE/deno_js_dist
- name: Create npm release
if: steps.js_tests.outcome == 'success' && steps.deno_tests.outcome == 'success'
run: |
if [ "$(npm --prefix $GITHUB_WORKSPACE/javascript show . version)" = "$VERSION" ]; then
echo "This version is already published"
exit 0
fi
EXTRA_ARGS="--access public"
if [[ $VERSION == *"alpha."* ]] || [[ $VERSION == *"beta."* ]] || [[ $VERSION == *"rc."* ]]; then
echo "Is pre-release version"
EXTRA_ARGS="$EXTRA_ARGS --tag next"
fi
if [ "$NODE_AUTH_TOKEN" = "" ]; then
echo "Can't publish on NPM, You need a NPM_TOKEN secret."
false
fi
npm publish $GITHUB_WORKSPACE/javascript $EXTRA_ARGS
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
VERSION: ${{ needs.check_if_js_version_upgraded.outputs.js_version }}
- name: Commit js deno release files
run: |
git config --global user.name "actions"
git config --global user.email actions@github.com
git add $GITHUB_WORKSPACE/deno_js_dist
git commit -am "Add deno js release files"
git push origin js_tmp_branch
- name: Tag JS release
if: steps.js_tests.outcome == 'success' && steps.deno_tests.outcome == 'success'
uses: softprops/action-gh-release@v1
with:
name: Automerge v${{ needs.check_if_js_version_upgraded.outputs.js_version }}
tag_name: js/automerge-${{ needs.check_if_js_version_upgraded.outputs.js_version }}
target_commitish: js_tmp_branch
generate_release_notes: false
draft: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Remove js_tmp_branch
run: git push origin :js_tmp_branch

3
.gitignore vendored
View file

@ -1,6 +1,7 @@
/target
/.direnv
perf.*
/Cargo.lock
build/
automerge/proptest-regressions/
.vim/*
/target

View file

@ -3,15 +3,19 @@ members = [
"automerge",
"automerge-c",
"automerge-cli",
"automerge-test",
"automerge-wasm",
"edit-trace",
]
resolver = "2"
[profile.release]
debug = true
lto = true
codegen-units = 1
opt-level = 'z'
[profile.bench]
debug = true
debug = true
[profile.release.package.automerge-wasm]
debug = false
opt-level = 'z'

20
Makefile Normal file
View file

@ -0,0 +1,20 @@
.PHONY: rust
rust:
cd automerge && cargo test
.PHONY: wasm
wasm:
cd automerge-wasm && yarn
cd automerge-wasm && yarn build
cd automerge-wasm && yarn test
cd automerge-wasm && yarn link
.PHONY: js
js: wasm
cd automerge-js && yarn
cd automerge-js && yarn link "automerge-wasm"
cd automerge-js && yarn test
.PHONY: clean
clean:
git clean -x -d -f

198
README.md
View file

@ -1,4 +1,4 @@
# Automerge
# Automerge RS
<img src='./img/sign.svg' width='500' alt='Automerge logo' />
@ -7,141 +7,103 @@
[![ci](https://github.com/automerge/automerge-rs/actions/workflows/ci.yaml/badge.svg)](https://github.com/automerge/automerge-rs/actions/workflows/ci.yaml)
[![docs](https://github.com/automerge/automerge-rs/actions/workflows/docs.yaml/badge.svg)](https://github.com/automerge/automerge-rs/actions/workflows/docs.yaml)
Automerge is a library which provides fast implementations of several different
CRDTs, a compact compression format for these CRDTs, and a sync protocol for
efficiently transmitting those changes over the network. The objective of the
project is to support [local-first](https://www.inkandswitch.com/local-first/) applications in the same way that relational
databases support server applications - by providing mechanisms for persistence
which allow application developers to avoid thinking about hard distributed
computing problems. Automerge aims to be PostgreSQL for your local-first app.
This is a Rust library implementation of the [Automerge](https://github.com/automerge/automerge) file format and network protocol. Its focus is to support the creation of Automerge implementations in other languages, currently; WASM, JS and C. A `libautomerge` if you will.
If you're looking for documentation on the JavaScript implementation take a look
at https://automerge.org/docs/hello/. There are other implementations in both
Rust and C, but they are earlier and don't have documentation yet. You can find
them in `rust/automerge` and `rust/automerge-c` if you are comfortable
reading the code and tests to figure out how to use them.
If you're familiar with CRDTs and interested in the design of Automerge in
particular take a look at https://automerge.org/docs/how-it-works/backend/
Finally, if you want to talk to us about this project please [join the
Slack](https://join.slack.com/t/automerge/shared_invite/zt-e4p3760n-kKh7r3KRH1YwwNfiZM8ktw)
The original [Automerge](https://github.com/automerge/automerge) project (written in JS from the ground up) is still very much maintained and recommended. Indeed it is because of the success of that project that the next stage of Automerge is being explored here. Hopefully Rust can offer a more performant and scalable Automerge, opening up even more use cases.
## Status
This project is formed of a core Rust implementation which is exposed via FFI in
javascript+WASM, C, and soon other languages. Alex
([@alexjg](https://github.com/alexjg/)]) is working full time on maintaining
automerge, other members of Ink and Switch are also contributing time and there
are several other maintainers. The focus is currently on shipping the new JS
package. We expect to be iterating the API and adding new features over the next
six months so there will likely be several major version bumps in all packages
in that time.
The project has 5 components:
In general we try and respect semver.
1. [_automerge_](automerge) - The main Rust implementation of the library.
2. [_automerge-wasm_](automerge-wasm) - A JS/WASM interface to the underlying Rust library. This API is generally mature and in use in a handful of projects.
3. [_automerge-js_](automerge-js) - This is a Javascript library using the WASM interface to export the same public API of the primary Automerge project. Currently this project passes all of Automerge's tests but has not been used in any real project or packaged as an NPM. Alpha testers welcome.
4. [_automerge-c_](automerge-c) - This is a C library intended to be an FFI integration point for all other languages. It is currently a work in progress and not yet ready for any testing.
5. [_automerge-cli_](automerge-cli) - An experimental CLI wrapper around the Rust library. Currently not functional.
### JavaScript
## How?
A stable release of the javascript package is currently available as
`@automerge/automerge@2.0.0` where. pre-release verisions of the `2.0.1` are
available as `2.0.1-alpha.n`. `2.0.1*` packages are also available for Deno at
https://deno.land/x/automerge
The magic of the architecture is built around the `OpTree`. This is a data structure
which supports efficiently inserting new operations and realising values of
existing operations. Most interactions with the `OpTree` are in the form of
implementations of `TreeQuery` - a trait which can be used to traverse the
`OpTree` and producing state of some kind. User facing operations are exposed on
an `Automerge` object, under the covers these operations typically instantiate
some `TreeQuery` and run it over the `OpTree`.
### Rust
## Development
The rust codebase is currently oriented around producing a performant backend
for the Javascript wrapper and as such the API for Rust code is low level and
not well documented. We will be returning to this over the next few months but
for now you will need to be comfortable reading the tests and asking questions
to figure out how to use it. If you are looking to build rust applications which
use automerge you may want to look into
[autosurgeon](https://github.com/alexjg/autosurgeon)
Please feel free to open issues and pull requests.
## Repository Organisation
### Running CI
- `./rust` - the rust rust implementation and also the Rust components of
platform specific wrappers (e.g. `automerge-wasm` for the WASM API or
`automerge-c` for the C FFI bindings)
- `./javascript` - The javascript library which uses `automerge-wasm`
internally but presents a more idiomatic javascript interface
- `./scripts` - scripts which are useful to maintenance of the repository.
This includes the scripts which are run in CI.
- `./img` - static assets for use in `.md` files
The steps CI will run are all defined in `./scripts/ci`. Obviously CI will run
everything when you submit a PR, but if you want to run everything locally
before you push you can run `./scripts/ci/run` to run everything.
## Building
### Running the JS tests
To build this codebase you will need:
You will need to have [node](https://nodejs.org/en/), [yarn](https://yarnpkg.com/getting-started/install), [rust](https://rustup.rs/) and [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) installed.
- `rust`
- `node`
- `yarn`
- `cmake`
- `cmocka`
To build and test the rust library:
You will also need to install the following with `cargo install`
- `wasm-bindgen-cli`
- `wasm-opt`
- `cargo-deny`
And ensure you have added the `wasm32-unknown-unknown` target for rust cross-compilation.
The various subprojects (the rust code, the wrapper projects) have their own
build instructions, but to run the tests that will be run in CI you can run
`./scripts/ci/run`.
### For macOS
These instructions worked to build locally on macOS 13.1 (arm64) as of
Nov 29th 2022.
```bash
# clone the repo
git clone https://github.com/automerge/automerge-rs
cd automerge-rs
# install rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# install homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# install cmake, node, cmocka
brew install cmake node cmocka
# install yarn
npm install --global yarn
# install javascript dependencies
yarn --cwd ./javascript
# install rust dependencies
cargo install wasm-bindgen-cli wasm-opt cargo-deny
# get nightly rust to produce optimized automerge-c builds
rustup toolchain install nightly
rustup component add rust-src --toolchain nightly
# add wasm target in addition to current architecture
rustup target add wasm32-unknown-unknown
# Run ci script
./scripts/ci/run
```shell
$ cd automerge
$ cargo test
```
If your build fails to find `cmocka.h` you may need to teach it about homebrew's
installation location:
To build and test the wasm library:
```
export CPATH=/opt/homebrew/include
export LIBRARY_PATH=/opt/homebrew/lib
./scripts/ci/run
```shell
## setup
$ cd automerge-wasm
$ yarn
## building or testing
$ yarn build
$ yarn test
## without this the js library wont automatically use changes
$ yarn link
## cutting a release or doing benchmarking
$ yarn release
```
## Contributing
To test the js library. This is where most of the tests reside.
Please try and split your changes up into relatively independent commits which
change one subsystem at a time and add good commit messages which describe what
the change is and why you're making it (err on the side of longer commit
messages). `git blame` should give future maintainers a good idea of why
something is the way it is.
```shell
## setup
$ cd automerge-js
$ yarn
$ yarn link "automerge-wasm"
## testing
$ yarn test
```
And finally, to build and test the C bindings with CMake:
```shell
## setup
$ cd automerge-c
$ mkdir -p build
$ cd build
$ cmake -S .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF
## building and testing
$ cmake --build . --target test_automerge
```
To add debugging symbols, replace `Release` with `Debug`.
To build a shared library instead of a static one, replace `OFF` with `ON`.
The C bindings can be built and tested on any platform for which CMake is
available but the steps for doing so vary across platforms and are too numerous
to list here.
## Benchmarking
The [`edit-trace`](edit-trace) folder has the main code for running the edit trace benchmarking.
## The old Rust project
If you are looking for the origional `automerge-rs` project that can be used as a wasm backend to the javascript implementation, it can be found [here](https://github.com/automerge/automerge-rs/tree/automerge-1.0).

32
TODO.md Normal file
View file

@ -0,0 +1,32 @@
### next steps:
1. C API
2. port rust command line tool
3. fast load
### ergonomics:
1. value() -> () or something that into's a value
### automerge:
1. single pass (fast) load
2. micro-patches / bare bones observation API / fully hydrated documents
### future:
1. handle columns with unknown data in and out
2. branches with different indexes
### Peritext
1. add mark / remove mark -- type, start/end elemid (inclusive,exclusive)
2. track any formatting ops that start or end on a character
3. ops right before the character, ops right after that character
4. query a single character - character, plus marks that start or end on that character
what is its current formatting,
what are the ops that include that in their span,
None = same as last time, Set( bold, italic ),
keep these on index
5. op probably belongs with the start character - possible packed at the beginning or end of the list
### maybe:
1. tables
### no:
1. cursors

3
automerge-c/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
automerge
automerge.h
automerge.o

141
automerge-c/CMakeLists.txt Normal file
View file

@ -0,0 +1,141 @@
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
# Parse the library name, project name and project version out of Cargo's TOML file.
set(CARGO_LIB_SECTION OFF)
set(LIBRARY_NAME "")
set(CARGO_PKG_SECTION OFF)
set(CARGO_PKG_NAME "")
set(CARGO_PKG_VERSION "")
file(READ Cargo.toml TOML_STRING)
string(REPLACE ";" "\\\\;" TOML_STRING "${TOML_STRING}")
string(REPLACE "\n" ";" TOML_LINES "${TOML_STRING}")
foreach(TOML_LINE IN ITEMS ${TOML_LINES})
string(REGEX MATCH "^\\[(lib|package)\\]$" _ ${TOML_LINE})
if(CMAKE_MATCH_1 STREQUAL "lib")
set(CARGO_LIB_SECTION ON)
set(CARGO_PKG_SECTION OFF)
elseif(CMAKE_MATCH_1 STREQUAL "package")
set(CARGO_LIB_SECTION OFF)
set(CARGO_PKG_SECTION ON)
endif()
string(REGEX MATCH "^name += +\"([^\"]+)\"$" _ ${TOML_LINE})
if(CMAKE_MATCH_1 AND (CARGO_LIB_SECTION AND NOT CARGO_PKG_SECTION))
set(LIBRARY_NAME "${CMAKE_MATCH_1}")
elseif(CMAKE_MATCH_1 AND (NOT CARGO_LIB_SECTION AND CARGO_PKG_SECTION))
set(CARGO_PKG_NAME "${CMAKE_MATCH_1}")
endif()
string(REGEX MATCH "^version += +\"([^\"]+)\"$" _ ${TOML_LINE})
if(CMAKE_MATCH_1 AND CARGO_PKG_SECTION)
set(CARGO_PKG_VERSION "${CMAKE_MATCH_1}")
endif()
if(LIBRARY_NAME AND (CARGO_PKG_NAME AND CARGO_PKG_VERSION))
break()
endif()
endforeach()
project(${CARGO_PKG_NAME} VERSION ${CARGO_PKG_VERSION} LANGUAGES C DESCRIPTION "C bindings for the Automerge Rust backend.")
include(CTest)
option(BUILD_SHARED_LIBS "Enable the choice of a shared or static library.")
include(CMakePackageConfigHelpers)
include(GNUInstallDirs)
string(MAKE_C_IDENTIFIER ${PROJECT_NAME} SYMBOL_PREFIX)
string(TOUPPER ${SYMBOL_PREFIX} SYMBOL_PREFIX)
set(CARGO_TARGET_DIR "${CMAKE_CURRENT_BINARY_DIR}/Cargo/target")
set(CBINDGEN_INCLUDEDIR "${CARGO_TARGET_DIR}/${CMAKE_INSTALL_INCLUDEDIR}")
set(CBINDGEN_TARGET_DIR "${CBINDGEN_INCLUDEDIR}/${PROJECT_NAME}")
add_subdirectory(src)
# Generate and install the configuration header.
math(EXPR INTEGER_PROJECT_VERSION_MAJOR "${PROJECT_VERSION_MAJOR} * 100000")
math(EXPR INTEGER_PROJECT_VERSION_MINOR "${PROJECT_VERSION_MINOR} * 100")
math(EXPR INTEGER_PROJECT_VERSION_PATCH "${PROJECT_VERSION_PATCH}")
math(EXPR INTEGER_PROJECT_VERSION "${INTEGER_PROJECT_VERSION_MAJOR} + ${INTEGER_PROJECT_VERSION_MINOR} + ${INTEGER_PROJECT_VERSION_PATCH}")
configure_file(
${CMAKE_MODULE_PATH}/config.h.in
config.h
@ONLY
NEWLINE_STYLE LF
)
install(
FILES ${CMAKE_BINARY_DIR}/config.h
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
)
if(BUILD_TESTING)
add_subdirectory(test EXCLUDE_FROM_ALL)
enable_testing()
endif()
add_subdirectory(examples EXCLUDE_FROM_ALL)
# Generate and install .cmake files
set(PROJECT_CONFIG_NAME "${PROJECT_NAME}-config")
set(PROJECT_CONFIG_VERSION_NAME "${PROJECT_CONFIG_NAME}-version")
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_VERSION_NAME}.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY ExactVersion
)
# The namespace label starts with the title-cased library name.
string(SUBSTRING ${LIBRARY_NAME} 0 1 NS_FIRST)
string(SUBSTRING ${LIBRARY_NAME} 1 -1 NS_REST)
string(TOUPPER ${NS_FIRST} NS_FIRST)
string(TOLOWER ${NS_REST} NS_REST)
string(CONCAT NAMESPACE ${NS_FIRST} ${NS_REST} "::")
# \note CMake doesn't automate the exporting of an imported library's targets
# so the package configuration script must do it.
configure_package_config_file(
${CMAKE_MODULE_PATH}/${PROJECT_CONFIG_NAME}.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_NAME}.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
)
install(
FILES
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_NAME}.cmake
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_VERSION_NAME}.cmake
DESTINATION
${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
)

View file

@ -7,8 +7,8 @@ license = "MIT"
rust-version = "1.57.0"
[lib]
name = "automerge_core"
crate-type = ["staticlib"]
name = "automerge"
crate-type = ["cdylib", "staticlib"]
bench = false
doc = false
@ -19,4 +19,4 @@ libc = "^0.2"
smol_str = "^0.1.21"
[build-dependencies]
cbindgen = "^0.24"
cbindgen = "^0.20"

97
automerge-c/README.md Normal file
View file

@ -0,0 +1,97 @@
## Methods we need to support
### Basic management
1. `AMcreate()`
1. `AMclone(doc)`
1. `AMfree(doc)`
1. `AMconfig(doc, key, val)` // set actor
1. `actor = get_actor(doc)`
### Transactions
1. `AMpendingOps(doc)`
1. `AMcommit(doc, message, time)`
1. `AMrollback(doc)`
### Write
1. `AMset{Map|List}(doc, obj, prop, value)`
1. `AMinsert(doc, obj, index, value)`
1. `AMpush(doc, obj, value)`
1. `AMdel{Map|List}(doc, obj, prop)`
1. `AMinc{Map|List}(doc, obj, prop, value)`
1. `AMspliceText(doc, obj, start, num_del, text)`
### Read (the heads argument is optional and can be on an `at` variant)
1. `AMkeys(doc, obj, heads)`
1. `AMlength(doc, obj, heads)`
1. `AMlistRange(doc, obj, heads)`
1. `AMmapRange(doc, obj, heads)`
1. `AMvalues(doc, obj, heads)`
1. `AMtext(doc, obj, heads)`
### Sync
1. `AMgenerateSyncMessage(doc, state)`
1. `AMreceiveSyncMessage(doc, state, message)`
1. `AMinitSyncState()`
### Save / Load
1. `AMload(data)`
1. `AMloadIncremental(doc, data)`
1. `AMsave(doc)`
1. `AMsaveIncremental(doc)`
### Low Level Access
1. `AMapplyChanges(doc, changes)`
1. `AMgetChanges(doc, deps)`
1. `AMgetChangesAdded(doc1, doc2)`
1. `AMgetHeads(doc)`
1. `AMgetLastLocalChange(doc)`
1. `AMgetMissingDeps(doc, heads)`
### Encode/Decode
1. `AMencodeChange(change)`
1. `AMdecodeChange(change)`
1. `AMencodeSyncMessage(change)`
1. `AMdecodeSyncMessage(change)`
1. `AMencodeSyncState(change)`
1. `AMdecodeSyncState(change)`
## Open Question - Memory management
Most of these calls return one or more items of arbitrary length. Doing memory management in C is tricky. This is my proposed solution...
###
```
// returns 1 or zero opids
n = automerge_set(doc, "_root", "hello", datatype, value);
if (n) {
automerge_pop(doc, &obj, len);
}
// returns n values
n = automerge_values(doc, "_root", "hello");
for (i = 0; i<n ;i ++) {
automerge_pop_value(doc, &value, &datatype, len);
}
```
There would be one pop method per object type. Users allocs and frees the buffers. Multiple return values would result in multiple pops. Too small buffers would error and allow retry.
### Formats
Actors - We could do (bytes,len) or a hex encoded string?.
ObjIds - We could do flat bytes of the ExId struct but lets do human readable strings for now - the struct would be faster but opque
Heads - Might as well make it a flat buffer `(n, hash, hash, ...)`
Changes - Put them all in a flat concatenated buffer
Encode/Decode - to json strings?

View file

@ -10,7 +10,7 @@ fn main() {
let config = cbindgen::Config::from_file("cbindgen.toml")
.expect("Unable to find cbindgen.toml configuration file");
if let Ok(writer) = cbindgen::generate_with_config(crate_dir, config) {
if let Ok(writer) = cbindgen::generate_with_config(&crate_dir, config) {
// \note CMake sets this environment variable before invoking Cargo so
// that it can direct the generated header file into its
// out-of-source build directory for post-processing.

View file

@ -1,7 +1,7 @@
after_includes = """\n
/**
* \\defgroup enumerations Public Enumerations
* Symbolic names for integer constants.
Symbolic names for integer constants.
*/
/**
@ -12,23 +12,21 @@ after_includes = """\n
#define AM_ROOT NULL
/**
* \\memberof AMdoc
* \\memberof AMchangeHash
* \\def AM_CHANGE_HASH_SIZE
* \\brief The count of bytes in a change hash.
*/
#define AM_CHANGE_HASH_SIZE 32
"""
autogen_warning = """
/**
* \\file
* \\brief All constants, functions and types in the core Automerge C API.
*
* \\warning This file is auto-generated by cbindgen.
*/
"""
autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
documentation = true
documentation_style = "doxy"
include_guard = "AUTOMERGE_C_H"
header = """
/** \\file
* All constants, functions and types in the Automerge library's C API.
*/
"""
include_guard = "AUTOMERGE_H"
includes = []
language = "C"
line_length = 140

View file

@ -0,0 +1,14 @@
#ifndef @SYMBOL_PREFIX@_CONFIG_H
#define @SYMBOL_PREFIX@_CONFIG_H
/* This header is auto-generated by CMake. */
#define @SYMBOL_PREFIX@_VERSION @INTEGER_PROJECT_VERSION@
#define @SYMBOL_PREFIX@_MAJOR_VERSION (@SYMBOL_PREFIX@_VERSION / 100000)
#define @SYMBOL_PREFIX@_MINOR_VERSION ((@SYMBOL_PREFIX@_VERSION / 100) % 1000)
#define @SYMBOL_PREFIX@_PATCH_VERSION (@SYMBOL_PREFIX@_VERSION % 100)
#endif /* @SYMBOL_PREFIX@_CONFIG_H */

View file

@ -1,6 +1,4 @@
# This CMake script is used to perform string substitutions within a generated
# file.
cmake_minimum_required(VERSION 3.23 FATAL_ERROR)
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
if(NOT DEFINED MATCH_REGEX)
message(FATAL_ERROR "Variable \"MATCH_REGEX\" is not defined.")

View file

@ -1,6 +1,4 @@
# This CMake script is used to force Cargo to regenerate the header file for the
# core bindings after the out-of-source build directory has been cleaned.
cmake_minimum_required(VERSION 3.23 FATAL_ERROR)
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
if(NOT DEFINED CONDITION)
message(FATAL_ERROR "Variable \"CONDITION\" is not defined.")

View file

@ -1,39 +1,41 @@
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
add_executable(
${LIBRARY_NAME}_quickstart
example_quickstart
quickstart.c
)
set_target_properties(${LIBRARY_NAME}_quickstart PROPERTIES LINKER_LANGUAGE C)
set_target_properties(example_quickstart PROPERTIES LINKER_LANGUAGE C)
# \note An imported library's INTERFACE_INCLUDE_DIRECTORIES property can't
# contain a non-existent path so its build-time include directory
# must be specified for all of its dependent targets instead.
target_include_directories(
${LIBRARY_NAME}_quickstart
example_quickstart
PRIVATE "$<BUILD_INTERFACE:${CBINDGEN_INCLUDEDIR}>"
)
target_link_libraries(${LIBRARY_NAME}_quickstart PRIVATE ${LIBRARY_NAME})
target_link_libraries(example_quickstart PRIVATE ${LIBRARY_NAME})
add_dependencies(${LIBRARY_NAME}_quickstart ${BINDINGS_NAME}_artifacts)
add_dependencies(example_quickstart ${LIBRARY_NAME}_artifacts)
if(BUILD_SHARED_LIBS AND WIN32)
add_custom_command(
TARGET ${LIBRARY_NAME}_quickstart
TARGET example_quickstart
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_SHARED_LIBRARY_SUFFIX}
${CMAKE_BINARY_DIR}
${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Copying the DLL built by Cargo into the examples directory..."
VERBATIM
)
endif()
add_custom_command(
TARGET ${LIBRARY_NAME}_quickstart
TARGET example_quickstart
POST_BUILD
COMMAND
${LIBRARY_NAME}_quickstart
example_quickstart
COMMENT
"Running the example quickstart..."
VERBATIM

View file

@ -5,5 +5,5 @@
```shell
cmake -E make_directory automerge-c/build
cmake -S automerge-c -B automerge-c/build
cmake --build automerge-c/build --target automerge_quickstart
cmake --build automerge-c/build --target example_quickstart
```

View file

@ -0,0 +1,146 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <automerge-c/automerge.h>
static void abort_cb(AMresultStack**, uint8_t);
/**
* \brief Based on https://automerge.github.io/docs/quickstart
*/
int main(int argc, char** argv) {
AMresultStack* stack = NULL;
AMdoc* const doc1 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, abort_cb).doc;
AMobjId const* const cards = AMpush(&stack,
AMmapPutObject(doc1, AM_ROOT, "cards", AM_OBJ_TYPE_LIST),
AM_VALUE_OBJ_ID,
abort_cb).obj_id;
AMobjId const* const card1 = AMpush(&stack,
AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP),
AM_VALUE_OBJ_ID,
abort_cb).obj_id;
AMfree(AMmapPutStr(doc1, card1, "title", "Rewrite everything in Clojure"));
AMfree(AMmapPutBool(doc1, card1, "done", false));
AMobjId const* const card2 = AMpush(&stack,
AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP),
AM_VALUE_OBJ_ID,
abort_cb).obj_id;
AMfree(AMmapPutStr(doc1, card2, "title", "Rewrite everything in Haskell"));
AMfree(AMmapPutBool(doc1, card2, "done", false));
AMfree(AMcommit(doc1, "Add card", NULL));
AMdoc* doc2 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, abort_cb).doc;
AMfree(AMmerge(doc2, doc1));
AMbyteSpan const binary = AMpush(&stack, AMsave(doc1), AM_VALUE_BYTES, abort_cb).bytes;
doc2 = AMpush(&stack, AMload(binary.src, binary.count), AM_VALUE_DOC, abort_cb).doc;
AMfree(AMmapPutBool(doc1, card1, "done", true));
AMfree(AMcommit(doc1, "Mark card as done", NULL));
AMfree(AMlistDelete(doc2, cards, 0));
AMfree(AMcommit(doc2, "Delete card", NULL));
AMfree(AMmerge(doc1, doc2));
AMchanges changes = AMpush(&stack, AMgetChanges(doc1, NULL), AM_VALUE_CHANGES, abort_cb).changes;
AMchange const* change = NULL;
while ((change = AMchangesNext(&changes, 1)) != NULL) {
AMbyteSpan const change_hash = AMchangeHash(change);
AMchangeHashes const heads = AMpush(&stack,
AMchangeHashesInit(&change_hash, 1),
AM_VALUE_CHANGE_HASHES,
abort_cb).change_hashes;
printf("%s %ld\n", AMchangeMessage(change), AMobjSize(doc1, cards, &heads));
}
AMfreeStack(&stack);
}
static char const* discriminant_suffix(AMvalueVariant const);
/**
* \brief Prints an error message to `stderr`, deallocates all results in the
* given stack and exits.
*
* \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
* \param[in] discriminant An `AMvalueVariant` enum tag.
* \pre \p stack` != NULL`.
* \post `*stack == NULL`.
*/
static void abort_cb(AMresultStack** stack, uint8_t discriminant) {
static char buffer[512] = {0};
char const* suffix = NULL;
if (!stack) {
suffix = "Stack*";
}
else if (!*stack) {
suffix = "Stack";
}
else if (!(*stack)->result) {
suffix = "";
}
if (suffix) {
fprintf(stderr, "Null `AMresult%s*`.", suffix);
AMfreeStack(stack);
exit(EXIT_FAILURE);
return;
}
AMstatus const status = AMresultStatus((*stack)->result);
switch (status) {
case AM_STATUS_ERROR: strcpy(buffer, "Error"); break;
case AM_STATUS_INVALID_RESULT: strcpy(buffer, "Invalid result"); break;
case AM_STATUS_OK: break;
default: sprintf(buffer, "Unknown `AMstatus` tag %d", status);
}
if (buffer[0]) {
fprintf(stderr, "%s; %s.", buffer, AMerrorMessage((*stack)->result));
AMfreeStack(stack);
exit(EXIT_FAILURE);
return;
}
AMvalue const value = AMresultValue((*stack)->result);
fprintf(stderr, "Unexpected tag `AM_VALUE_%s` (%d); expected `AM_VALUE_%s`.",
discriminant_suffix(value.tag),
value.tag,
discriminant_suffix(discriminant));
AMfreeStack(stack);
exit(EXIT_FAILURE);
}
/**
* \brief Gets the suffix for a discriminant's corresponding string
* representation.
*
* \param[in] discriminant An `AMvalueVariant` enum tag.
* \return A UTF-8 string.
*/
static char const* discriminant_suffix(AMvalueVariant const discriminant) {
char const* suffix = NULL;
switch (discriminant) {
case AM_VALUE_ACTOR_ID: suffix = "ACTOR_ID"; break;
case AM_VALUE_BOOLEAN: suffix = "BOOLEAN"; break;
case AM_VALUE_BYTES: suffix = "BYTES"; break;
case AM_VALUE_CHANGE_HASHES: suffix = "CHANGE_HASHES"; break;
case AM_VALUE_CHANGES: suffix = "CHANGES"; break;
case AM_VALUE_COUNTER: suffix = "COUNTER"; break;
case AM_VALUE_DOC: suffix = "DOC"; break;
case AM_VALUE_F64: suffix = "F64"; break;
case AM_VALUE_INT: suffix = "INT"; break;
case AM_VALUE_LIST_ITEMS: suffix = "LIST_ITEMS"; break;
case AM_VALUE_MAP_ITEMS: suffix = "MAP_ITEMS"; break;
case AM_VALUE_NULL: suffix = "NULL"; break;
case AM_VALUE_OBJ_ID: suffix = "OBJ_ID"; break;
case AM_VALUE_OBJ_ITEMS: suffix = "OBJ_ITEMS"; break;
case AM_VALUE_STR: suffix = "STR"; break;
case AM_VALUE_STRS: suffix = "STRINGS"; break;
case AM_VALUE_SYNC_MESSAGE: suffix = "SYNC_MESSAGE"; break;
case AM_VALUE_SYNC_STATE: suffix = "SYNC_STATE"; break;
case AM_VALUE_TIMESTAMP: suffix = "TIMESTAMP"; break;
case AM_VALUE_UINT: suffix = "UINT"; break;
case AM_VALUE_VOID: suffix = "VOID"; break;
default: suffix = "...";
}
return suffix;
}

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,250 @@
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
find_program (
CARGO_CMD
"cargo"
PATHS "$ENV{CARGO_HOME}/bin"
DOC "The Cargo command"
)
if(NOT CARGO_CMD)
message(FATAL_ERROR "Cargo (Rust package manager) not found! Install it and/or set the CARGO_HOME environment variable.")
endif()
string(TOLOWER "${CMAKE_BUILD_TYPE}" BUILD_TYPE_LOWER)
if(BUILD_TYPE_LOWER STREQUAL debug)
set(CARGO_BUILD_TYPE "debug")
set(CARGO_FLAG "")
else()
set(CARGO_BUILD_TYPE "release")
set(CARGO_FLAG "--release")
endif()
set(CARGO_FEATURES "")
set(CARGO_CURRENT_BINARY_DIR "${CARGO_TARGET_DIR}/${CARGO_BUILD_TYPE}")
set(
CARGO_OUTPUT
${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}
${CARGO_CURRENT_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}
)
if(WIN32)
# \note The basename of an import library output by Cargo is the filename
# of its corresponding shared library.
list(APPEND CARGO_OUTPUT ${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}${CMAKE_STATIC_LIBRARY_SUFFIX})
endif()
add_custom_command(
OUTPUT
${CARGO_OUTPUT}
COMMAND
# \note cbindgen won't regenerate its output header file after it's
# been removed but it will after its configuration file has been
# updated.
${CMAKE_COMMAND} -DCONDITION=NOT_EXISTS -P ${CMAKE_SOURCE_DIR}/cmake/file_touch.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h ${CMAKE_SOURCE_DIR}/cbindgen.toml
COMMAND
${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} CBINDGEN_TARGET_DIR=${CBINDGEN_TARGET_DIR} ${CARGO_CMD} build ${CARGO_FLAG} ${CARGO_FEATURES}
MAIN_DEPENDENCY
lib.rs
DEPENDS
actor_id.rs
byte_span.rs
change_hashes.rs
change.rs
changes.rs
doc.rs
doc/list.rs
doc/list/item.rs
doc/list/items.rs
doc/map.rs
doc/map/item.rs
doc/map/items.rs
doc/utils.rs
obj.rs
obj/item.rs
obj/items.rs
result.rs
result_stack.rs
strs.rs
sync.rs
sync/have.rs
sync/haves.rs
sync/message.rs
sync/state.rs
${CMAKE_SOURCE_DIR}/build.rs
${CMAKE_SOURCE_DIR}/Cargo.toml
${CMAKE_SOURCE_DIR}/cbindgen.toml
WORKING_DIRECTORY
${CMAKE_SOURCE_DIR}
COMMENT
"Producing the library artifacts with Cargo..."
VERBATIM
)
add_custom_target(
${LIBRARY_NAME}_artifacts ALL
DEPENDS ${CARGO_OUTPUT}
)
# \note cbindgen's naming behavior isn't fully configurable and it ignores
# `const fn` calls (https://github.com/eqrion/cbindgen/issues/252).
add_custom_command(
TARGET ${LIBRARY_NAME}_artifacts
POST_BUILD
COMMAND
# Compensate for cbindgen's variant struct naming.
${CMAKE_COMMAND} -DMATCH_REGEX=AM\([^_]+_[^_]+\)_Body -DREPLACE_EXPR=AM\\1 -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
COMMAND
# Compensate for cbindgen's union tag enum type naming.
${CMAKE_COMMAND} -DMATCH_REGEX=AM\([^_]+\)_Tag -DREPLACE_EXPR=AM\\1Variant -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
COMMAND
# Compensate for cbindgen's translation of consecutive uppercase letters to "ScreamingSnakeCase".
${CMAKE_COMMAND} -DMATCH_REGEX=A_M\([^_]+\)_ -DREPLACE_EXPR=AM_\\1_ -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
COMMAND
# Compensate for cbindgen ignoring `std:mem::size_of<usize>()` calls.
${CMAKE_COMMAND} -DMATCH_REGEX=USIZE_ -DREPLACE_EXPR=\+${CMAKE_SIZEOF_VOID_P} -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
WORKING_DIRECTORY
${CMAKE_SOURCE_DIR}
COMMENT
"Compensating for cbindgen deficits..."
VERBATIM
)
if(BUILD_SHARED_LIBS)
if(WIN32)
set(LIBRARY_DESTINATION "${CMAKE_INSTALL_BINDIR}")
else()
set(LIBRARY_DESTINATION "${CMAKE_INSTALL_LIBDIR}")
endif()
set(LIBRARY_DEFINE_SYMBOL "${SYMBOL_PREFIX}_EXPORTS")
# \note The basename of an import library output by Cargo is the filename
# of its corresponding shared library.
set(LIBRARY_IMPLIB "${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}${CMAKE_STATIC_LIBRARY_SUFFIX}")
set(LIBRARY_LOCATION "${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}")
set(LIBRARY_NO_SONAME "${WIN32}")
set(LIBRARY_SONAME "${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_SHARED_LIBRARY_SUFFIX}")
set(LIBRARY_TYPE "SHARED")
else()
set(LIBRARY_DEFINE_SYMBOL "")
set(LIBRARY_DESTINATION "${CMAKE_INSTALL_LIBDIR}")
set(LIBRARY_IMPLIB "")
set(LIBRARY_LOCATION "${CARGO_CURRENT_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}")
set(LIBRARY_NO_SONAME "TRUE")
set(LIBRARY_SONAME "")
set(LIBRARY_TYPE "STATIC")
endif()
add_library(${LIBRARY_NAME} ${LIBRARY_TYPE} IMPORTED GLOBAL)
set_target_properties(
${LIBRARY_NAME}
PROPERTIES
# \note Cargo writes a debug build into a nested directory instead of
# decorating its name.
DEBUG_POSTFIX ""
DEFINE_SYMBOL "${LIBRARY_DEFINE_SYMBOL}"
IMPORTED_IMPLIB "${LIBRARY_IMPLIB}"
IMPORTED_LOCATION "${LIBRARY_LOCATION}"
IMPORTED_NO_SONAME "${LIBRARY_NO_SONAME}"
IMPORTED_SONAME "${LIBRARY_SONAME}"
LINKER_LANGUAGE C
PUBLIC_HEADER "${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h"
SOVERSION "${PROJECT_VERSION_MAJOR}"
VERSION "${PROJECT_VERSION}"
# \note Cargo exports all of the symbols automatically.
WINDOWS_EXPORT_ALL_SYMBOLS "TRUE"
)
target_compile_definitions(${LIBRARY_NAME} INTERFACE $<TARGET_PROPERTY:${LIBRARY_NAME},DEFINE_SYMBOL>)
target_include_directories(
${LIBRARY_NAME}
INTERFACE
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}>"
)
set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
find_package(Threads REQUIRED)
set(LIBRARY_DEPENDENCIES Threads::Threads ${CMAKE_DL_LIBS})
if(WIN32)
list(APPEND LIBRARY_DEPENDENCIES Bcrypt userenv ws2_32)
else()
list(APPEND LIBRARY_DEPENDENCIES m)
endif()
target_link_libraries(${LIBRARY_NAME} INTERFACE ${LIBRARY_DEPENDENCIES})
install(
FILES $<TARGET_PROPERTY:${LIBRARY_NAME},IMPORTED_IMPLIB>
TYPE LIB
# \note The basename of an import library output by Cargo is the filename
# of its corresponding shared library.
RENAME "${CMAKE_STATIC_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_STATIC_LIBRARY_SUFFIX}"
OPTIONAL
)
set(LIBRARY_FILE_NAME "${CMAKE_${LIBRARY_TYPE}_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_${LIBRARY_TYPE}_LIBRARY_SUFFIX}")
install(
FILES $<TARGET_PROPERTY:${LIBRARY_NAME},IMPORTED_LOCATION>
RENAME "${LIBRARY_FILE_NAME}"
DESTINATION ${LIBRARY_DESTINATION}
)
install(
FILES $<TARGET_PROPERTY:${LIBRARY_NAME},PUBLIC_HEADER>
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
)
find_package(Doxygen OPTIONAL_COMPONENTS dot)
if(DOXYGEN_FOUND)
set(DOXYGEN_ALIASES "installed_headerfile=\\headerfile ${LIBRARY_NAME}.h <${PROJECT_NAME}/${LIBRARY_NAME}.h>")
set(DOXYGEN_GENERATE_LATEX YES)
set(DOXYGEN_PDF_HYPERLINKS YES)
set(DOXYGEN_PROJECT_LOGO "${CMAKE_SOURCE_DIR}/img/brandmark.png")
set(DOXYGEN_SORT_BRIEF_DOCS YES)
set(DOXYGEN_USE_MDFILE_AS_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md")
doxygen_add_docs(
${LIBRARY_NAME}_docs
"${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h"
"${CMAKE_SOURCE_DIR}/README.md"
USE_STAMP_FILE
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Producing documentation with Doxygen..."
)
# \note A Doxygen input file isn't a file-level dependency so the Doxygen
# command must instead depend upon a target that outputs the file or
# it will just output an error message when it can't be found.
add_dependencies(${LIBRARY_NAME}_docs ${LIBRARY_NAME}_artifacts)
endif()

166
automerge-c/src/actor_id.rs Normal file
View file

@ -0,0 +1,166 @@
use automerge as am;
use std::cell::RefCell;
use std::cmp::Ordering;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::str::FromStr;
use crate::byte_span::AMbyteSpan;
use crate::result::{to_result, AMresult};
/// \struct AMactorId
/// \installed_headerfile
/// \brief An actor's unique identifier.
#[derive(Eq, PartialEq)]
pub struct AMactorId {
body: *const am::ActorId,
c_str: RefCell<Option<CString>>,
}
impl AMactorId {
pub fn new(actor_id: &am::ActorId) -> Self {
Self {
body: actor_id,
c_str: Default::default(),
}
}
pub fn as_c_str(&self) -> *const c_char {
let mut c_str = self.c_str.borrow_mut();
match c_str.as_mut() {
None => {
let hex_str = unsafe { (*self.body).to_hex_string() };
c_str.insert(CString::new(hex_str).unwrap()).as_ptr()
}
Some(hex_str) => hex_str.as_ptr(),
}
}
}
impl AsRef<am::ActorId> for AMactorId {
fn as_ref(&self) -> &am::ActorId {
unsafe { &*self.body }
}
}
/// \memberof AMactorId
/// \brief Gets the value of an actor identifier as a sequence of bytes.
///
/// \param[in] actor_id A pointer to an `AMactorId` struct.
/// \pre \p actor_id `!= NULL`.
/// \return An `AMbyteSpan` struct.
/// \internal
///
/// # Safety
/// actor_id must be a valid pointer to an AMactorId
#[no_mangle]
pub unsafe extern "C" fn AMactorIdBytes(actor_id: *const AMactorId) -> AMbyteSpan {
match actor_id.as_ref() {
Some(actor_id) => actor_id.as_ref().into(),
None => AMbyteSpan::default(),
}
}
/// \memberof AMactorId
/// \brief Compares two actor identifiers.
///
/// \param[in] actor_id1 A pointer to an `AMactorId` struct.
/// \param[in] actor_id2 A pointer to an `AMactorId` struct.
/// \return `-1` if \p actor_id1 `<` \p actor_id2, `0` if
/// \p actor_id1 `==` \p actor_id2 and `1` if
/// \p actor_id1 `>` \p actor_id2.
/// \pre \p actor_id1 `!= NULL`.
/// \pre \p actor_id2 `!= NULL`.
/// \internal
///
/// #Safety
/// actor_id1 must be a valid pointer to an AMactorId
/// actor_id2 must be a valid pointer to an AMactorId
#[no_mangle]
pub unsafe extern "C" fn AMactorIdCmp(
actor_id1: *const AMactorId,
actor_id2: *const AMactorId,
) -> isize {
match (actor_id1.as_ref(), actor_id2.as_ref()) {
(Some(actor_id1), Some(actor_id2)) => match actor_id1.as_ref().cmp(actor_id2.as_ref()) {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
},
(None, Some(_)) => -1,
(Some(_), None) => 1,
(None, None) => 0,
}
}
/// \memberof AMactorId
/// \brief Allocates a new actor identifier and initializes it with a random
/// UUID.
///
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMactorId` struct.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
#[no_mangle]
pub unsafe extern "C" fn AMactorIdInit() -> *mut AMresult {
to_result(Ok::<am::ActorId, am::AutomergeError>(am::ActorId::random()))
}
/// \memberof AMactorId
/// \brief Allocates a new actor identifier and initializes it from a sequence
/// of bytes.
///
/// \param[in] src A pointer to a contiguous sequence of bytes.
/// \param[in] count The number of bytes to copy from \p src.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMactorId` struct.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMactorIdInitBytes(src: *const u8, count: usize) -> *mut AMresult {
let slice = std::slice::from_raw_parts(src, count);
to_result(Ok::<am::ActorId, am::InvalidActorId>(am::ActorId::from(
slice,
)))
}
/// \memberof AMactorId
/// \brief Allocates a new actor identifier and initializes it from a
/// hexadecimal string.
///
/// \param[in] hex_str A UTF-8 string.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMactorId` struct.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// hex_str must be a null-terminated array of `c_char`
#[no_mangle]
pub unsafe extern "C" fn AMactorIdInitStr(hex_str: *const c_char) -> *mut AMresult {
to_result(am::ActorId::from_str(
CStr::from_ptr(hex_str).to_str().unwrap(),
))
}
/// \memberof AMactorId
/// \brief Gets the value of an actor identifier as a hexadecimal string.
///
/// \param[in] actor_id A pointer to an `AMactorId` struct.
/// \pre \p actor_id `!= NULL`.
/// \return A UTF-8 string.
/// \internal
///
/// # Safety
/// actor_id must be a valid pointer to an AMactorId
#[no_mangle]
pub unsafe extern "C" fn AMactorIdStr(actor_id: *const AMactorId) -> *const c_char {
match actor_id.as_ref() {
Some(actor_id) => actor_id.as_c_str(),
None => std::ptr::null::<c_char>(),
}
}

View file

@ -0,0 +1,64 @@
use automerge as am;
/// \struct AMbyteSpan
/// \installed_headerfile
/// \brief A view onto a contiguous sequence of bytes.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMbyteSpan {
/// A pointer to an array of bytes.
/// \attention <b>NEVER CALL `free()` ON \p src!</b>
/// \warning \p src is only valid until the `AMfree()` function is called
/// on the `AMresult` struct that stores the array of bytes to
/// which it points.
pub src: *const u8,
/// The number of bytes in the array.
pub count: usize,
}
impl Default for AMbyteSpan {
fn default() -> Self {
Self {
src: std::ptr::null(),
count: 0,
}
}
}
impl From<&am::ActorId> for AMbyteSpan {
fn from(actor: &am::ActorId) -> Self {
let slice = actor.to_bytes();
Self {
src: slice.as_ptr(),
count: slice.len(),
}
}
}
impl From<&mut am::ActorId> for AMbyteSpan {
fn from(actor: &mut am::ActorId) -> Self {
let slice = actor.to_bytes();
Self {
src: slice.as_ptr(),
count: slice.len(),
}
}
}
impl From<&am::ChangeHash> for AMbyteSpan {
fn from(change_hash: &am::ChangeHash) -> Self {
Self {
src: change_hash.0.as_ptr(),
count: change_hash.0.len(),
}
}
}
impl From<&[u8]> for AMbyteSpan {
fn from(slice: &[u8]) -> Self {
Self {
src: slice.as_ptr(),
count: slice.len(),
}
}
}

View file

@ -1,7 +1,10 @@
use automerge as am;
use std::cell::RefCell;
use std::ffi::CString;
use std::os::raw::c_char;
use crate::byte_span::AMbyteSpan;
use crate::change_hashes::AMchangeHashes;
use crate::result::{to_result, AMresult};
macro_rules! to_change {
@ -9,7 +12,7 @@ macro_rules! to_change {
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::error("Invalid `AMchange*`").into(),
None => return AMresult::err("Invalid AMchange pointer").into(),
}
}};
}
@ -20,31 +23,43 @@ macro_rules! to_change {
#[derive(Eq, PartialEq)]
pub struct AMchange {
body: *mut am::Change,
change_hash: RefCell<Option<am::ChangeHash>>,
c_msg: RefCell<Option<CString>>,
c_changehash: RefCell<Option<am::ChangeHash>>,
}
impl AMchange {
pub fn new(change: &mut am::Change) -> Self {
Self {
body: change,
change_hash: Default::default(),
c_msg: Default::default(),
c_changehash: Default::default(),
}
}
pub fn message(&self) -> AMbyteSpan {
if let Some(message) = unsafe { (*self.body).message() } {
return message.as_str().as_bytes().into();
pub fn message(&self) -> *const c_char {
let mut c_msg = self.c_msg.borrow_mut();
match c_msg.as_mut() {
None => {
if let Some(message) = unsafe { (*self.body).message() } {
return c_msg
.insert(CString::new(message.as_bytes()).unwrap())
.as_ptr();
}
}
Some(message) => {
return message.as_ptr();
}
}
Default::default()
std::ptr::null()
}
pub fn hash(&self) -> AMbyteSpan {
let mut change_hash = self.change_hash.borrow_mut();
if let Some(change_hash) = change_hash.as_ref() {
change_hash.into()
let mut c_changehash = self.c_changehash.borrow_mut();
if let Some(c_changehash) = c_changehash.as_ref() {
c_changehash.into()
} else {
let hash = unsafe { (*self.body).hash() };
let ptr = change_hash.insert(hash);
let ptr = c_changehash.insert(hash);
AMbyteSpan {
src: ptr.0.as_ptr(),
count: hash.as_ref().len(),
@ -69,12 +84,12 @@ impl AsRef<am::Change> for AMchange {
/// \brief Gets the first referenced actor identifier in a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_ACTOR_ID` item.
/// \pre \p change `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \pre \p change `!= NULL`.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMactorId` struct.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// change must be a valid pointer to an AMchange
#[no_mangle]
@ -88,8 +103,8 @@ pub unsafe extern "C" fn AMchangeActorId(change: *const AMchange) -> *mut AMresu
/// \memberof AMchange
/// \brief Compresses the raw bytes of a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \pre \p change `!= NULL`
/// \param[in,out] change A pointer to an `AMchange` struct.
/// \pre \p change `!= NULL`.
/// \internal
///
/// # Safety
@ -105,20 +120,18 @@ pub unsafe extern "C" fn AMchangeCompress(change: *mut AMchange) {
/// \brief Gets the dependencies of a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p change `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return A pointer to an `AMchangeHashes` struct or `NULL`.
/// \pre \p change `!= NULL`.
/// \internal
///
/// # Safety
/// change must be a valid pointer to an AMchange
#[no_mangle]
pub unsafe extern "C" fn AMchangeDeps(change: *const AMchange) -> *mut AMresult {
to_result(match change.as_ref() {
Some(change) => change.as_ref().deps(),
None => Default::default(),
})
pub unsafe extern "C" fn AMchangeDeps(change: *const AMchange) -> AMchangeHashes {
match change.as_ref() {
Some(change) => AMchangeHashes::new(change.as_ref().deps()),
None => AMchangeHashes::default(),
}
}
/// \memberof AMchange
@ -126,7 +139,7 @@ pub unsafe extern "C" fn AMchangeDeps(change: *const AMchange) -> *mut AMresult
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return An `AMbyteSpan` struct.
/// \pre \p change `!= NULL`
/// \pre \p change `!= NULL`.
/// \internal
///
/// # Safety
@ -136,38 +149,36 @@ pub unsafe extern "C" fn AMchangeExtraBytes(change: *const AMchange) -> AMbyteSp
if let Some(change) = change.as_ref() {
change.as_ref().extra_bytes().into()
} else {
Default::default()
AMbyteSpan::default()
}
}
/// \memberof AMchange
/// \brief Allocates a new change and initializes it from an array of bytes value.
/// \brief Loads a sequence of bytes into a change.
///
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The count of bytes to load from the array pointed to by
/// \p src.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_CHANGE` item.
/// \pre \p src `!= NULL`
/// \pre `sizeof(`\p src `) > 0`
/// \pre \p count `<= sizeof(`\p src `)`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] count The number of bytes in \p src to load.
/// \return A pointer to an `AMresult` struct containing an `AMchange` struct.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// src must be a byte array of length `>= count`
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMchangeFromBytes(src: *const u8, count: usize) -> *mut AMresult {
let data = std::slice::from_raw_parts(src, count);
to_result(am::Change::from_bytes(data.to_vec()))
let mut data = Vec::new();
data.extend_from_slice(std::slice::from_raw_parts(src, count));
to_result(am::Change::from_bytes(data))
}
/// \memberof AMchange
/// \brief Gets the hash of a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return An `AMbyteSpan` struct for a change hash.
/// \pre \p change `!= NULL`
/// \return A change hash as an `AMbyteSpan` struct.
/// \pre \p change `!= NULL`.
/// \internal
///
/// # Safety
@ -176,7 +187,7 @@ pub unsafe extern "C" fn AMchangeFromBytes(src: *const u8, count: usize) -> *mut
pub unsafe extern "C" fn AMchangeHash(change: *const AMchange) -> AMbyteSpan {
match change.as_ref() {
Some(change) => change.hash(),
None => Default::default(),
None => AMbyteSpan::default(),
}
}
@ -184,8 +195,8 @@ pub unsafe extern "C" fn AMchangeHash(change: *const AMchange) -> AMbyteSpan {
/// \brief Tests the emptiness of a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return `true` if \p change is empty, `false` otherwise.
/// \pre \p change `!= NULL`
/// \return A boolean.
/// \pre \p change `!= NULL`.
/// \internal
///
/// # Safety
@ -199,37 +210,12 @@ pub unsafe extern "C" fn AMchangeIsEmpty(change: *const AMchange) -> bool {
}
}
/// \memberof AMchange
/// \brief Loads a document into a sequence of changes.
///
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The count of bytes to load from the array pointed to by
/// \p src.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE` items.
/// \pre \p src `!= NULL`
/// \pre `sizeof(`\p src `) > 0`
/// \pre \p count `<= sizeof(`\p src `)`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
/// src must be a byte array of length `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMchangeLoadDocument(src: *const u8, count: usize) -> *mut AMresult {
let data = std::slice::from_raw_parts(src, count);
to_result::<Result<Vec<am::Change>, _>>(
am::Automerge::load(data)
.and_then(|d| d.get_changes(&[]).map(|c| c.into_iter().cloned().collect())),
)
}
/// \memberof AMchange
/// \brief Gets the maximum operation index of a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return A 64-bit unsigned integer.
/// \pre \p change `!= NULL`
/// \pre \p change `!= NULL`.
/// \internal
///
/// # Safety
@ -247,18 +233,18 @@ pub unsafe extern "C" fn AMchangeMaxOp(change: *const AMchange) -> u64 {
/// \brief Gets the message of a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return An `AMbyteSpan` struct for a UTF-8 string.
/// \pre \p change `!= NULL`
/// \return A UTF-8 string or `NULL`.
/// \pre \p change `!= NULL`.
/// \internal
///
/// # Safety
/// change must be a valid pointer to an AMchange
#[no_mangle]
pub unsafe extern "C" fn AMchangeMessage(change: *const AMchange) -> AMbyteSpan {
pub unsafe extern "C" fn AMchangeMessage(change: *const AMchange) -> *const c_char {
if let Some(change) = change.as_ref() {
return change.message();
};
Default::default()
std::ptr::null()
}
/// \memberof AMchange
@ -266,7 +252,7 @@ pub unsafe extern "C" fn AMchangeMessage(change: *const AMchange) -> AMbyteSpan
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return A 64-bit unsigned integer.
/// \pre \p change `!= NULL`
/// \pre \p change `!= NULL`.
/// \internal
///
/// # Safety
@ -285,7 +271,7 @@ pub unsafe extern "C" fn AMchangeSeq(change: *const AMchange) -> u64 {
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return A 64-bit unsigned integer.
/// \pre \p change `!= NULL`
/// \pre \p change `!= NULL`.
/// \internal
///
/// # Safety
@ -293,9 +279,10 @@ pub unsafe extern "C" fn AMchangeSeq(change: *const AMchange) -> u64 {
#[no_mangle]
pub unsafe extern "C" fn AMchangeSize(change: *const AMchange) -> usize {
if let Some(change) = change.as_ref() {
return change.as_ref().len();
change.as_ref().len()
} else {
0
}
0
}
/// \memberof AMchange
@ -303,7 +290,7 @@ pub unsafe extern "C" fn AMchangeSize(change: *const AMchange) -> usize {
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return A 64-bit unsigned integer.
/// \pre \p change `!= NULL`
/// \pre \p change `!= NULL`.
/// \internal
///
/// # Safety
@ -322,7 +309,7 @@ pub unsafe extern "C" fn AMchangeStartOp(change: *const AMchange) -> u64 {
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return A 64-bit signed integer.
/// \pre \p change `!= NULL`
/// \pre \p change `!= NULL`.
/// \internal
///
/// # Safety
@ -340,8 +327,8 @@ pub unsafe extern "C" fn AMchangeTime(change: *const AMchange) -> i64 {
/// \brief Gets the raw bytes of a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return An `AMbyteSpan` struct for an array of bytes.
/// \pre \p change `!= NULL`
/// \return An `AMbyteSpan` struct.
/// \pre \p change `!= NULL`.
/// \internal
///
/// # Safety
@ -351,6 +338,30 @@ pub unsafe extern "C" fn AMchangeRawBytes(change: *const AMchange) -> AMbyteSpan
if let Some(change) = change.as_ref() {
change.as_ref().raw_bytes().into()
} else {
Default::default()
AMbyteSpan::default()
}
}
/// \memberof AMchange
/// \brief Loads a document into a sequence of changes.
///
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The number of bytes in \p src to load.
/// \return A pointer to an `AMresult` struct containing a sequence of
/// `AMchange` structs.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMchangeLoadDocument(src: *const u8, count: usize) -> *mut AMresult {
let mut data = Vec::new();
data.extend_from_slice(std::slice::from_raw_parts(src, count));
to_result::<Result<Vec<am::Change>, _>>(
am::Automerge::load(&data)
.and_then(|d| d.get_changes(&[]).map(|c| c.into_iter().cloned().collect())),
)
}

View file

@ -0,0 +1,399 @@
use automerge as am;
use std::cmp::Ordering;
use std::ffi::c_void;
use std::mem::size_of;
use crate::byte_span::AMbyteSpan;
use crate::result::{to_result, AMresult};
#[repr(C)]
struct Detail {
len: usize,
offset: isize,
ptr: *const c_void,
}
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
/// propagate the name of a constant initialized from it so if the
/// constant's name is a symbolic representation of the value it can be
/// converted into a number by post-processing the header it generated.
pub const USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
impl Detail {
fn new(change_hashes: &[am::ChangeHash], offset: isize) -> Self {
Self {
len: change_hashes.len(),
offset,
ptr: change_hashes.as_ptr() as *const c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, len))
}
}
}
pub fn get_index(&self) -> usize {
(self.offset
+ if self.offset < 0 {
self.len as isize
} else {
0
}) as usize
}
pub fn next(&mut self, n: isize) -> Option<&am::ChangeHash> {
if self.is_stopped() {
return None;
}
let slice: &[am::ChangeHash] =
unsafe { std::slice::from_raw_parts(self.ptr as *const am::ChangeHash, self.len) };
let value = &slice[self.get_index()];
self.advance(n);
Some(value)
}
pub fn is_stopped(&self) -> bool {
let len = self.len as isize;
self.offset < -len || self.offset == len
}
pub fn prev(&mut self, n: isize) -> Option<&am::ChangeHash> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &[am::ChangeHash] =
unsafe { std::slice::from_raw_parts(self.ptr as *const am::ChangeHash, self.len) };
Some(&slice[self.get_index()])
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
ptr: self.ptr,
}
}
}
impl From<Detail> for [u8; USIZE_USIZE_USIZE_] {
fn from(detail: Detail) -> Self {
unsafe {
std::slice::from_raw_parts((&detail as *const Detail) as *const u8, USIZE_USIZE_USIZE_)
.try_into()
.unwrap()
}
}
}
/// \struct AMchangeHashes
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of change hashes.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMchangeHashes {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_],
}
impl AMchangeHashes {
pub fn new(change_hashes: &[am::ChangeHash]) -> Self {
Self {
detail: Detail::new(change_hashes, 0).into(),
}
}
pub fn advance(&mut self, n: isize) {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.advance(n);
}
pub fn len(&self) -> usize {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
detail.len
}
pub fn next(&mut self, n: isize) -> Option<&am::ChangeHash> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<&am::ChangeHash> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.prev(n)
}
pub fn reversed(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.reversed().into(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
}
}
}
impl AsRef<[am::ChangeHash]> for AMchangeHashes {
fn as_ref(&self) -> &[am::ChangeHash] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const am::ChangeHash, detail.len) }
}
}
impl Default for AMchangeHashes {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMchangeHashes
/// \brief Advances an iterator over a sequence of change hashes by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] change_hashes A pointer to an `AMchangeHashes` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p change_hashes `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes must be a valid pointer to an AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesAdvance(change_hashes: *mut AMchangeHashes, n: isize) {
if let Some(change_hashes) = change_hashes.as_mut() {
change_hashes.advance(n);
};
}
/// \memberof AMchangeHashes
/// \brief Compares the sequences of change hashes underlying a pair of
/// iterators.
///
/// \param[in] change_hashes1 A pointer to an `AMchangeHashes` struct.
/// \param[in] change_hashes2 A pointer to an `AMchangeHashes` struct.
/// \return `-1` if \p change_hashes1 `<` \p change_hashes2, `0` if
/// \p change_hashes1 `==` \p change_hashes2 and `1` if
/// \p change_hashes1 `>` \p change_hashes2.
/// \pre \p change_hashes1 `!= NULL`.
/// \pre \p change_hashes2 `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes1 must be a valid pointer to an AMchangeHashes
/// change_hashes2 must be a valid pointer to an AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesCmp(
change_hashes1: *const AMchangeHashes,
change_hashes2: *const AMchangeHashes,
) -> isize {
match (change_hashes1.as_ref(), change_hashes2.as_ref()) {
(Some(change_hashes1), Some(change_hashes2)) => {
match change_hashes1.as_ref().cmp(change_hashes2.as_ref()) {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
}
}
(None, Some(_)) => -1,
(Some(_), None) => 1,
(None, None) => 0,
}
}
/// \memberof AMchangeHashes
/// \brief Allocates an iterator over a sequence of change hashes and
/// initializes it from a sequence of byte spans.
///
/// \param[in] src A pointer to an array of `AMbyteSpan` structs.
/// \param[in] count The number of `AMbyteSpan` structs to copy from \p src.
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
/// struct.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`) / sizeof(AMbyteSpan)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// src must be an AMbyteSpan array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesInit(src: *const AMbyteSpan, count: usize) -> *mut AMresult {
let mut change_hashes = Vec::<am::ChangeHash>::new();
for n in 0..count {
let byte_span = &*src.add(n);
let slice = std::slice::from_raw_parts(byte_span.src, byte_span.count);
match slice.try_into() {
Ok(change_hash) => {
change_hashes.push(change_hash);
}
Err(e) => {
return to_result(Err(e));
}
}
}
to_result(Ok::<Vec<am::ChangeHash>, am::InvalidChangeHashSlice>(
change_hashes,
))
}
/// \memberof AMchangeHashes
/// \brief Gets the change hash at the current position of an iterator over a
/// sequence of change hashes and then advances it by at most \p |n|
/// positions where the sign of \p n is relative to the iterator's
/// direction.
///
/// \param[in,out] change_hashes A pointer to an `AMchangeHashes` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return An `AMbyteSpan` struct with `.src == NULL` when \p change_hashes
/// was previously advanced past its forward/reverse limit.
/// \pre \p change_hashes `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes must be a valid pointer to an AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesNext(
change_hashes: *mut AMchangeHashes,
n: isize,
) -> AMbyteSpan {
if let Some(change_hashes) = change_hashes.as_mut() {
if let Some(change_hash) = change_hashes.next(n) {
return change_hash.into();
}
}
AMbyteSpan::default()
}
/// \memberof AMchangeHashes
/// \brief Advances an iterator over a sequence of change hashes by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the change hash at its new
/// position.
///
/// \param[in,out] change_hashes A pointer to an `AMchangeHashes` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return An `AMbyteSpan` struct with `.src == NULL` when \p change_hashes is
/// presently advanced past its forward/reverse limit.
/// \pre \p change_hashes `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes must be a valid pointer to an AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesPrev(
change_hashes: *mut AMchangeHashes,
n: isize,
) -> AMbyteSpan {
if let Some(change_hashes) = change_hashes.as_mut() {
if let Some(change_hash) = change_hashes.prev(n) {
return change_hash.into();
}
}
AMbyteSpan::default()
}
/// \memberof AMchangeHashes
/// \brief Gets the size of the sequence of change hashes underlying an
/// iterator.
///
/// \param[in] change_hashes A pointer to an `AMchangeHashes` struct.
/// \return The count of values in \p change_hashes.
/// \pre \p change_hashes `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes must be a valid pointer to an AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesSize(change_hashes: *const AMchangeHashes) -> usize {
if let Some(change_hashes) = change_hashes.as_ref() {
change_hashes.len()
} else {
0
}
}
/// \memberof AMchangeHashes
/// \brief Creates an iterator over the same sequence of change hashes as the
/// given one but with the opposite position and direction.
///
/// \param[in] change_hashes A pointer to an `AMchangeHashes` struct.
/// \return An `AMchangeHashes` struct
/// \pre \p change_hashes `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes must be a valid pointer to an AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesReversed(
change_hashes: *const AMchangeHashes,
) -> AMchangeHashes {
if let Some(change_hashes) = change_hashes.as_ref() {
change_hashes.reversed()
} else {
AMchangeHashes::default()
}
}
/// \memberof AMchangeHashes
/// \brief Creates an iterator at the starting position over the same sequence
/// of change hashes as the given one.
///
/// \param[in] change_hashes A pointer to an `AMchangeHashes` struct.
/// \return An `AMchangeHashes` struct
/// \pre \p change_hashes `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes must be a valid pointer to an AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesRewound(
change_hashes: *const AMchangeHashes,
) -> AMchangeHashes {
if let Some(change_hashes) = change_hashes.as_ref() {
change_hashes.rewound()
} else {
AMchangeHashes::default()
}
}

398
automerge-c/src/changes.rs Normal file
View file

@ -0,0 +1,398 @@
use automerge as am;
use std::collections::BTreeMap;
use std::ffi::c_void;
use std::mem::size_of;
use crate::byte_span::AMbyteSpan;
use crate::change::AMchange;
use crate::result::{to_result, AMresult};
#[repr(C)]
struct Detail {
len: usize,
offset: isize,
ptr: *const c_void,
storage: *mut c_void,
}
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
/// propagate the name of a constant initialized from it so if the
/// constant's name is a symbolic representation of the value it can be
/// converted into a number by post-processing the header it generated.
pub const USIZE_USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
impl Detail {
fn new(changes: &[am::Change], offset: isize, storage: &mut BTreeMap<usize, AMchange>) -> Self {
let storage: *mut BTreeMap<usize, AMchange> = storage;
Self {
len: changes.len(),
offset,
ptr: changes.as_ptr() as *const c_void,
storage: storage as *mut c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, len))
}
}
}
pub fn get_index(&self) -> usize {
(self.offset
+ if self.offset < 0 {
self.len as isize
} else {
0
}) as usize
}
pub fn next(&mut self, n: isize) -> Option<*const AMchange> {
if self.is_stopped() {
return None;
}
let slice: &mut [am::Change] =
unsafe { std::slice::from_raw_parts_mut(self.ptr as *mut am::Change, self.len) };
let storage = unsafe { &mut *(self.storage as *mut BTreeMap<usize, AMchange>) };
let index = self.get_index();
let value = match storage.get_mut(&index) {
Some(value) => value,
None => {
storage.insert(index, AMchange::new(&mut slice[index]));
storage.get_mut(&index).unwrap()
}
};
self.advance(n);
Some(value)
}
pub fn is_stopped(&self) -> bool {
let len = self.len as isize;
self.offset < -len || self.offset == len
}
pub fn prev(&mut self, n: isize) -> Option<*const AMchange> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &mut [am::Change] =
unsafe { std::slice::from_raw_parts_mut(self.ptr as *mut am::Change, self.len) };
let storage = unsafe { &mut *(self.storage as *mut BTreeMap<usize, AMchange>) };
let index = self.get_index();
Some(match storage.get_mut(&index) {
Some(value) => value,
None => {
storage.insert(index, AMchange::new(&mut slice[index]));
storage.get_mut(&index).unwrap()
}
})
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
storage: self.storage,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
ptr: self.ptr,
storage: self.storage,
}
}
}
impl From<Detail> for [u8; USIZE_USIZE_USIZE_USIZE_] {
fn from(detail: Detail) -> Self {
unsafe {
std::slice::from_raw_parts(
(&detail as *const Detail) as *const u8,
USIZE_USIZE_USIZE_USIZE_,
)
.try_into()
.unwrap()
}
}
}
/// \struct AMchanges
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of changes.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMchanges {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_USIZE_],
}
impl AMchanges {
pub fn new(changes: &[am::Change], storage: &mut BTreeMap<usize, AMchange>) -> Self {
Self {
detail: Detail::new(changes, 0, &mut *storage).into(),
}
}
pub fn advance(&mut self, n: isize) {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.advance(n);
}
pub fn len(&self) -> usize {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
detail.len
}
pub fn next(&mut self, n: isize) -> Option<*const AMchange> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<*const AMchange> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.prev(n)
}
pub fn reversed(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.reversed().into(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
}
}
}
impl AsRef<[am::Change]> for AMchanges {
fn as_ref(&self) -> &[am::Change] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const am::Change, detail.len) }
}
}
impl Default for AMchanges {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMchanges
/// \brief Advances an iterator over a sequence of changes by at most \p |n|
/// positions where the sign of \p n is relative to the iterator's
/// direction.
///
/// \param[in,out] changes A pointer to an `AMchanges` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p changes `!= NULL`.
/// \internal
///
/// #Safety
/// changes must be a valid pointer to an AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesAdvance(changes: *mut AMchanges, n: isize) {
if let Some(changes) = changes.as_mut() {
changes.advance(n);
};
}
/// \memberof AMchanges
/// \brief Tests the equality of two sequences of changes underlying a pair of
/// iterators.
///
/// \param[in] changes1 A pointer to an `AMchanges` struct.
/// \param[in] changes2 A pointer to an `AMchanges` struct.
/// \return `true` if \p changes1 `==` \p changes2 and `false` otherwise.
/// \pre \p changes1 `!= NULL`.
/// \pre \p changes2 `!= NULL`.
/// \internal
///
/// #Safety
/// changes1 must be a valid pointer to an AMchanges
/// changes2 must be a valid pointer to an AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesEqual(
changes1: *const AMchanges,
changes2: *const AMchanges,
) -> bool {
match (changes1.as_ref(), changes2.as_ref()) {
(Some(changes1), Some(changes2)) => changes1.as_ref() == changes2.as_ref(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMchanges
/// \brief Allocates an iterator over a sequence of changes and initializes it
/// from a sequence of byte spans.
///
/// \param[in] src A pointer to an array of `AMbyteSpan` structs.
/// \param[in] count The number of `AMbyteSpan` structs to copy from \p src.
/// \return A pointer to an `AMresult` struct containing an `AMchanges` struct.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`) / sizeof(AMbyteSpan)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// src must be an AMbyteSpan array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMchangesInit(src: *const AMbyteSpan, count: usize) -> *mut AMresult {
let mut changes = Vec::<am::Change>::new();
for n in 0..count {
let byte_span = &*src.add(n);
let slice = std::slice::from_raw_parts(byte_span.src, byte_span.count);
match slice.try_into() {
Ok(change) => {
changes.push(change);
}
Err(e) => {
return to_result(Err::<Vec<am::Change>, am::LoadChangeError>(e));
}
}
}
to_result(Ok::<Vec<am::Change>, am::LoadChangeError>(changes))
}
/// \memberof AMchanges
/// \brief Gets the change at the current position of an iterator over a
/// sequence of changes and then advances it by at most \p |n| positions
/// where the sign of \p n is relative to the iterator's direction.
///
/// \param[in,out] changes A pointer to an `AMchanges` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMchange` struct that's `NULL` when \p changes was
/// previously advanced past its forward/reverse limit.
/// \pre \p changes `!= NULL`.
/// \internal
///
/// #Safety
/// changes must be a valid pointer to an AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesNext(changes: *mut AMchanges, n: isize) -> *const AMchange {
if let Some(changes) = changes.as_mut() {
if let Some(change) = changes.next(n) {
return change;
}
}
std::ptr::null()
}
/// \memberof AMchanges
/// \brief Advances an iterator over a sequence of changes by at most \p |n|
/// positions where the sign of \p n is relative to the iterator's
/// direction and then gets the change at its new position.
///
/// \param[in,out] changes A pointer to an `AMchanges` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMchange` struct that's `NULL` when \p changes is
/// presently advanced past its forward/reverse limit.
/// \pre \p changes `!= NULL`.
/// \internal
///
/// #Safety
/// changes must be a valid pointer to an AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesPrev(changes: *mut AMchanges, n: isize) -> *const AMchange {
if let Some(changes) = changes.as_mut() {
if let Some(change) = changes.prev(n) {
return change;
}
}
std::ptr::null()
}
/// \memberof AMchanges
/// \brief Gets the size of the sequence of changes underlying an iterator.
///
/// \param[in] changes A pointer to an `AMchanges` struct.
/// \return The count of values in \p changes.
/// \pre \p changes `!= NULL`.
/// \internal
///
/// #Safety
/// changes must be a valid pointer to an AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesSize(changes: *const AMchanges) -> usize {
if let Some(changes) = changes.as_ref() {
changes.len()
} else {
0
}
}
/// \memberof AMchanges
/// \brief Creates an iterator over the same sequence of changes as the given
/// one but with the opposite position and direction.
///
/// \param[in] changes A pointer to an `AMchanges` struct.
/// \return An `AMchanges` struct.
/// \pre \p changes `!= NULL`.
/// \internal
///
/// #Safety
/// changes must be a valid pointer to an AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesReversed(changes: *const AMchanges) -> AMchanges {
if let Some(changes) = changes.as_ref() {
changes.reversed()
} else {
AMchanges::default()
}
}
/// \memberof AMchanges
/// \brief Creates an iterator at the starting position over the same sequence
/// of changes as the given one.
///
/// \param[in] changes A pointer to an `AMchanges` struct.
/// \return An `AMchanges` struct
/// \pre \p changes `!= NULL`.
/// \internal
///
/// #Safety
/// changes must be a valid pointer to an AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesRewound(changes: *const AMchanges) -> AMchanges {
if let Some(changes) = changes.as_ref() {
changes.rewound()
} else {
AMchanges::default()
}
}

833
automerge-c/src/doc.rs Normal file
View file

@ -0,0 +1,833 @@
use automerge as am;
use automerge::transaction::{CommitOptions, Transactable};
use std::ops::{Deref, DerefMut};
use std::os::raw::c_char;
use crate::actor_id::AMactorId;
use crate::change_hashes::AMchangeHashes;
use crate::obj::AMobjId;
use crate::result::{to_result, AMresult, AMvalue};
use crate::sync::{to_sync_message, AMsyncMessage, AMsyncState};
pub mod list;
pub mod map;
pub mod utils;
use crate::changes::AMchanges;
use crate::doc::utils::to_str;
use crate::doc::utils::{to_actor_id, to_doc, to_doc_mut, to_obj_id};
macro_rules! to_changes {
($handle:expr) => {{
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::err("Invalid AMchanges pointer").into(),
}
}};
}
macro_rules! to_index {
($index:expr, $len:expr, $param_name:expr) => {{
if $index > $len && $index != usize::MAX {
return AMresult::err(&format!("Invalid {} {}", $param_name, $index)).into();
}
std::cmp::min($index, $len)
}};
}
macro_rules! to_sync_state_mut {
($handle:expr) => {{
let handle = $handle.as_mut();
match handle {
Some(b) => b,
None => return AMresult::err("Invalid AMsyncState pointer").into(),
}
}};
}
/// \struct AMdoc
/// \installed_headerfile
/// \brief A JSON-like CRDT.
#[derive(Clone)]
pub struct AMdoc(am::AutoCommit);
impl AMdoc {
pub fn new(auto_commit: am::AutoCommit) -> Self {
Self(auto_commit)
}
}
impl AsRef<am::AutoCommit> for AMdoc {
fn as_ref(&self) -> &am::AutoCommit {
&self.0
}
}
impl Deref for AMdoc {
type Target = am::AutoCommit;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for AMdoc {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
/// \memberof AMdoc
/// \brief Applies a sequence of changes to a document.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] changes A pointer to an `AMchanges` struct.
/// \pre \p doc `!= NULL`.
/// \pre \p changes `!= NULL`.
/// \return A pointer to an `AMresult` struct containing a void.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// changes must be a valid pointer to an AMchanges.
#[no_mangle]
pub unsafe extern "C" fn AMapplyChanges(
doc: *mut AMdoc,
changes: *const AMchanges,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let changes = to_changes!(changes);
to_result(doc.apply_changes(changes.as_ref().to_vec()))
}
/// \memberof AMdoc
/// \brief Allocates storage for a document and initializes it by duplicating
/// the given document.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMdoc` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMclone(doc: *const AMdoc) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.as_ref().clone())
}
/// \memberof AMdoc
/// \brief Allocates a new document and initializes it with defaults.
///
/// \param[in] actor_id A pointer to an `AMactorId` struct or `NULL` for a
/// random one.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMdoc` struct.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
///
/// # Safety
/// actor_id must be a valid pointer to an AMactorId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMcreate(actor_id: *const AMactorId) -> *mut AMresult {
to_result(match actor_id.as_ref() {
Some(actor_id) => am::AutoCommit::new().with_actor(actor_id.as_ref().clone()),
None => am::AutoCommit::new(),
})
}
/// \memberof AMdoc
/// \brief Commits the current operations on a document with an optional
/// message and/or time override as seconds since the epoch.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] message A UTF-8 string or `NULL`.
/// \param[in] time A pointer to a `time_t` value or `NULL`.
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
/// with one element.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMcommit(
doc: *mut AMdoc,
message: *const c_char,
time: *const libc::time_t,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let mut options = CommitOptions::default();
if !message.is_null() {
options.set_message(to_str(message));
}
if let Some(time) = time.as_ref() {
options.set_time(*time);
}
to_result(doc.commit_with(options))
}
/// \memberof AMdoc
/// \brief Tests the equality of two documents after closing their respective
/// transactions.
///
/// \param[in,out] doc1 An `AMdoc` struct.
/// \param[in,out] doc2 An `AMdoc` struct.
/// \return `true` if \p doc1 `==` \p doc2 and `false` otherwise.
/// \pre \p doc1 `!= NULL`.
/// \pre \p doc2 `!= NULL`.
/// \internal
///
/// #Safety
/// doc1 must be a valid pointer to an AMdoc
/// doc2 must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMequal(doc1: *mut AMdoc, doc2: *mut AMdoc) -> bool {
match (doc1.as_mut(), doc2.as_mut()) {
(Some(doc1), Some(doc2)) => doc1.document().get_heads() == doc2.document().get_heads(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMdoc
/// \brief Forks this document at the current or a historical point for use by
/// a different actor.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
/// point or `NULL` for the current point.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMdoc` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMfork(doc: *mut AMdoc, heads: *const AMchangeHashes) -> *mut AMresult {
let doc = to_doc_mut!(doc);
match heads.as_ref() {
None => to_result(doc.fork()),
Some(heads) => to_result(doc.fork_at(heads.as_ref())),
}
}
/// \memberof AMdoc
/// \brief Generates a synchronization message for a peer based upon the given
/// synchronization state.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in,out] sync_state A pointer to an `AMsyncState` struct.
/// \return A pointer to an `AMresult` struct containing either a pointer to an
/// `AMsyncMessage` struct or a void.
/// \pre \p doc `!= NULL`.
/// \pre \p sync_state `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// sync_state must be a valid pointer to an AMsyncState
#[no_mangle]
pub unsafe extern "C" fn AMgenerateSyncMessage(
doc: *mut AMdoc,
sync_state: *mut AMsyncState,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let sync_state = to_sync_state_mut!(sync_state);
to_result(doc.generate_sync_message(sync_state.as_mut()))
}
/// \memberof AMdoc
/// \brief Gets a document's actor identifier.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMactorId` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMgetActorId(doc: *const AMdoc) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(Ok::<am::ActorId, am::AutomergeError>(
doc.get_actor().clone(),
))
}
/// \memberof AMdoc
/// \brief Gets the change added to a document by its respective hash.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The number of bytes in \p src.
/// \return A pointer to an `AMresult` struct containing an `AMchanges` struct.
/// \pre \p doc `!= NULL`.
/// \pre \p src `!= NULL`.
/// \pre \p count `>= AM_CHANGE_HASH_SIZE`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// src must be a byte array of size `>= automerge::types::HASH_SIZE`
#[no_mangle]
pub unsafe extern "C" fn AMgetChangeByHash(
doc: *mut AMdoc,
src: *const u8,
count: usize,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let slice = std::slice::from_raw_parts(src, count);
match slice.try_into() {
Ok(change_hash) => to_result(doc.get_change_by_hash(&change_hash)),
Err(e) => AMresult::err(&e.to_string()).into(),
}
}
/// \memberof AMdoc
/// \brief Gets the changes added to a document by their respective hashes.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] have_deps A pointer to an `AMchangeHashes` struct or `NULL`.
/// \return A pointer to an `AMresult` struct containing an `AMchanges` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMgetChanges(
doc: *mut AMdoc,
have_deps: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let empty_deps = Vec::<am::ChangeHash>::new();
let have_deps = match have_deps.as_ref() {
Some(have_deps) => have_deps.as_ref(),
None => &empty_deps,
};
to_result(doc.get_changes(have_deps))
}
/// \memberof AMdoc
/// \brief Gets the changes added to a second document that weren't added to
/// a first document.
///
/// \param[in,out] doc1 An `AMdoc` struct.
/// \param[in,out] doc2 An `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an `AMchanges` struct.
/// \pre \p doc1 `!= NULL`.
/// \pre \p doc2 `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc1 must be a valid pointer to an AMdoc
/// doc2 must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMgetChangesAdded(doc1: *mut AMdoc, doc2: *mut AMdoc) -> *mut AMresult {
let doc1 = to_doc_mut!(doc1);
let doc2 = to_doc_mut!(doc2);
to_result(doc1.get_changes_added(doc2))
}
/// \memberof AMdoc
/// \brief Gets the current heads of a document.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
/// struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMgetHeads(doc: *mut AMdoc) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(Ok::<Vec<am::ChangeHash>, am::AutomergeError>(
doc.get_heads(),
))
}
/// \memberof AMdoc
/// \brief Gets the hashes of the changes in a document that aren't transitive
/// dependencies of the given hashes of changes.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] heads A pointer to an `AMchangeHashes` struct or `NULL`.
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
/// struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMgetMissingDeps(
doc: *mut AMdoc,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let empty_heads = Vec::<am::ChangeHash>::new();
let heads = match heads.as_ref() {
Some(heads) => heads.as_ref(),
None => &empty_heads,
};
to_result(doc.get_missing_deps(heads))
}
/// \memberof AMdoc
/// \brief Gets the last change made to a document.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing either an `AMchange`
/// struct or a void.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMgetLastLocalChange(doc: *mut AMdoc) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.get_last_local_change())
}
/// \memberof AMdoc
/// \brief Gets the current or historical keys of a map object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
/// keys or `NULL` for current keys.
/// \return A pointer to an `AMresult` struct containing an `AMstrs` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMkeys(
doc: *const AMdoc,
obj_id: *const AMobjId,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
match heads.as_ref() {
None => to_result(doc.keys(obj_id)),
Some(heads) => to_result(doc.keys_at(obj_id, heads.as_ref())),
}
}
/// \memberof AMdoc
/// \brief Allocates storage for a document and initializes it with the compact
/// form of an incremental save.
///
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The number of bytes in \p src to load.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMdoc` struct.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMload(src: *const u8, count: usize) -> *mut AMresult {
let mut data = Vec::new();
data.extend_from_slice(std::slice::from_raw_parts(src, count));
to_result(am::AutoCommit::load(&data))
}
/// \memberof AMdoc
/// \brief Loads the compact form of an incremental save into a document.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The number of bytes in \p src to load.
/// \return A pointer to an `AMresult` struct containing the number of
/// operations loaded from \p src.
/// \pre \p doc `!= NULL`.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMloadIncremental(
doc: *mut AMdoc,
src: *const u8,
count: usize,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let mut data = Vec::new();
data.extend_from_slice(std::slice::from_raw_parts(src, count));
to_result(doc.load_incremental(&data))
}
/// \memberof AMdoc
/// \brief Applies all of the changes in \p src which are not in \p dest to
/// \p dest.
///
/// \param[in,out] dest A pointer to an `AMdoc` struct.
/// \param[in,out] src A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
/// struct.
/// \pre \p dest `!= NULL`.
/// \pre \p src `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// dest must be a valid pointer to an AMdoc
/// src must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMmerge(dest: *mut AMdoc, src: *mut AMdoc) -> *mut AMresult {
let dest = to_doc_mut!(dest);
to_result(dest.merge(to_doc_mut!(src)))
}
/// \memberof AMdoc
/// \brief Gets the current or historical size of an object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
/// size or `NULL` for current size.
/// \return A 64-bit unsigned integer.
/// \pre \p doc `!= NULL`.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMobjSize(
doc: *const AMdoc,
obj_id: *const AMobjId,
heads: *const AMchangeHashes,
) -> usize {
if let Some(doc) = doc.as_ref() {
let obj_id = to_obj_id!(obj_id);
match heads.as_ref() {
None => doc.length(obj_id),
Some(heads) => doc.length_at(obj_id, heads.as_ref()),
}
} else {
0
}
}
/// \memberof AMdoc
/// \brief Gets the current or historical values of an object within its entire
/// range.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
/// items or `NULL` for current items.
/// \return A pointer to an `AMresult` struct containing an `AMobjItems` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMobjValues(
doc: *const AMdoc,
obj_id: *const AMobjId,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
match heads.as_ref() {
None => to_result(doc.values(obj_id)),
Some(heads) => to_result(doc.values_at(obj_id, heads.as_ref())),
}
}
/// \memberof AMdoc
/// \brief Gets the number of pending operations added during a document's
/// current transaction.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return The count of pending operations for \p doc.
/// \pre \p doc `!= NULL`.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMpendingOps(doc: *const AMdoc) -> usize {
if let Some(doc) = doc.as_ref() {
doc.pending_ops()
} else {
0
}
}
/// \memberof AMdoc
/// \brief Receives a synchronization message from a peer based upon a given
/// synchronization state.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in,out] sync_state A pointer to an `AMsyncState` struct.
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p sync_state `!= NULL`.
/// \pre \p sync_message `!= NULL`.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// sync_state must be a valid pointer to an AMsyncState
/// sync_message must be a valid pointer to an AMsyncMessage
#[no_mangle]
pub unsafe extern "C" fn AMreceiveSyncMessage(
doc: *mut AMdoc,
sync_state: *mut AMsyncState,
sync_message: *const AMsyncMessage,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let sync_state = to_sync_state_mut!(sync_state);
let sync_message = to_sync_message!(sync_message);
to_result(doc.receive_sync_message(sync_state.as_mut(), sync_message.as_ref().clone()))
}
/// \memberof AMdoc
/// \brief Cancels the pending operations added during a document's current
/// transaction and gets the number of cancellations.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \return The count of pending operations for \p doc that were cancelled.
/// \pre \p doc `!= NULL`.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMrollback(doc: *mut AMdoc) -> usize {
if let Some(doc) = doc.as_mut() {
doc.rollback()
} else {
0
}
}
/// \memberof AMdoc
/// \brief Saves the entirety of a document into a compact form.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an array of bytes as
/// an `AMbyteSpan` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMsave(doc: *mut AMdoc) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(Ok(doc.save()))
}
/// \memberof AMdoc
/// \brief Saves the changes to a document since its last save into a compact
/// form.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an array of bytes as
/// an `AMbyteSpan` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMsaveIncremental(doc: *mut AMdoc) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(Ok(doc.save_incremental()))
}
/// \memberof AMdoc
/// \brief Puts the actor identifier of a document.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] actor_id A pointer to an `AMactorId` struct.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p actor_id `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// actor_id must be a valid pointer to an AMactorId
#[no_mangle]
pub unsafe extern "C" fn AMsetActorId(
doc: *mut AMdoc,
actor_id: *const AMactorId,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let actor_id = to_actor_id!(actor_id);
doc.set_actor(actor_id.as_ref().clone());
to_result(Ok(()))
}
/// \memberof AMdoc
/// \brief Splices values into and/or removes values from the identified object
/// at a given position within it.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos A position in the object identified by \p obj_id or
/// `SIZE_MAX` to indicate one past its end.
/// \param[in] del The number of characters to delete or `SIZE_MAX` to indicate
/// all of them.
/// \param[in] src A pointer to an array of `AMvalue` structs.
/// \param[in] count The number of `AMvalue` structs in \p src to load.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id`)` or \p pos `== SIZE_MAX`.
/// \pre `0 <=` \p del `<= AMobjSize(`\p obj_id`)` or \p del `== SIZE_MAX`.
/// \pre `(`\p src `!= NULL and 1 <=` \p count `<= sizeof(`\p src`)/
/// sizeof(AMvalue)) or `\p src `== NULL or `\p count `== 0`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// src must be an AMvalue array of size `>= count` or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMsplice(
doc: *mut AMdoc,
obj_id: *const AMobjId,
pos: usize,
del: usize,
src: *const AMvalue,
count: usize,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let len = doc.length(obj_id);
let pos = to_index!(pos, len, "pos");
let del = to_index!(del, len, "del");
let mut vals: Vec<am::ScalarValue> = vec![];
if !(src.is_null() || count == 0) {
let c_vals = std::slice::from_raw_parts(src, count);
for c_val in c_vals {
match c_val.try_into() {
Ok(s) => {
vals.push(s);
}
Err(e) => {
return AMresult::err(&e.to_string()).into();
}
}
}
}
to_result(doc.splice(obj_id, pos, del, vals))
}
/// \memberof AMdoc
/// \brief Splices characters into and/or removes characters from the
/// identified object at a given position within it.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos A position in the text object identified by \p obj_id or
/// `SIZE_MAX` to indicate one past its end.
/// \param[in] del The number of characters to delete or `SIZE_MAX` to indicate
/// all of them.
/// \param[in] text A UTF-8 string.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id`)` or \p pos `== SIZE_MAX`.
/// \pre `0 <=` \p del `<= AMobjSize(`\p obj_id`)` or \p del `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// text must be a null-terminated array of `c_char` or NULL.
#[no_mangle]
pub unsafe extern "C" fn AMspliceText(
doc: *mut AMdoc,
obj_id: *const AMobjId,
pos: usize,
del: usize,
text: *const c_char,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let len = doc.length(obj_id);
let pos = to_index!(pos, len, "pos");
let del = to_index!(del, len, "del");
to_result(doc.splice_text(obj_id, pos, del, &to_str(text)))
}
/// \memberof AMdoc
/// \brief Gets the current or historical string represented by a text object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
/// keys or `NULL` for current keys.
/// \return A pointer to an `AMresult` struct containing a UTF-8 string.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMtext(
doc: *const AMdoc,
obj_id: *const AMobjId,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
match heads.as_ref() {
None => to_result(doc.text(obj_id)),
Some(heads) => to_result(doc.text_at(obj_id, heads.as_ref())),
}
}

604
automerge-c/src/doc/list.rs Normal file
View file

@ -0,0 +1,604 @@
use automerge as am;
use automerge::transaction::Transactable;
use std::os::raw::c_char;
use crate::change_hashes::AMchangeHashes;
use crate::doc::{to_doc, to_doc_mut, to_obj_id, to_str, AMdoc};
use crate::obj::{AMobjId, AMobjType};
use crate::result::{to_result, AMresult};
pub mod item;
pub mod items;
macro_rules! adjust {
($index:expr, $insert:expr, $len:expr) => {{
// An empty object can only be inserted into.
let insert = $insert || $len == 0;
let end = if insert { $len } else { $len - 1 };
if $index > end && $index != usize::MAX {
return AMresult::err(&format!("Invalid index {}", $index)).into();
}
(std::cmp::min($index, end), insert)
}};
}
macro_rules! to_range {
($begin:expr, $end:expr) => {{
if $begin > $end {
return AMresult::err(&format!("Invalid range [{}-{})", $begin, $end)).into();
};
($begin..$end)
}};
}
/// \memberof AMdoc
/// \brief Deletes an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistDelete(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, _) = adjust!(index, false, doc.length(obj_id));
to_result(doc.delete(obj_id, index))
}
/// \memberof AMdoc
/// \brief Gets the current or historical value at an index in a list object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
/// value or `NULL` for the current value.
/// \return A pointer to an `AMresult` struct that doesn't contain a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistGet(
doc: *const AMdoc,
obj_id: *const AMobjId,
index: usize,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, _) = adjust!(index, false, doc.length(obj_id));
match heads.as_ref() {
None => to_result(doc.get(obj_id, index)),
Some(heads) => to_result(doc.get_at(obj_id, index, heads.as_ref())),
}
}
/// \memberof AMdoc
/// \brief Gets all of the historical values at an index in a list object until
/// its current one or a specific one.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
/// last value or `NULL` for the current last value.
/// \return A pointer to an `AMresult` struct containing an `AMobjItems` struct.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistGetAll(
doc: *const AMdoc,
obj_id: *const AMobjId,
index: usize,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, _) = adjust!(index, false, doc.length(obj_id));
match heads.as_ref() {
None => to_result(doc.get_all(obj_id, index)),
Some(heads) => to_result(doc.get_all_at(obj_id, index, heads.as_ref())),
}
}
/// \memberof AMdoc
/// \brief Increments a counter at an index in a list object by the given
/// value.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistIncrement(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, _) = adjust!(index, false, doc.length(obj_id));
to_result(doc.increment(obj_id, index, value))
}
/// \memberof AMdoc
/// \brief Puts a boolean as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A boolean.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistPutBool(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: bool,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
let value = am::ScalarValue::Boolean(value);
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a sequence of bytes as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p src before \p index instead of
/// writing \p src over \p index.
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The number of bytes to copy from \p src.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMlistPutBytes(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
src: *const u8,
count: usize,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
let mut value = Vec::new();
value.extend_from_slice(std::slice::from_raw_parts(src, count));
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a CRDT counter as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistPutCounter(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
let value = am::ScalarValue::Counter(value.into());
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a float as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A 64-bit float.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistPutF64(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: f64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a signed integer as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistPutInt(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts null as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistPutNull(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
to_result(if insert {
doc.insert(obj_id, index, ())
} else {
doc.put(obj_id, index, ())
})
}
/// \memberof AMdoc
/// \brief Puts an empty object as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] obj_type An `AMobjIdType` enum tag.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMobjId` struct.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistPutObject(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
obj_type: AMobjType,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
let object = obj_type.into();
to_result(if insert {
doc.insert_object(obj_id, index, object)
} else {
doc.put_object(obj_id, index, object)
})
}
/// \memberof AMdoc
/// \brief Puts a UTF-8 string as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A UTF-8 string.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \pre \p value `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// value must be a null-terminated array of `c_char`
#[no_mangle]
pub unsafe extern "C" fn AMlistPutStr(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: *const c_char,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
let value = to_str(value);
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a Lamport timestamp as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistPutTimestamp(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
let value = am::ScalarValue::Timestamp(value);
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts an unsigned integer as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A 64-bit unsigned integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistPutUint(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: u64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Gets the current or historical indices and values of the list object
/// within the given range.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] begin The first index in a range of indices.
/// \param[in] end At least one past the last index in a range of indices.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
/// indices and values or `NULL` for current indices and
/// values.
/// \return A pointer to an `AMresult` struct containing an `AMlistItems`
/// struct.
/// \pre \p doc `!= NULL`.
/// \pre \p begin `<=` \p end `<= SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistRange(
doc: *const AMdoc,
obj_id: *const AMobjId,
begin: usize,
end: usize,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let range = to_range!(begin, end);
match heads.as_ref() {
None => to_result(doc.list_range(obj_id, range)),
Some(heads) => to_result(doc.list_range_at(obj_id, range, heads.as_ref())),
}
}

View file

@ -0,0 +1,100 @@
use automerge as am;
use std::cell::RefCell;
use std::ffi::CString;
use crate::obj::AMobjId;
use crate::result::AMvalue;
/// \struct AMlistItem
/// \installed_headerfile
/// \brief An item in a list object.
#[repr(C)]
pub struct AMlistItem {
/// The index of an item in a list object.
index: usize,
/// The object identifier of an item in a list object.
obj_id: AMobjId,
/// The value of an item in a list object.
value: (am::Value<'static>, RefCell<Option<CString>>),
}
impl AMlistItem {
pub fn new(index: usize, value: am::Value<'static>, obj_id: am::ObjId) -> Self {
Self {
index,
obj_id: AMobjId::new(obj_id),
value: (value, Default::default()),
}
}
}
impl PartialEq for AMlistItem {
fn eq(&self, other: &Self) -> bool {
self.index == other.index && self.obj_id == other.obj_id && self.value.0 == other.value.0
}
}
/*
impl From<&AMlistItem> for (usize, am::Value<'static>, am::ObjId) {
fn from(list_item: &AMlistItem) -> Self {
(list_item.index, list_item.value.0.clone(), list_item.obj_id.as_ref().clone())
}
}
*/
/// \memberof AMlistItem
/// \brief Gets the index of an item in a list object.
///
/// \param[in] list_item A pointer to an `AMlistItem` struct.
/// \return A 64-bit unsigned integer.
/// \pre \p list_item `!= NULL`.
/// \internal
///
/// # Safety
/// list_item must be a valid pointer to an AMlistItem
#[no_mangle]
pub unsafe extern "C" fn AMlistItemIndex(list_item: *const AMlistItem) -> usize {
if let Some(list_item) = list_item.as_ref() {
list_item.index
} else {
usize::MAX
}
}
/// \memberof AMlistItem
/// \brief Gets the object identifier of an item in a list object.
///
/// \param[in] list_item A pointer to an `AMlistItem` struct.
/// \return A pointer to an `AMobjId` struct.
/// \pre \p list_item `!= NULL`.
/// \internal
///
/// # Safety
/// list_item must be a valid pointer to an AMlistItem
#[no_mangle]
pub unsafe extern "C" fn AMlistItemObjId(list_item: *const AMlistItem) -> *const AMobjId {
if let Some(list_item) = list_item.as_ref() {
&list_item.obj_id
} else {
std::ptr::null()
}
}
/// \memberof AMlistItem
/// \brief Gets the value of an item in a list object.
///
/// \param[in] list_item A pointer to an `AMlistItem` struct.
/// \return An `AMvalue` struct.
/// \pre \p list_item `!= NULL`.
/// \internal
///
/// # Safety
/// list_item must be a valid pointer to an AMlistItem
#[no_mangle]
pub unsafe extern "C" fn AMlistItemValue<'a>(list_item: *const AMlistItem) -> AMvalue<'a> {
if let Some(list_item) = list_item.as_ref() {
(&list_item.value.0, &list_item.value.1).into()
} else {
AMvalue::Void
}
}

View file

@ -0,0 +1,348 @@
use std::ffi::c_void;
use std::mem::size_of;
use crate::doc::list::item::AMlistItem;
#[repr(C)]
struct Detail {
len: usize,
offset: isize,
ptr: *const c_void,
}
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
/// propagate the name of a constant initialized from it so if the
/// constant's name is a symbolic representation of the value it can be
/// converted into a number by post-processing the header it generated.
pub const USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
impl Detail {
fn new(list_items: &[AMlistItem], offset: isize) -> Self {
Self {
len: list_items.len(),
offset,
ptr: list_items.as_ptr() as *const c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, len))
}
}
}
pub fn get_index(&self) -> usize {
(self.offset
+ if self.offset < 0 {
self.len as isize
} else {
0
}) as usize
}
pub fn next(&mut self, n: isize) -> Option<&AMlistItem> {
if self.is_stopped() {
return None;
}
let slice: &[AMlistItem] =
unsafe { std::slice::from_raw_parts(self.ptr as *const AMlistItem, self.len) };
let value = &slice[self.get_index()];
self.advance(n);
Some(value)
}
pub fn is_stopped(&self) -> bool {
let len = self.len as isize;
self.offset < -len || self.offset == len
}
pub fn prev(&mut self, n: isize) -> Option<&AMlistItem> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &[AMlistItem] =
unsafe { std::slice::from_raw_parts(self.ptr as *const AMlistItem, self.len) };
Some(&slice[self.get_index()])
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
ptr: self.ptr,
}
}
}
impl From<Detail> for [u8; USIZE_USIZE_USIZE_] {
fn from(detail: Detail) -> Self {
unsafe {
std::slice::from_raw_parts((&detail as *const Detail) as *const u8, USIZE_USIZE_USIZE_)
.try_into()
.unwrap()
}
}
}
/// \struct AMlistItems
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of list object items.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMlistItems {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_],
}
impl AMlistItems {
pub fn new(list_items: &[AMlistItem]) -> Self {
Self {
detail: Detail::new(list_items, 0).into(),
}
}
pub fn advance(&mut self, n: isize) {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.advance(n);
}
pub fn len(&self) -> usize {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
detail.len
}
pub fn next(&mut self, n: isize) -> Option<&AMlistItem> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<&AMlistItem> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.prev(n)
}
pub fn reversed(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.reversed().into(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
}
}
}
impl AsRef<[AMlistItem]> for AMlistItems {
fn as_ref(&self) -> &[AMlistItem] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const AMlistItem, detail.len) }
}
}
impl Default for AMlistItems {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMlistItems
/// \brief Advances an iterator over a sequence of list object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] list_items A pointer to an `AMlistItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p list_items `!= NULL`.
/// \internal
///
/// #Safety
/// list_items must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsAdvance(list_items: *mut AMlistItems, n: isize) {
if let Some(list_items) = list_items.as_mut() {
list_items.advance(n);
};
}
/// \memberof AMlistItems
/// \brief Tests the equality of two sequences of list object items underlying
/// a pair of iterators.
///
/// \param[in] list_items1 A pointer to an `AMlistItems` struct.
/// \param[in] list_items2 A pointer to an `AMlistItems` struct.
/// \return `true` if \p list_items1 `==` \p list_items2 and `false` otherwise.
/// \pre \p list_items1 `!= NULL`.
/// \pre \p list_items2 `!= NULL`.
/// \internal
///
/// #Safety
/// list_items1 must be a valid pointer to an AMlistItems
/// list_items2 must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsEqual(
list_items1: *const AMlistItems,
list_items2: *const AMlistItems,
) -> bool {
match (list_items1.as_ref(), list_items2.as_ref()) {
(Some(list_items1), Some(list_items2)) => list_items1.as_ref() == list_items2.as_ref(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMlistItems
/// \brief Gets the list object item at the current position of an iterator
/// over a sequence of list object items and then advances it by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] list_items A pointer to an `AMlistItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMlistItem` struct that's `NULL` when
/// \p list_items was previously advanced past its forward/reverse
/// limit.
/// \pre \p list_items `!= NULL`.
/// \internal
///
/// #Safety
/// list_items must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsNext(
list_items: *mut AMlistItems,
n: isize,
) -> *const AMlistItem {
if let Some(list_items) = list_items.as_mut() {
if let Some(list_item) = list_items.next(n) {
return list_item;
}
}
std::ptr::null()
}
/// \memberof AMlistItems
/// \brief Advances an iterator over a sequence of list object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the list object item at its new
/// position.
///
/// \param[in,out] list_items A pointer to an `AMlistItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMlistItem` struct that's `NULL` when
/// \p list_items is presently advanced past its forward/reverse limit.
/// \pre \p list_items `!= NULL`.
/// \internal
///
/// #Safety
/// list_items must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsPrev(
list_items: *mut AMlistItems,
n: isize,
) -> *const AMlistItem {
if let Some(list_items) = list_items.as_mut() {
if let Some(list_item) = list_items.prev(n) {
return list_item;
}
}
std::ptr::null()
}
/// \memberof AMlistItems
/// \brief Gets the size of the sequence of list object items underlying an
/// iterator.
///
/// \param[in] list_items A pointer to an `AMlistItems` struct.
/// \return The count of values in \p list_items.
/// \pre \p list_items `!= NULL`.
/// \internal
///
/// #Safety
/// list_items must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsSize(list_items: *const AMlistItems) -> usize {
if let Some(list_items) = list_items.as_ref() {
list_items.len()
} else {
0
}
}
/// \memberof AMlistItems
/// \brief Creates an iterator over the same sequence of list object items as
/// the given one but with the opposite position and direction.
///
/// \param[in] list_items A pointer to an `AMlistItems` struct.
/// \return An `AMlistItems` struct
/// \pre \p list_items `!= NULL`.
/// \internal
///
/// #Safety
/// list_items must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsReversed(list_items: *const AMlistItems) -> AMlistItems {
if let Some(list_items) = list_items.as_ref() {
list_items.reversed()
} else {
AMlistItems::default()
}
}
/// \memberof AMlistItems
/// \brief Creates an iterator at the starting position over the same sequence
/// of list object items as the given one.
///
/// \param[in] list_items A pointer to an `AMlistItems` struct.
/// \return An `AMlistItems` struct
/// \pre \p list_items `!= NULL`.
/// \internal
///
/// #Safety
/// list_items must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsRewound(list_items: *const AMlistItems) -> AMlistItems {
if let Some(list_items) = list_items.as_ref() {
list_items.rewound()
} else {
AMlistItems::default()
}
}

506
automerge-c/src/doc/map.rs Normal file
View file

@ -0,0 +1,506 @@
use automerge as am;
use automerge::transaction::Transactable;
use std::os::raw::c_char;
use crate::change_hashes::AMchangeHashes;
use crate::doc::utils::to_str;
use crate::doc::{to_doc, to_doc_mut, to_obj_id, AMdoc};
use crate::obj::{AMobjId, AMobjType};
use crate::result::{to_result, AMresult};
pub mod item;
pub mod items;
/// \memberof AMdoc
/// \brief Deletes a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapDelete(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.delete(to_obj_id!(obj_id), to_str(key)))
}
/// \memberof AMdoc
/// \brief Gets the current or historical value for a key in a map object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by
/// \p obj_id.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
/// value or `NULL` for the current value.
/// \return A pointer to an `AMresult` struct that doesn't contain a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMmapGet(
doc: *const AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
match heads.as_ref() {
None => to_result(doc.get(obj_id, to_str(key))),
Some(heads) => to_result(doc.get_at(obj_id, to_str(key), heads.as_ref())),
}
}
/// \memberof AMdoc
/// \brief Gets all of the historical values for a key in a map object until
/// its current one or a specific one.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by
/// \p obj_id.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
/// last value or `NULL` for the current last value.
/// \return A pointer to an `AMresult` struct containing an `AMobjItems` struct.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMmapGetAll(
doc: *const AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
match heads.as_ref() {
None => to_result(doc.get_all(obj_id, to_str(key))),
Some(heads) => to_result(doc.get_all_at(obj_id, to_str(key), heads.as_ref())),
}
}
/// \memberof AMdoc
/// \brief Increments a counter for a key in a map object by the given value.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapIncrement(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.increment(to_obj_id!(obj_id), to_str(key), value))
}
/// \memberof AMdoc
/// \brief Puts a boolean as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A boolean.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutBool(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: bool,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.put(to_obj_id!(obj_id), to_str(key), value))
}
/// \memberof AMdoc
/// \brief Puts a sequence of bytes as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The number of bytes to copy from \p src.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMmapPutBytes(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
src: *const u8,
count: usize,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let mut vec = Vec::new();
vec.extend_from_slice(std::slice::from_raw_parts(src, count));
to_result(doc.put(to_obj_id!(obj_id), to_str(key), vec))
}
/// \memberof AMdoc
/// \brief Puts a CRDT counter as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutCounter(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.put(
to_obj_id!(obj_id),
to_str(key),
am::ScalarValue::Counter(value.into()),
))
}
/// \memberof AMdoc
/// \brief Puts null as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutNull(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.put(to_obj_id!(obj_id), to_str(key), ()))
}
/// \memberof AMdoc
/// \brief Puts an empty object as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] obj_type An `AMobjIdType` enum tag.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMobjId` struct.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutObject(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
obj_type: AMobjType,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.put_object(to_obj_id!(obj_id), to_str(key), obj_type.into()))
}
/// \memberof AMdoc
/// \brief Puts a float as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A 64-bit float.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutF64(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: f64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.put(to_obj_id!(obj_id), to_str(key), value))
}
/// \memberof AMdoc
/// \brief Puts a signed integer as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutInt(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.put(to_obj_id!(obj_id), to_str(key), value))
}
/// \memberof AMdoc
/// \brief Puts a UTF-8 string as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A UTF-8 string.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \pre \p value `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
/// value must be a null-terminated array of `c_char`
#[no_mangle]
pub unsafe extern "C" fn AMmapPutStr(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: *const c_char,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.put(to_obj_id!(obj_id), to_str(key), to_str(value)))
}
/// \memberof AMdoc
/// \brief Puts a Lamport timestamp as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutTimestamp(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.put(
to_obj_id!(obj_id),
to_str(key),
am::ScalarValue::Timestamp(value),
))
}
/// \memberof AMdoc
/// \brief Puts an unsigned integer as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A 64-bit unsigned integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutUint(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: u64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.put(to_obj_id!(obj_id), to_str(key), value))
}
/// \memberof AMdoc
/// \brief Gets the current or historical keys and values of the map object
/// within the given range.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] begin The first key in a subrange or `NULL` to indicate the
/// absolute first key.
/// \param[in] end The key one past the last key in a subrange or `NULL` to
/// indicate one past the absolute last key.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
/// keys and values or `NULL` for current keys and values.
/// \return A pointer to an `AMresult` struct containing an `AMmapItems`
/// struct.
/// \pre \p doc `!= NULL`.
/// \pre `strcmp(`\p begin, \p end`) != 1` if \p begin `!= NULL` and \p end `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMmapRange(
doc: *const AMdoc,
obj_id: *const AMobjId,
begin: *const c_char,
end: *const c_char,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
match (begin.as_ref(), end.as_ref()) {
(Some(_), Some(_)) => {
let (begin, end) = (to_str(begin), to_str(end));
if begin > end {
return AMresult::err(&format!("Invalid range [{}-{})", begin, end)).into();
};
let bounds = begin..end;
if let Some(heads) = heads.as_ref() {
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
} else {
to_result(doc.map_range(obj_id, bounds))
}
}
(Some(_), None) => {
let bounds = to_str(begin)..;
if let Some(heads) = heads.as_ref() {
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
} else {
to_result(doc.map_range(obj_id, bounds))
}
}
(None, Some(_)) => {
let bounds = ..to_str(end);
if let Some(heads) = heads.as_ref() {
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
} else {
to_result(doc.map_range(obj_id, bounds))
}
}
(None, None) => {
let bounds = ..;
if let Some(heads) = heads.as_ref() {
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
} else {
to_result(doc.map_range(obj_id, bounds))
}
}
}
}

View file

@ -0,0 +1,101 @@
use automerge as am;
use std::cell::RefCell;
use std::ffi::CString;
use std::os::raw::c_char;
use crate::obj::AMobjId;
use crate::result::AMvalue;
/// \struct AMmapItem
/// \installed_headerfile
/// \brief An item in a map object.
#[repr(C)]
pub struct AMmapItem {
/// The key of an item in a map object.
key: CString,
/// The object identifier of an item in a map object.
obj_id: AMobjId,
/// The value of an item in a map object.
value: (am::Value<'static>, RefCell<Option<CString>>),
}
impl AMmapItem {
pub fn new(key: &'static str, value: am::Value<'static>, obj_id: am::ObjId) -> Self {
Self {
key: CString::new(key).unwrap(),
obj_id: AMobjId::new(obj_id),
value: (value, Default::default()),
}
}
}
impl PartialEq for AMmapItem {
fn eq(&self, other: &Self) -> bool {
self.key == other.key && self.obj_id == other.obj_id && self.value.0 == other.value.0
}
}
/*
impl From<&AMmapItem> for (String, am::Value<'static>, am::ObjId) {
fn from(map_item: &AMmapItem) -> Self {
(map_item.key.into_string().unwrap(), map_item.value.0.clone(), map_item.obj_id.as_ref().clone())
}
}
*/
/// \memberof AMmapItem
/// \brief Gets the key of an item in a map object.
///
/// \param[in] map_item A pointer to an `AMmapItem` struct.
/// \return A 64-bit unsigned integer.
/// \pre \p map_item `!= NULL`.
/// \internal
///
/// # Safety
/// map_item must be a valid pointer to an AMmapItem
#[no_mangle]
pub unsafe extern "C" fn AMmapItemKey(map_item: *const AMmapItem) -> *const c_char {
if let Some(map_item) = map_item.as_ref() {
map_item.key.as_ptr()
} else {
std::ptr::null()
}
}
/// \memberof AMmapItem
/// \brief Gets the object identifier of an item in a map object.
///
/// \param[in] map_item A pointer to an `AMmapItem` struct.
/// \return A pointer to an `AMobjId` struct.
/// \pre \p map_item `!= NULL`.
/// \internal
///
/// # Safety
/// map_item must be a valid pointer to an AMmapItem
#[no_mangle]
pub unsafe extern "C" fn AMmapItemObjId(map_item: *const AMmapItem) -> *const AMobjId {
if let Some(map_item) = map_item.as_ref() {
&map_item.obj_id
} else {
std::ptr::null()
}
}
/// \memberof AMmapItem
/// \brief Gets the value of an item in a map object.
///
/// \param[in] map_item A pointer to an `AMmapItem` struct.
/// \return An `AMvalue` struct.
/// \pre \p map_item `!= NULL`.
/// \internal
///
/// # Safety
/// map_item must be a valid pointer to an AMmapItem
#[no_mangle]
pub unsafe extern "C" fn AMmapItemValue<'a>(map_item: *const AMmapItem) -> AMvalue<'a> {
if let Some(map_item) = map_item.as_ref() {
(&map_item.value.0, &map_item.value.1).into()
} else {
AMvalue::Void
}
}

View file

@ -0,0 +1,340 @@
use std::ffi::c_void;
use std::mem::size_of;
use crate::doc::map::item::AMmapItem;
#[repr(C)]
struct Detail {
len: usize,
offset: isize,
ptr: *const c_void,
}
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
/// propagate the name of a constant initialized from it so if the
/// constant's name is a symbolic representation of the value it can be
/// converted into a number by post-processing the header it generated.
pub const USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
impl Detail {
fn new(map_items: &[AMmapItem], offset: isize) -> Self {
Self {
len: map_items.len(),
offset,
ptr: map_items.as_ptr() as *const c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, len))
}
}
}
pub fn get_index(&self) -> usize {
(self.offset
+ if self.offset < 0 {
self.len as isize
} else {
0
}) as usize
}
pub fn next(&mut self, n: isize) -> Option<&AMmapItem> {
if self.is_stopped() {
return None;
}
let slice: &[AMmapItem] =
unsafe { std::slice::from_raw_parts(self.ptr as *const AMmapItem, self.len) };
let value = &slice[self.get_index()];
self.advance(n);
Some(value)
}
pub fn is_stopped(&self) -> bool {
let len = self.len as isize;
self.offset < -len || self.offset == len
}
pub fn prev(&mut self, n: isize) -> Option<&AMmapItem> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &[AMmapItem] =
unsafe { std::slice::from_raw_parts(self.ptr as *const AMmapItem, self.len) };
Some(&slice[self.get_index()])
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
ptr: self.ptr,
}
}
}
impl From<Detail> for [u8; USIZE_USIZE_USIZE_] {
fn from(detail: Detail) -> Self {
unsafe {
std::slice::from_raw_parts((&detail as *const Detail) as *const u8, USIZE_USIZE_USIZE_)
.try_into()
.unwrap()
}
}
}
/// \struct AMmapItems
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of map object items.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMmapItems {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_],
}
impl AMmapItems {
pub fn new(map_items: &[AMmapItem]) -> Self {
Self {
detail: Detail::new(map_items, 0).into(),
}
}
pub fn advance(&mut self, n: isize) {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.advance(n);
}
pub fn len(&self) -> usize {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
detail.len
}
pub fn next(&mut self, n: isize) -> Option<&AMmapItem> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<&AMmapItem> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.prev(n)
}
pub fn reversed(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.reversed().into(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
}
}
}
impl AsRef<[AMmapItem]> for AMmapItems {
fn as_ref(&self) -> &[AMmapItem] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const AMmapItem, detail.len) }
}
}
impl Default for AMmapItems {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMmapItems
/// \brief Advances an iterator over a sequence of map object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] map_items A pointer to an `AMmapItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p map_items `!= NULL`.
/// \internal
///
/// #Safety
/// map_items must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsAdvance(map_items: *mut AMmapItems, n: isize) {
if let Some(map_items) = map_items.as_mut() {
map_items.advance(n);
};
}
/// \memberof AMmapItems
/// \brief Tests the equality of two sequences of map object items underlying
/// a pair of iterators.
///
/// \param[in] map_items1 A pointer to an `AMmapItems` struct.
/// \param[in] map_items2 A pointer to an `AMmapItems` struct.
/// \return `true` if \p map_items1 `==` \p map_items2 and `false` otherwise.
/// \pre \p map_items1 `!= NULL`.
/// \pre \p map_items2 `!= NULL`.
/// \internal
///
/// #Safety
/// map_items1 must be a valid pointer to an AMmapItems
/// map_items2 must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsEqual(
map_items1: *const AMmapItems,
map_items2: *const AMmapItems,
) -> bool {
match (map_items1.as_ref(), map_items2.as_ref()) {
(Some(map_items1), Some(map_items2)) => map_items1.as_ref() == map_items2.as_ref(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMmapItems
/// \brief Gets the map object item at the current position of an iterator
/// over a sequence of map object items and then advances it by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] map_items A pointer to an `AMmapItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMmapItem` struct that's `NULL` when \p map_items
/// was previously advanced past its forward/reverse limit.
/// \pre \p map_items `!= NULL`.
/// \internal
///
/// #Safety
/// map_items must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsNext(map_items: *mut AMmapItems, n: isize) -> *const AMmapItem {
if let Some(map_items) = map_items.as_mut() {
if let Some(map_item) = map_items.next(n) {
return map_item;
}
}
std::ptr::null()
}
/// \memberof AMmapItems
/// \brief Advances an iterator over a sequence of map object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the map object item at its new
/// position.
///
/// \param[in,out] map_items A pointer to an `AMmapItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMmapItem` struct that's `NULL` when \p map_items
/// is presently advanced past its forward/reverse limit.
/// \pre \p map_items `!= NULL`.
/// \internal
///
/// #Safety
/// map_items must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsPrev(map_items: *mut AMmapItems, n: isize) -> *const AMmapItem {
if let Some(map_items) = map_items.as_mut() {
if let Some(map_item) = map_items.prev(n) {
return map_item;
}
}
std::ptr::null()
}
/// \memberof AMmapItems
/// \brief Gets the size of the sequence of map object items underlying an
/// iterator.
///
/// \param[in] map_items A pointer to an `AMmapItems` struct.
/// \return The count of values in \p map_items.
/// \pre \p map_items `!= NULL`.
/// \internal
///
/// #Safety
/// map_items must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsSize(map_items: *const AMmapItems) -> usize {
if let Some(map_items) = map_items.as_ref() {
map_items.len()
} else {
0
}
}
/// \memberof AMmapItems
/// \brief Creates an iterator over the same sequence of map object items as
/// the given one but with the opposite position and direction.
///
/// \param[in] map_items A pointer to an `AMmapItems` struct.
/// \return An `AMmapItems` struct
/// \pre \p map_items `!= NULL`.
/// \internal
///
/// #Safety
/// map_items must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsReversed(map_items: *const AMmapItems) -> AMmapItems {
if let Some(map_items) = map_items.as_ref() {
map_items.reversed()
} else {
AMmapItems::default()
}
}
/// \memberof AMmapItems
/// \brief Creates an iterator at the starting position over the same sequence of map object items as the given one.
///
/// \param[in] map_items A pointer to an `AMmapItems` struct.
/// \return An `AMmapItems` struct
/// \pre \p map_items `!= NULL`.
/// \internal
///
/// #Safety
/// map_items must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsRewound(map_items: *const AMmapItems) -> AMmapItems {
if let Some(map_items) = map_items.as_ref() {
map_items.rewound()
} else {
AMmapItems::default()
}
}

View file

@ -0,0 +1,57 @@
use std::ffi::CStr;
use std::os::raw::c_char;
macro_rules! to_actor_id {
($handle:expr) => {{
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::err("Invalid AMactorId pointer").into(),
}
}};
}
pub(crate) use to_actor_id;
macro_rules! to_doc {
($handle:expr) => {{
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::err("Invalid AMdoc pointer").into(),
}
}};
}
pub(crate) use to_doc;
macro_rules! to_doc_mut {
($handle:expr) => {{
let handle = $handle.as_mut();
match handle {
Some(b) => b,
None => return AMresult::err("Invalid AMdoc pointer").into(),
}
}};
}
pub(crate) use to_doc_mut;
macro_rules! to_obj_id {
($handle:expr) => {{
match $handle.as_ref() {
Some(obj_id) => obj_id,
None => &automerge::ROOT,
}
}};
}
pub(crate) use to_obj_id;
pub(crate) unsafe fn to_str(c: *const c_char) -> String {
if !c.is_null() {
CStr::from_ptr(c).to_string_lossy().to_string()
} else {
String::default()
}
}

11
automerge-c/src/lib.rs Normal file
View file

@ -0,0 +1,11 @@
mod actor_id;
mod byte_span;
mod change;
mod change_hashes;
mod changes;
mod doc;
mod obj;
mod result;
mod result_stack;
mod strs;
mod sync;

View file

@ -1,32 +1,11 @@
use automerge as am;
use std::any::type_name;
use std::cell::RefCell;
use std::ops::Deref;
use crate::actor_id::AMactorId;
macro_rules! to_obj_id {
($handle:expr) => {{
match $handle.as_ref() {
Some(obj_id) => obj_id,
None => &automerge::ROOT,
}
}};
}
pub(crate) use to_obj_id;
macro_rules! to_obj_type {
($c_obj_type:expr) => {{
let result: Result<am::ObjType, am::AutomergeError> = (&$c_obj_type).try_into();
match result {
Ok(obj_type) => obj_type,
Err(e) => return AMresult::error(&e.to_string()).into(),
}
}};
}
pub(crate) use to_obj_type;
pub mod item;
pub mod items;
/// \struct AMobjId
/// \installed_headerfile
@ -76,11 +55,11 @@ impl Deref for AMobjId {
}
/// \memberof AMobjId
/// \brief Gets the actor identifier component of an object identifier.
/// \brief Gets the actor identifier of an object identifier.
///
/// \param[in] obj_id A pointer to an `AMobjId` struct.
/// \return A pointer to an `AMactorId` struct or `NULL`.
/// \pre \p obj_id `!= NULL`
/// \pre \p obj_id `!= NULL`.
/// \internal
///
/// # Safety
@ -94,11 +73,11 @@ pub unsafe extern "C" fn AMobjIdActorId(obj_id: *const AMobjId) -> *const AMacto
}
/// \memberof AMobjId
/// \brief Gets the counter component of an object identifier.
/// \brief Gets the counter of an object identifier.
///
/// \param[in] obj_id A pointer to an `AMobjId` struct.
/// \return A 64-bit unsigned integer.
/// \pre \p obj_id `!= NULL`
/// \pre \p obj_id `!= NULL`.
/// \internal
///
/// # Safety
@ -121,9 +100,8 @@ pub unsafe extern "C" fn AMobjIdCounter(obj_id: *const AMobjId) -> u64 {
/// \param[in] obj_id1 A pointer to an `AMobjId` struct.
/// \param[in] obj_id2 A pointer to an `AMobjId` struct.
/// \return `true` if \p obj_id1 `==` \p obj_id2 and `false` otherwise.
/// \pre \p obj_id1 `!= NULL`
/// \pre \p obj_id1 `!= NULL`
/// \post `!(`\p obj_id1 `&&` \p obj_id2 `) -> false`
/// \pre \p obj_id1 `!= NULL`.
/// \pre \p obj_id2 `!= NULL`.
/// \internal
///
/// #Safety
@ -133,28 +111,26 @@ pub unsafe extern "C" fn AMobjIdCounter(obj_id: *const AMobjId) -> u64 {
pub unsafe extern "C" fn AMobjIdEqual(obj_id1: *const AMobjId, obj_id2: *const AMobjId) -> bool {
match (obj_id1.as_ref(), obj_id2.as_ref()) {
(Some(obj_id1), Some(obj_id2)) => obj_id1 == obj_id2,
(None, None) | (None, Some(_)) | (Some(_), None) => false,
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMobjId
/// \brief Gets the index component of an object identifier.
/// \brief Gets the index of an object identifier.
///
/// \param[in] obj_id A pointer to an `AMobjId` struct.
/// \return A 64-bit unsigned integer.
/// \pre \p obj_id `!= NULL`
/// \pre \p obj_id `!= NULL`.
/// \internal
///
/// # Safety
/// obj_id must be a valid pointer to an AMobjId
#[no_mangle]
pub unsafe extern "C" fn AMobjIdIndex(obj_id: *const AMobjId) -> usize {
use am::ObjId::*;
if let Some(obj_id) = obj_id.as_ref() {
match obj_id.as_ref() {
Id(_, _, index) => *index,
Root => 0,
am::ObjId::Id(_, _, index) => *index,
am::ObjId::Root => 0,
}
} else {
usize::MAX
@ -163,13 +139,9 @@ pub unsafe extern "C" fn AMobjIdIndex(obj_id: *const AMobjId) -> usize {
/// \ingroup enumerations
/// \enum AMobjType
/// \installed_headerfile
/// \brief The type of an object value.
#[derive(PartialEq, Eq)]
#[repr(u8)]
pub enum AMobjType {
/// The default tag, not a type signifier.
Default = 0,
/// A list.
List = 1,
/// A key-value map.
@ -178,39 +150,12 @@ pub enum AMobjType {
Text,
}
impl Default for AMobjType {
fn default() -> Self {
Self::Default
}
}
impl From<&am::ObjType> for AMobjType {
fn from(o: &am::ObjType) -> Self {
use am::ObjType::*;
impl From<AMobjType> for am::ObjType {
fn from(o: AMobjType) -> Self {
match o {
List => Self::List,
Map | Table => Self::Map,
Text => Self::Text,
}
}
}
impl TryFrom<&AMobjType> for am::ObjType {
type Error = am::AutomergeError;
fn try_from(c_obj_type: &AMobjType) -> Result<Self, Self::Error> {
use am::AutomergeError::InvalidValueType;
use AMobjType::*;
match c_obj_type {
List => Ok(Self::List),
Map => Ok(Self::Map),
Text => Ok(Self::Text),
_ => Err(InvalidValueType {
expected: type_name::<Self>().to_string(),
unexpected: type_name::<AMobjType>().to_string(),
}),
AMobjType::Map => am::ObjType::Map,
AMobjType::List => am::ObjType::List,
AMobjType::Text => am::ObjType::Text,
}
}
}

View file

@ -0,0 +1,76 @@
use automerge as am;
use std::cell::RefCell;
use std::ffi::CString;
use crate::obj::AMobjId;
use crate::result::AMvalue;
/// \struct AMobjItem
/// \installed_headerfile
/// \brief An item in an object.
#[repr(C)]
pub struct AMobjItem {
/// The object identifier of an item in an object.
obj_id: AMobjId,
/// The value of an item in an object.
value: (am::Value<'static>, RefCell<Option<CString>>),
}
impl AMobjItem {
pub fn new(value: am::Value<'static>, obj_id: am::ObjId) -> Self {
Self {
obj_id: AMobjId::new(obj_id),
value: (value, Default::default()),
}
}
}
impl PartialEq for AMobjItem {
fn eq(&self, other: &Self) -> bool {
self.obj_id == other.obj_id && self.value.0 == other.value.0
}
}
impl From<&AMobjItem> for (am::Value<'static>, am::ObjId) {
fn from(obj_item: &AMobjItem) -> Self {
(obj_item.value.0.clone(), obj_item.obj_id.as_ref().clone())
}
}
/// \memberof AMobjItem
/// \brief Gets the object identifier of an item in an object.
///
/// \param[in] obj_item A pointer to an `AMobjItem` struct.
/// \return A pointer to an `AMobjId` struct.
/// \pre \p obj_item `!= NULL`.
/// \internal
///
/// # Safety
/// obj_item must be a valid pointer to an AMobjItem
#[no_mangle]
pub unsafe extern "C" fn AMobjItemObjId(obj_item: *const AMobjItem) -> *const AMobjId {
if let Some(obj_item) = obj_item.as_ref() {
&obj_item.obj_id
} else {
std::ptr::null()
}
}
/// \memberof AMobjItem
/// \brief Gets the value of an item in an object.
///
/// \param[in] obj_item A pointer to an `AMobjItem` struct.
/// \return An `AMvalue` struct.
/// \pre \p obj_item `!= NULL`.
/// \internal
///
/// # Safety
/// obj_item must be a valid pointer to an AMobjItem
#[no_mangle]
pub unsafe extern "C" fn AMobjItemValue<'a>(obj_item: *const AMobjItem) -> AMvalue<'a> {
if let Some(obj_item) = obj_item.as_ref() {
(&obj_item.value.0, &obj_item.value.1).into()
} else {
AMvalue::Void
}
}

View file

@ -0,0 +1,341 @@
use std::ffi::c_void;
use std::mem::size_of;
use crate::obj::item::AMobjItem;
#[repr(C)]
struct Detail {
len: usize,
offset: isize,
ptr: *const c_void,
}
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
/// propagate the name of a constant initialized from it so if the
/// constant's name is a symbolic representation of the value it can be
/// converted into a number by post-processing the header it generated.
pub const USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
impl Detail {
fn new(obj_items: &[AMobjItem], offset: isize) -> Self {
Self {
len: obj_items.len(),
offset,
ptr: obj_items.as_ptr() as *const c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, len))
}
}
}
pub fn get_index(&self) -> usize {
(self.offset
+ if self.offset < 0 {
self.len as isize
} else {
0
}) as usize
}
pub fn next(&mut self, n: isize) -> Option<&AMobjItem> {
if self.is_stopped() {
return None;
}
let slice: &[AMobjItem] =
unsafe { std::slice::from_raw_parts(self.ptr as *const AMobjItem, self.len) };
let value = &slice[self.get_index()];
self.advance(n);
Some(value)
}
pub fn is_stopped(&self) -> bool {
let len = self.len as isize;
self.offset < -len || self.offset == len
}
pub fn prev(&mut self, n: isize) -> Option<&AMobjItem> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &[AMobjItem] =
unsafe { std::slice::from_raw_parts(self.ptr as *const AMobjItem, self.len) };
Some(&slice[self.get_index()])
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
ptr: self.ptr,
}
}
}
impl From<Detail> for [u8; USIZE_USIZE_USIZE_] {
fn from(detail: Detail) -> Self {
unsafe {
std::slice::from_raw_parts((&detail as *const Detail) as *const u8, USIZE_USIZE_USIZE_)
.try_into()
.unwrap()
}
}
}
/// \struct AMobjItems
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of object items.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMobjItems {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_],
}
impl AMobjItems {
pub fn new(obj_items: &[AMobjItem]) -> Self {
Self {
detail: Detail::new(obj_items, 0).into(),
}
}
pub fn advance(&mut self, n: isize) {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.advance(n);
}
pub fn len(&self) -> usize {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
detail.len
}
pub fn next(&mut self, n: isize) -> Option<&AMobjItem> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<&AMobjItem> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.prev(n)
}
pub fn reversed(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.reversed().into(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
}
}
}
impl AsRef<[AMobjItem]> for AMobjItems {
fn as_ref(&self) -> &[AMobjItem] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const AMobjItem, detail.len) }
}
}
impl Default for AMobjItems {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMobjItems
/// \brief Advances an iterator over a sequence of object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] obj_items A pointer to an `AMobjItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p obj_items `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsAdvance(obj_items: *mut AMobjItems, n: isize) {
if let Some(obj_items) = obj_items.as_mut() {
obj_items.advance(n);
};
}
/// \memberof AMobjItems
/// \brief Tests the equality of two sequences of object items underlying a
/// pair of iterators.
///
/// \param[in] obj_items1 A pointer to an `AMobjItems` struct.
/// \param[in] obj_items2 A pointer to an `AMobjItems` struct.
/// \return `true` if \p obj_items1 `==` \p obj_items2 and `false` otherwise.
/// \pre \p obj_items1 `!= NULL`.
/// \pre \p obj_items2 `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items1 must be a valid pointer to an AMobjItems
/// obj_items2 must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsEqual(
obj_items1: *const AMobjItems,
obj_items2: *const AMobjItems,
) -> bool {
match (obj_items1.as_ref(), obj_items2.as_ref()) {
(Some(obj_items1), Some(obj_items2)) => obj_items1.as_ref() == obj_items2.as_ref(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMobjItems
/// \brief Gets the object item at the current position of an iterator over a
/// sequence of object items and then advances it by at most \p |n|
/// positions where the sign of \p n is relative to the iterator's
/// direction.
///
/// \param[in,out] obj_items A pointer to an `AMobjItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMobjItem` struct that's `NULL` when \p obj_items
/// was previously advanced past its forward/reverse limit.
/// \pre \p obj_items `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsNext(obj_items: *mut AMobjItems, n: isize) -> *const AMobjItem {
if let Some(obj_items) = obj_items.as_mut() {
if let Some(obj_item) = obj_items.next(n) {
return obj_item;
}
}
std::ptr::null()
}
/// \memberof AMobjItems
/// \brief Advances an iterator over a sequence of object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the object item at its new
/// position.
///
/// \param[in,out] obj_items A pointer to an `AMobjItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMobjItem` struct that's `NULL` when \p obj_items
/// is presently advanced past its forward/reverse limit.
/// \pre \p obj_items `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsPrev(obj_items: *mut AMobjItems, n: isize) -> *const AMobjItem {
if let Some(obj_items) = obj_items.as_mut() {
if let Some(obj_item) = obj_items.prev(n) {
return obj_item;
}
}
std::ptr::null()
}
/// \memberof AMobjItems
/// \brief Gets the size of the sequence of object items underlying an
/// iterator.
///
/// \param[in] obj_items A pointer to an `AMobjItems` struct.
/// \return The count of values in \p obj_items.
/// \pre \p obj_items `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsSize(obj_items: *const AMobjItems) -> usize {
if let Some(obj_items) = obj_items.as_ref() {
obj_items.len()
} else {
0
}
}
/// \memberof AMobjItems
/// \brief Creates an iterator over the same sequence of object items as the
/// given one but with the opposite position and direction.
///
/// \param[in] obj_items A pointer to an `AMobjItems` struct.
/// \return An `AMobjItems` struct
/// \pre \p obj_items `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsReversed(obj_items: *const AMobjItems) -> AMobjItems {
if let Some(obj_items) = obj_items.as_ref() {
obj_items.reversed()
} else {
AMobjItems::default()
}
}
/// \memberof AMobjItems
/// \brief Creates an iterator at the starting position over the same sequence
/// of object items as the given one.
///
/// \param[in] obj_items A pointer to an `AMobjItems` struct.
/// \return An `AMobjItems` struct
/// \pre \p obj_items `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsRewound(obj_items: *const AMobjItems) -> AMobjItems {
if let Some(obj_items) = obj_items.as_ref() {
obj_items.rewound()
} else {
AMobjItems::default()
}
}

914
automerge-c/src/result.rs Normal file
View file

@ -0,0 +1,914 @@
use automerge as am;
use libc::strcmp;
use smol_str::SmolStr;
use std::any::type_name;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::ffi::CString;
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
use std::os::raw::c_char;
use crate::actor_id::AMactorId;
use crate::byte_span::AMbyteSpan;
use crate::change::AMchange;
use crate::change_hashes::AMchangeHashes;
use crate::changes::AMchanges;
use crate::doc::list::{item::AMlistItem, items::AMlistItems};
use crate::doc::map::{item::AMmapItem, items::AMmapItems};
use crate::doc::utils::to_str;
use crate::doc::AMdoc;
use crate::obj::item::AMobjItem;
use crate::obj::items::AMobjItems;
use crate::obj::AMobjId;
use crate::strs::AMstrs;
use crate::sync::{AMsyncMessage, AMsyncState};
/// \struct AMvalue
/// \installed_headerfile
/// \brief A discriminated union of value type variants for a result.
///
/// \enum AMvalueVariant
/// \brief A value type discriminant.
///
/// \var AMvalue::actor_id
/// An actor identifier as a pointer to an `AMactorId` struct.
///
/// \var AMvalue::boolean
/// A boolean.
///
/// \var AMvalue::bytes
/// A sequence of bytes as an `AMbyteSpan` struct.
///
/// \var AMvalue::change_hashes
/// A sequence of change hashes as an `AMchangeHashes` struct.
///
/// \var AMvalue::changes
/// A sequence of changes as an `AMchanges` struct.
///
/// \var AMvalue::counter
/// A CRDT counter.
///
/// \var AMvalue::doc
/// A document as a pointer to an `AMdoc` struct.
///
/// \var AMvalue::f64
/// A 64-bit float.
///
/// \var AMvalue::int_
/// A 64-bit signed integer.
///
/// \var AMvalue::list_items
/// A sequence of list object items as an `AMlistItems` struct.
///
/// \var AMvalue::map_items
/// A sequence of map object items as an `AMmapItems` struct.
///
/// \var AMvalue::obj_id
/// An object identifier as a pointer to an `AMobjId` struct.
///
/// \var AMvalue::obj_items
/// A sequence of object items as an `AMobjItems` struct.
///
/// \var AMvalue::str
/// A UTF-8 string.
///
/// \var AMvalue::strs
/// A sequence of UTF-8 strings as an `AMstrs` struct.
///
/// \var AMvalue::sync_message
/// A synchronization message as a pointer to an `AMsyncMessage` struct.
///
/// \var AMvalue::sync_state
/// A synchronization state as a pointer to an `AMsyncState` struct.
///
/// \var AMvalue::tag
/// The variant discriminator.
///
/// \var AMvalue::timestamp
/// A Lamport timestamp.
///
/// \var AMvalue::uint
/// A 64-bit unsigned integer.
///
/// \var AMvalue::unknown
/// A value of unknown type as an `AMunknownValue` struct.
#[repr(u8)]
pub enum AMvalue<'a> {
/// A void variant.
/// \note This tag is unalphabetized so that a zeroed struct will have it.
Void,
/// An actor identifier variant.
ActorId(&'a AMactorId),
/// A boolean variant.
Boolean(bool),
/// A byte array variant.
Bytes(AMbyteSpan),
/// A change hashes variant.
ChangeHashes(AMchangeHashes),
/// A changes variant.
Changes(AMchanges),
/// A CRDT counter variant.
Counter(i64),
/// A document variant.
Doc(*mut AMdoc),
/// A 64-bit float variant.
F64(f64),
/// A 64-bit signed integer variant.
Int(i64),
/// A list items variant.
ListItems(AMlistItems),
/// A map items variant.
MapItems(AMmapItems),
/// A null variant.
Null,
/// An object identifier variant.
ObjId(&'a AMobjId),
/// An object items variant.
ObjItems(AMobjItems),
/// A UTF-8 string variant.
Str(*const libc::c_char),
/// A UTF-8 strings variant.
Strs(AMstrs),
/// A synchronization message variant.
SyncMessage(&'a AMsyncMessage),
/// A synchronization state variant.
SyncState(&'a mut AMsyncState),
/// A Lamport timestamp variant.
Timestamp(i64),
/// A 64-bit unsigned integer variant.
Uint(u64),
/// An unknown type of scalar value variant.
Unknown(AMunknownValue),
}
impl<'a> PartialEq for AMvalue<'a> {
fn eq(&self, other: &Self) -> bool {
use AMvalue::*;
match (self, other) {
(ActorId(lhs), ActorId(rhs)) => *lhs == *rhs,
(Boolean(lhs), Boolean(rhs)) => lhs == rhs,
(Bytes(lhs), Bytes(rhs)) => lhs == rhs,
(ChangeHashes(lhs), ChangeHashes(rhs)) => lhs == rhs,
(Changes(lhs), Changes(rhs)) => lhs == rhs,
(Counter(lhs), Counter(rhs)) => lhs == rhs,
(Doc(lhs), Doc(rhs)) => *lhs == *rhs,
(F64(lhs), F64(rhs)) => lhs == rhs,
(Int(lhs), Int(rhs)) => lhs == rhs,
(ListItems(lhs), ListItems(rhs)) => lhs == rhs,
(MapItems(lhs), MapItems(rhs)) => lhs == rhs,
(ObjId(lhs), ObjId(rhs)) => *lhs == *rhs,
(ObjItems(lhs), ObjItems(rhs)) => lhs == rhs,
(Str(lhs), Str(rhs)) => unsafe { strcmp(*lhs, *rhs) == 0 },
(Strs(lhs), Strs(rhs)) => lhs == rhs,
(SyncMessage(lhs), SyncMessage(rhs)) => *lhs == *rhs,
(SyncState(lhs), SyncState(rhs)) => *lhs == *rhs,
(Timestamp(lhs), Timestamp(rhs)) => lhs == rhs,
(Uint(lhs), Uint(rhs)) => lhs == rhs,
(Unknown(lhs), Unknown(rhs)) => lhs == rhs,
(Null, Null) | (Void, Void) => true,
_ => false,
}
}
}
impl From<(&am::Value<'_>, &RefCell<Option<CString>>)> for AMvalue<'_> {
fn from((value, c_str): (&am::Value<'_>, &RefCell<Option<CString>>)) -> Self {
match value {
am::Value::Scalar(scalar) => match scalar.as_ref() {
am::ScalarValue::Boolean(flag) => AMvalue::Boolean(*flag),
am::ScalarValue::Bytes(bytes) => AMvalue::Bytes(bytes.as_slice().into()),
am::ScalarValue::Counter(counter) => AMvalue::Counter(counter.into()),
am::ScalarValue::F64(float) => AMvalue::F64(*float),
am::ScalarValue::Int(int) => AMvalue::Int(*int),
am::ScalarValue::Null => AMvalue::Null,
am::ScalarValue::Str(smol_str) => {
let mut c_str = c_str.borrow_mut();
AMvalue::Str(match c_str.as_mut() {
None => {
let value_str = CString::new(smol_str.to_string()).unwrap();
c_str.insert(value_str).as_ptr()
}
Some(value_str) => value_str.as_ptr(),
})
}
am::ScalarValue::Timestamp(timestamp) => AMvalue::Timestamp(*timestamp),
am::ScalarValue::Uint(uint) => AMvalue::Uint(*uint),
am::ScalarValue::Unknown { bytes, type_code } => AMvalue::Unknown(AMunknownValue {
bytes: bytes.as_slice().into(),
type_code: *type_code,
}),
},
// \todo Confirm that an object variant should be ignored
// when there's no object ID variant.
am::Value::Object(_) => AMvalue::Void,
}
}
}
impl From<&AMvalue<'_>> for u8 {
fn from(value: &AMvalue) -> Self {
use AMvalue::*;
// \warning These numbers must correspond to the order in which the
// variants of an AMvalue are declared within it.
match value {
ActorId(_) => 1,
Boolean(_) => 2,
Bytes(_) => 3,
ChangeHashes(_) => 4,
Changes(_) => 5,
Counter(_) => 6,
Doc(_) => 7,
F64(_) => 8,
Int(_) => 9,
ListItems(_) => 10,
MapItems(_) => 11,
Null => 12,
ObjId(_) => 13,
ObjItems(_) => 14,
Str(_) => 15,
Strs(_) => 16,
SyncMessage(_) => 17,
SyncState(_) => 18,
Timestamp(_) => 19,
Uint(_) => 20,
Unknown(..) => 21,
Void => 0,
}
}
}
impl TryFrom<&AMvalue<'_>> for am::ScalarValue {
type Error = am::AutomergeError;
fn try_from(c_value: &AMvalue) -> Result<Self, Self::Error> {
use am::AutomergeError::InvalidValueType;
use AMvalue::*;
let expected = type_name::<am::ScalarValue>().to_string();
match c_value {
Boolean(b) => Ok(am::ScalarValue::Boolean(*b)),
Bytes(span) => {
let slice = unsafe { std::slice::from_raw_parts(span.src, span.count) };
Ok(am::ScalarValue::Bytes(slice.to_vec()))
}
Counter(c) => Ok(am::ScalarValue::Counter(c.into())),
F64(f) => Ok(am::ScalarValue::F64(*f)),
Int(i) => Ok(am::ScalarValue::Int(*i)),
Str(c_str) => {
let smol_str = unsafe { SmolStr::new(to_str(*c_str)) };
Ok(am::ScalarValue::Str(smol_str))
}
Timestamp(t) => Ok(am::ScalarValue::Timestamp(*t)),
Uint(u) => Ok(am::ScalarValue::Uint(*u)),
Null => Ok(am::ScalarValue::Null),
Unknown(AMunknownValue { bytes, type_code }) => {
let slice = unsafe { std::slice::from_raw_parts(bytes.src, bytes.count) };
Ok(am::ScalarValue::Unknown {
bytes: slice.to_vec(),
type_code: *type_code,
})
}
ActorId(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMactorId>().to_string(),
}),
ChangeHashes(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMchangeHashes>().to_string(),
}),
Changes(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMchanges>().to_string(),
}),
Doc(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMdoc>().to_string(),
}),
ListItems(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMlistItems>().to_string(),
}),
MapItems(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMmapItems>().to_string(),
}),
ObjId(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMobjId>().to_string(),
}),
ObjItems(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMobjItems>().to_string(),
}),
Strs(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMstrs>().to_string(),
}),
SyncMessage(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMsyncMessage>().to_string(),
}),
SyncState(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMsyncState>().to_string(),
}),
Void => Err(InvalidValueType {
expected,
unexpected: type_name::<()>().to_string(),
}),
}
}
}
/// \memberof AMvalue
/// \brief Tests the equality of two values.
///
/// \param[in] value1 A pointer to an `AMvalue` struct.
/// \param[in] value2 A pointer to an `AMvalue` struct.
/// \return `true` if \p value1 `==` \p value2 and `false` otherwise.
/// \pre \p value1 `!= NULL`.
/// \pre \p value2 `!= NULL`.
/// \internal
///
/// #Safety
/// value1 must be a valid AMvalue pointer
/// value2 must be a valid AMvalue pointer
#[no_mangle]
pub unsafe extern "C" fn AMvalueEqual(value1: *const AMvalue, value2: *const AMvalue) -> bool {
match (value1.as_ref(), value2.as_ref()) {
(Some(value1), Some(value2)) => *value1 == *value2,
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \struct AMresult
/// \installed_headerfile
/// \brief A discriminated union of result variants.
pub enum AMresult {
ActorId(am::ActorId, Option<AMactorId>),
ChangeHashes(Vec<am::ChangeHash>),
Changes(Vec<am::Change>, Option<BTreeMap<usize, AMchange>>),
Doc(Box<AMdoc>),
Error(CString),
ListItems(Vec<AMlistItem>),
MapItems(Vec<AMmapItem>),
ObjId(AMobjId),
ObjItems(Vec<AMobjItem>),
String(CString),
Strings(Vec<CString>),
SyncMessage(AMsyncMessage),
SyncState(Box<AMsyncState>),
Value(am::Value<'static>, RefCell<Option<CString>>),
Void,
}
impl AMresult {
pub(crate) fn err(s: &str) -> Self {
AMresult::Error(CString::new(s).unwrap())
}
}
impl From<am::AutoCommit> for AMresult {
fn from(auto_commit: am::AutoCommit) -> Self {
AMresult::Doc(Box::new(AMdoc::new(auto_commit)))
}
}
impl From<am::ChangeHash> for AMresult {
fn from(change_hash: am::ChangeHash) -> Self {
AMresult::ChangeHashes(vec![change_hash])
}
}
impl From<am::Keys<'_, '_>> for AMresult {
fn from(keys: am::Keys<'_, '_>) -> Self {
let cstrings: Vec<CString> = keys.map(|s| CString::new(s).unwrap()).collect();
AMresult::Strings(cstrings)
}
}
impl From<am::KeysAt<'_, '_>> for AMresult {
fn from(keys: am::KeysAt<'_, '_>) -> Self {
let cstrings: Vec<CString> = keys.map(|s| CString::new(s).unwrap()).collect();
AMresult::Strings(cstrings)
}
}
impl From<am::ListRange<'static, Range<usize>>> for AMresult {
fn from(list_range: am::ListRange<'static, Range<usize>>) -> Self {
AMresult::ListItems(
list_range
.map(|(i, v, o)| AMlistItem::new(i, v.clone(), o))
.collect(),
)
}
}
impl From<am::ListRangeAt<'static, Range<usize>>> for AMresult {
fn from(list_range: am::ListRangeAt<'static, Range<usize>>) -> Self {
AMresult::ListItems(
list_range
.map(|(i, v, o)| AMlistItem::new(i, v.clone(), o))
.collect(),
)
}
}
impl From<am::MapRange<'static, Range<String>>> for AMresult {
fn from(map_range: am::MapRange<'static, Range<String>>) -> Self {
let map_items: Vec<AMmapItem> = map_range
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
.collect();
AMresult::MapItems(map_items)
}
}
impl From<am::MapRangeAt<'static, Range<String>>> for AMresult {
fn from(map_range: am::MapRangeAt<'static, Range<String>>) -> Self {
let map_items: Vec<AMmapItem> = map_range
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
.collect();
AMresult::MapItems(map_items)
}
}
impl From<am::MapRange<'static, RangeFrom<String>>> for AMresult {
fn from(map_range: am::MapRange<'static, RangeFrom<String>>) -> Self {
let map_items: Vec<AMmapItem> = map_range
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
.collect();
AMresult::MapItems(map_items)
}
}
impl From<am::MapRangeAt<'static, RangeFrom<String>>> for AMresult {
fn from(map_range: am::MapRangeAt<'static, RangeFrom<String>>) -> Self {
let map_items: Vec<AMmapItem> = map_range
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
.collect();
AMresult::MapItems(map_items)
}
}
impl From<am::MapRange<'static, RangeFull>> for AMresult {
fn from(map_range: am::MapRange<'static, RangeFull>) -> Self {
let map_items: Vec<AMmapItem> = map_range
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
.collect();
AMresult::MapItems(map_items)
}
}
impl From<am::MapRangeAt<'static, RangeFull>> for AMresult {
fn from(map_range: am::MapRangeAt<'static, RangeFull>) -> Self {
let map_items: Vec<AMmapItem> = map_range
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
.collect();
AMresult::MapItems(map_items)
}
}
impl From<am::MapRange<'static, RangeTo<String>>> for AMresult {
fn from(map_range: am::MapRange<'static, RangeTo<String>>) -> Self {
let map_items: Vec<AMmapItem> = map_range
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
.collect();
AMresult::MapItems(map_items)
}
}
impl From<am::MapRangeAt<'static, RangeTo<String>>> for AMresult {
fn from(map_range: am::MapRangeAt<'static, RangeTo<String>>) -> Self {
let map_items: Vec<AMmapItem> = map_range
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
.collect();
AMresult::MapItems(map_items)
}
}
impl From<am::sync::State> for AMresult {
fn from(state: am::sync::State) -> Self {
AMresult::SyncState(Box::new(AMsyncState::new(state)))
}
}
impl From<am::Values<'static>> for AMresult {
fn from(pairs: am::Values<'static>) -> Self {
AMresult::ObjItems(pairs.map(|(v, o)| AMobjItem::new(v.clone(), o)).collect())
}
}
impl From<Result<Vec<(am::Value<'static>, am::ObjId)>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<Vec<(am::Value<'static>, am::ObjId)>, am::AutomergeError>) -> Self {
match maybe {
Ok(pairs) => AMresult::ObjItems(
pairs
.into_iter()
.map(|(v, o)| AMobjItem::new(v, o))
.collect(),
),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<AMresult> for *mut AMresult {
fn from(b: AMresult) -> Self {
Box::into_raw(Box::new(b))
}
}
impl From<Option<&am::Change>> for AMresult {
fn from(maybe: Option<&am::Change>) -> Self {
match maybe {
Some(change) => AMresult::Changes(vec![change.clone()], None),
None => AMresult::Void,
}
}
}
impl From<Option<am::sync::Message>> for AMresult {
fn from(maybe: Option<am::sync::Message>) -> Self {
match maybe {
Some(message) => AMresult::SyncMessage(AMsyncMessage::new(message)),
None => AMresult::Void,
}
}
}
impl From<Result<(), am::AutomergeError>> for AMresult {
fn from(maybe: Result<(), am::AutomergeError>) -> Self {
match maybe {
Ok(()) => AMresult::Void,
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::ActorId, am::AutomergeError>> for AMresult {
fn from(maybe: Result<am::ActorId, am::AutomergeError>) -> Self {
match maybe {
Ok(actor_id) => AMresult::ActorId(actor_id, None),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::ActorId, am::InvalidActorId>> for AMresult {
fn from(maybe: Result<am::ActorId, am::InvalidActorId>) -> Self {
match maybe {
Ok(actor_id) => AMresult::ActorId(actor_id, None),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::AutoCommit, am::AutomergeError>> for AMresult {
fn from(maybe: Result<am::AutoCommit, am::AutomergeError>) -> Self {
match maybe {
Ok(auto_commit) => AMresult::Doc(Box::new(AMdoc::new(auto_commit))),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::Change, am::LoadChangeError>> for AMresult {
fn from(maybe: Result<am::Change, am::LoadChangeError>) -> Self {
match maybe {
Ok(change) => AMresult::Changes(vec![change], None),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::ObjId, am::AutomergeError>> for AMresult {
fn from(maybe: Result<am::ObjId, am::AutomergeError>) -> Self {
match maybe {
Ok(obj_id) => AMresult::ObjId(AMobjId::new(obj_id)),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::sync::Message, am::sync::ReadMessageError>> for AMresult {
fn from(maybe: Result<am::sync::Message, am::sync::ReadMessageError>) -> Self {
match maybe {
Ok(message) => AMresult::SyncMessage(AMsyncMessage::new(message)),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::sync::State, am::sync::DecodeStateError>> for AMresult {
fn from(maybe: Result<am::sync::State, am::sync::DecodeStateError>) -> Self {
match maybe {
Ok(state) => AMresult::SyncState(Box::new(AMsyncState::new(state))),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::Value<'static>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<am::Value<'static>, am::AutomergeError>) -> Self {
match maybe {
Ok(value) => AMresult::Value(value, Default::default()),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Option<(am::Value<'static>, am::ObjId)>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<Option<(am::Value<'static>, am::ObjId)>, am::AutomergeError>) -> Self {
match maybe {
Ok(Some((value, obj_id))) => match value {
am::Value::Object(_) => AMresult::ObjId(AMobjId::new(obj_id)),
_ => AMresult::Value(value, Default::default()),
},
Ok(None) => AMresult::Void,
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<String, am::AutomergeError>> for AMresult {
fn from(maybe: Result<String, am::AutomergeError>) -> Self {
match maybe {
Ok(string) => AMresult::String(CString::new(string).unwrap()),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<usize, am::AutomergeError>> for AMresult {
fn from(maybe: Result<usize, am::AutomergeError>) -> Self {
match maybe {
Ok(size) => AMresult::Value(am::Value::uint(size as u64), Default::default()),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Vec<am::Change>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<Vec<am::Change>, am::AutomergeError>) -> Self {
match maybe {
Ok(changes) => AMresult::Changes(changes, None),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Vec<am::Change>, am::LoadChangeError>> for AMresult {
fn from(maybe: Result<Vec<am::Change>, am::LoadChangeError>) -> Self {
match maybe {
Ok(changes) => AMresult::Changes(changes, None),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Vec<&am::Change>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<Vec<&am::Change>, am::AutomergeError>) -> Self {
match maybe {
Ok(changes) => {
let changes: Vec<am::Change> =
changes.iter().map(|&change| change.clone()).collect();
AMresult::Changes(changes, None)
}
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Vec<am::ChangeHash>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<Vec<am::ChangeHash>, am::AutomergeError>) -> Self {
match maybe {
Ok(change_hashes) => AMresult::ChangeHashes(change_hashes),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Vec<am::ChangeHash>, am::InvalidChangeHashSlice>> for AMresult {
fn from(maybe: Result<Vec<am::ChangeHash>, am::InvalidChangeHashSlice>) -> Self {
match maybe {
Ok(change_hashes) => AMresult::ChangeHashes(change_hashes),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Vec<u8>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<Vec<u8>, am::AutomergeError>) -> Self {
match maybe {
Ok(bytes) => AMresult::Value(am::Value::bytes(bytes), Default::default()),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Vec<&am::Change>> for AMresult {
fn from(changes: Vec<&am::Change>) -> Self {
let changes: Vec<am::Change> = changes.iter().map(|&change| change.clone()).collect();
AMresult::Changes(changes, None)
}
}
impl From<Vec<am::ChangeHash>> for AMresult {
fn from(change_hashes: Vec<am::ChangeHash>) -> Self {
AMresult::ChangeHashes(change_hashes)
}
}
impl From<Vec<u8>> for AMresult {
fn from(bytes: Vec<u8>) -> Self {
AMresult::Value(am::Value::bytes(bytes), Default::default())
}
}
pub fn to_result<R: Into<AMresult>>(r: R) -> *mut AMresult {
(r.into()).into()
}
/// \ingroup enumerations
/// \enum AMstatus
/// \brief The status of an API call.
#[derive(Debug)]
#[repr(u8)]
pub enum AMstatus {
/// Success.
/// \note This tag is unalphabetized so that `0` indicates success.
Ok,
/// Failure due to an error.
Error,
/// Failure due to an invalid result.
InvalidResult,
}
/// \memberof AMresult
/// \brief Gets a result's error message string.
///
/// \param[in] result A pointer to an `AMresult` struct.
/// \return A UTF-8 string value or `NULL`.
/// \pre \p result `!= NULL`.
/// \internal
///
/// # Safety
/// result must be a valid pointer to an AMresult
#[no_mangle]
pub unsafe extern "C" fn AMerrorMessage(result: *const AMresult) -> *const c_char {
match result.as_ref() {
Some(AMresult::Error(s)) => s.as_ptr(),
_ => std::ptr::null::<c_char>(),
}
}
/// \memberof AMresult
/// \brief Deallocates the storage for a result.
///
/// \param[in,out] result A pointer to an `AMresult` struct.
/// \pre \p result `!= NULL`.
/// \internal
///
/// # Safety
/// result must be a valid pointer to an AMresult
#[no_mangle]
pub unsafe extern "C" fn AMfree(result: *mut AMresult) {
if !result.is_null() {
let result: AMresult = *Box::from_raw(result);
drop(result)
}
}
/// \memberof AMresult
/// \brief Gets the size of a result's value.
///
/// \param[in] result A pointer to an `AMresult` struct.
/// \return The count of values in \p result.
/// \pre \p result `!= NULL`.
/// \internal
///
/// # Safety
/// result must be a valid pointer to an AMresult
#[no_mangle]
pub unsafe extern "C" fn AMresultSize(result: *const AMresult) -> usize {
if let Some(result) = result.as_ref() {
use AMresult::*;
match result {
Error(_) | Void => 0,
ActorId(_, _)
| Doc(_)
| ObjId(_)
| String(_)
| SyncMessage(_)
| SyncState(_)
| Value(_, _) => 1,
ChangeHashes(change_hashes) => change_hashes.len(),
Changes(changes, _) => changes.len(),
ListItems(list_items) => list_items.len(),
MapItems(map_items) => map_items.len(),
ObjItems(obj_items) => obj_items.len(),
Strings(cstrings) => cstrings.len(),
}
} else {
0
}
}
/// \memberof AMresult
/// \brief Gets the status code of a result.
///
/// \param[in] result A pointer to an `AMresult` struct.
/// \return An `AMstatus` enum tag.
/// \pre \p result `!= NULL`.
/// \internal
///
/// # Safety
/// result must be a valid pointer to an AMresult
#[no_mangle]
pub unsafe extern "C" fn AMresultStatus(result: *const AMresult) -> AMstatus {
match result.as_ref() {
Some(AMresult::Error(_)) => AMstatus::Error,
None => AMstatus::InvalidResult,
_ => AMstatus::Ok,
}
}
/// \memberof AMresult
/// \brief Gets a result's value.
///
/// \param[in] result A pointer to an `AMresult` struct.
/// \return An `AMvalue` struct.
/// \pre \p result `!= NULL`.
/// \internal
///
/// # Safety
/// result must be a valid pointer to an AMresult
#[no_mangle]
pub unsafe extern "C" fn AMresultValue<'a>(result: *mut AMresult) -> AMvalue<'a> {
let mut content = AMvalue::Void;
if let Some(result) = result.as_mut() {
match result {
AMresult::ActorId(actor_id, c_actor_id) => match c_actor_id {
None => {
content = AMvalue::ActorId(&*c_actor_id.insert(AMactorId::new(&*actor_id)));
}
Some(c_actor_id) => {
content = AMvalue::ActorId(&*c_actor_id);
}
},
AMresult::ChangeHashes(change_hashes) => {
content = AMvalue::ChangeHashes(AMchangeHashes::new(change_hashes));
}
AMresult::Changes(changes, storage) => {
content = AMvalue::Changes(AMchanges::new(
changes,
storage.get_or_insert(BTreeMap::new()),
));
}
AMresult::Doc(doc) => content = AMvalue::Doc(&mut **doc),
AMresult::Error(_) => {}
AMresult::ListItems(list_items) => {
content = AMvalue::ListItems(AMlistItems::new(list_items));
}
AMresult::MapItems(map_items) => {
content = AMvalue::MapItems(AMmapItems::new(map_items));
}
AMresult::ObjId(obj_id) => {
content = AMvalue::ObjId(obj_id);
}
AMresult::ObjItems(obj_items) => {
content = AMvalue::ObjItems(AMobjItems::new(obj_items));
}
AMresult::String(cstring) => content = AMvalue::Str(cstring.as_ptr()),
AMresult::Strings(cstrings) => {
content = AMvalue::Strs(AMstrs::new(cstrings));
}
AMresult::SyncMessage(sync_message) => {
content = AMvalue::SyncMessage(sync_message);
}
AMresult::SyncState(sync_state) => {
content = AMvalue::SyncState(&mut *sync_state);
}
AMresult::Value(value, value_str) => {
content = (&*value, &*value_str).into();
}
AMresult::Void => {}
}
};
content
}
/// \struct AMunknownValue
/// \installed_headerfile
/// \brief A value (typically for a `set` operation) whose type is unknown.
///
#[derive(Eq, PartialEq)]
#[repr(C)]
pub struct AMunknownValue {
/// The value's raw bytes.
bytes: AMbyteSpan,
/// The value's encoded type identifier.
type_code: u8,
}

View file

@ -0,0 +1,156 @@
use crate::result::{AMfree, AMresult, AMresultStatus, AMresultValue, AMstatus, AMvalue};
/// \struct AMresultStack
/// \installed_headerfile
/// \brief A node in a singly-linked list of result pointers.
///
/// \note Using this data structure is purely optional because its only purpose
/// is to make memory management tolerable for direct usage of this API
/// in C, C++ and Objective-C.
#[repr(C)]
pub struct AMresultStack {
/// A result to be deallocated.
pub result: *mut AMresult,
/// The next node in the singly-linked list or `NULL`.
pub next: *mut AMresultStack,
}
impl AMresultStack {
pub fn new(result: *mut AMresult, next: *mut AMresultStack) -> Self {
Self { result, next }
}
}
/// \memberof AMresultStack
/// \brief Deallocates the storage for a stack of results.
///
/// \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
/// \return The number of `AMresult` structs freed.
/// \pre \p stack `!= NULL`.
/// \post `*stack == NULL`.
/// \note Calling this function is purely optional because its only purpose is
/// to make memory management tolerable for direct usage of this API in
/// C, C++ and Objective-C.
/// \internal
///
/// # Safety
/// stack must be a valid AMresultStack pointer pointer
#[no_mangle]
pub unsafe extern "C" fn AMfreeStack(stack: *mut *mut AMresultStack) -> usize {
if stack.is_null() {
return 0;
}
let mut count: usize = 0;
while !(*stack).is_null() {
AMfree(AMpop(stack));
count += 1;
}
count
}
/// \memberof AMresultStack
/// \brief Gets the topmost result from the stack after removing it.
///
/// \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
/// \return A pointer to an `AMresult` struct or `NULL`.
/// \pre \p stack `!= NULL`.
/// \post `*stack == NULL`.
/// \note Calling this function is purely optional because its only purpose is
/// to make memory management tolerable for direct usage of this API in
/// C, C++ and Objective-C.
/// \internal
///
/// # Safety
/// stack must be a valid AMresultStack pointer pointer
#[no_mangle]
pub unsafe extern "C" fn AMpop(stack: *mut *mut AMresultStack) -> *mut AMresult {
if stack.is_null() || (*stack).is_null() {
return std::ptr::null_mut();
}
let top = Box::from_raw(*stack);
*stack = top.next;
let result = top.result;
drop(top);
result
}
/// \memberof AMresultStack
/// \brief The prototype of a function to be called when a value matching the
/// given discriminant cannot be extracted from the result at the top of
/// the given stack.
///
/// \note Implementing this function is purely optional because its only purpose
/// is to make memory management tolerable for direct usage of this API
/// in C, C++ and Objective-C.
pub type AMpushCallback =
Option<extern "C" fn(stack: *mut *mut AMresultStack, discriminant: u8) -> ()>;
/// \memberof AMresultStack
/// \brief Pushes the given result onto the given stack and then either extracts
/// a value matching the given discriminant from that result or,
/// failing that, calls the given function and gets a void value instead.
///
/// \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
/// \param[in] result A pointer to an `AMresult` struct.
/// \param[in] discriminant An `AMvalue` variant's corresponding enum tag.
/// \param[in] callback A pointer to a function with the same signature as
/// `AMpushCallback()` or `NULL`.
/// \return An `AMvalue` struct.
/// \pre \p stack `!= NULL`.
/// \pre \p result `!= NULL`.
/// \warning If \p stack `== NULL` then \p result is deallocated in order to
/// prevent a memory leak.
/// \note Calling this function is purely optional because its only purpose is
/// to make memory management tolerable for direct usage of this API in
/// C, C++ and Objective-C.
/// \internal
///
/// # Safety
/// stack must be a valid AMresultStack pointer pointer
/// result must be a valid AMresult pointer
#[no_mangle]
pub unsafe extern "C" fn AMpush<'a>(
stack: *mut *mut AMresultStack,
result: *mut AMresult,
discriminant: u8,
callback: AMpushCallback,
) -> AMvalue<'a> {
if stack.is_null() {
// There's no stack to push the result onto so it has to be freed in
// order to prevent a memory leak.
AMfree(result);
if let Some(callback) = callback {
callback(stack, discriminant);
}
return AMvalue::Void;
} else if result.is_null() {
if let Some(callback) = callback {
callback(stack, discriminant);
}
return AMvalue::Void;
}
// Always push the result onto the stack, even if it's wrong, so that the
// given callback can retrieve it.
let node = Box::new(AMresultStack::new(result, *stack));
let top = Box::into_raw(node);
*stack = top;
// Test that the result contains a value.
match AMresultStatus(result) {
AMstatus::Ok => {}
_ => {
if let Some(callback) = callback {
callback(stack, discriminant);
}
return AMvalue::Void;
}
}
// Test that the result's value matches the given discriminant.
let value = AMresultValue(result);
if discriminant != u8::from(&value) {
if let Some(callback) = callback {
callback(stack, discriminant);
}
return AMvalue::Void;
}
value
}

344
automerge-c/src/strs.rs Normal file
View file

@ -0,0 +1,344 @@
use std::cmp::Ordering;
use std::ffi::{c_void, CString};
use std::mem::size_of;
use std::os::raw::c_char;
#[repr(C)]
struct Detail {
len: usize,
offset: isize,
ptr: *const c_void,
}
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
/// propagate the name of a constant initialized from it so if the
/// constant's name is a symbolic representation of the value it can be
/// converted into a number by post-processing the header it generated.
pub const USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
impl Detail {
fn new(c_strings: &[CString], offset: isize) -> Self {
Self {
len: c_strings.len(),
offset,
ptr: c_strings.as_ptr() as *const c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, len))
}
}
}
pub fn get_index(&self) -> usize {
(self.offset
+ if self.offset < 0 {
self.len as isize
} else {
0
}) as usize
}
pub fn next(&mut self, n: isize) -> Option<*const c_char> {
if self.is_stopped() {
return None;
}
let slice: &[CString] =
unsafe { std::slice::from_raw_parts(self.ptr as *const CString, self.len) };
let value = slice[self.get_index()].as_ptr();
self.advance(n);
Some(value)
}
pub fn is_stopped(&self) -> bool {
let len = self.len as isize;
self.offset < -len || self.offset == len
}
pub fn prev(&mut self, n: isize) -> Option<*const c_char> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &[CString] =
unsafe { std::slice::from_raw_parts(self.ptr as *const CString, self.len) };
Some(slice[self.get_index()].as_ptr())
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
ptr: self.ptr,
}
}
}
impl From<Detail> for [u8; USIZE_USIZE_USIZE_] {
fn from(detail: Detail) -> Self {
unsafe {
std::slice::from_raw_parts((&detail as *const Detail) as *const u8, USIZE_USIZE_USIZE_)
.try_into()
.unwrap()
}
}
}
/// \struct AMstrs
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of UTF-8 strings.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMstrs {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_],
}
impl AMstrs {
pub fn new(c_strings: &[CString]) -> Self {
Self {
detail: Detail::new(c_strings, 0).into(),
}
}
pub fn advance(&mut self, n: isize) {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.advance(n);
}
pub fn len(&self) -> usize {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
detail.len
}
pub fn next(&mut self, n: isize) -> Option<*const c_char> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<*const c_char> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.prev(n)
}
pub fn reversed(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.reversed().into(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
}
}
}
impl AsRef<[CString]> for AMstrs {
fn as_ref(&self) -> &[CString] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const CString, detail.len) }
}
}
impl Default for AMstrs {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMstrs
/// \brief Advances an iterator over a sequence of UTF-8 strings by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] strs A pointer to an `AMstrs` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p strs `!= NULL`.
/// \internal
///
/// #Safety
/// strs must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsAdvance(strs: *mut AMstrs, n: isize) {
if let Some(strs) = strs.as_mut() {
strs.advance(n);
};
}
/// \memberof AMstrs
/// \brief Compares the sequences of UTF-8 strings underlying a pair of
/// iterators.
///
/// \param[in] strs1 A pointer to an `AMstrs` struct.
/// \param[in] strs2 A pointer to an `AMstrs` struct.
/// \return `-1` if \p strs1 `<` \p strs2, `0` if
/// \p strs1 `==` \p strs2 and `1` if
/// \p strs1 `>` \p strs2.
/// \pre \p strs1 `!= NULL`.
/// \pre \p strs2 `!= NULL`.
/// \internal
///
/// #Safety
/// strs1 must be a valid pointer to an AMstrs
/// strs2 must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsCmp(strs1: *const AMstrs, strs2: *const AMstrs) -> isize {
match (strs1.as_ref(), strs2.as_ref()) {
(Some(strs1), Some(strs2)) => match strs1.as_ref().cmp(strs2.as_ref()) {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
},
(None, Some(_)) => -1,
(Some(_), None) => 1,
(None, None) => 0,
}
}
/// \memberof AMstrs
/// \brief Gets the key at the current position of an iterator over a sequence
/// of UTF-8 strings and then advances it by at most \p |n| positions
/// where the sign of \p n is relative to the iterator's direction.
///
/// \param[in,out] strs A pointer to an `AMstrs` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A UTF-8 string that's `NULL` when \p strs was previously advanced
/// past its forward/reverse limit.
/// \pre \p strs `!= NULL`.
/// \internal
///
/// #Safety
/// strs must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsNext(strs: *mut AMstrs, n: isize) -> *const c_char {
if let Some(strs) = strs.as_mut() {
if let Some(key) = strs.next(n) {
return key;
}
}
std::ptr::null()
}
/// \memberof AMstrs
/// \brief Advances an iterator over a sequence of UTF-8 strings by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the key at its new position.
///
/// \param[in,out] strs A pointer to an `AMstrs` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A UTF-8 string that's `NULL` when \p strs is presently advanced
/// past its forward/reverse limit.
/// \pre \p strs `!= NULL`.
/// \internal
///
/// #Safety
/// strs must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsPrev(strs: *mut AMstrs, n: isize) -> *const c_char {
if let Some(strs) = strs.as_mut() {
if let Some(key) = strs.prev(n) {
return key;
}
}
std::ptr::null()
}
/// \memberof AMstrs
/// \brief Gets the size of the sequence of UTF-8 strings underlying an
/// iterator.
///
/// \param[in] strs A pointer to an `AMstrs` struct.
/// \return The count of values in \p strs.
/// \pre \p strs `!= NULL`.
/// \internal
///
/// #Safety
/// strs must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsSize(strs: *const AMstrs) -> usize {
if let Some(strs) = strs.as_ref() {
strs.len()
} else {
0
}
}
/// \memberof AMstrs
/// \brief Creates an iterator over the same sequence of UTF-8 strings as the
/// given one but with the opposite position and direction.
///
/// \param[in] strs A pointer to an `AMstrs` struct.
/// \return An `AMstrs` struct.
/// \pre \p strs `!= NULL`.
/// \internal
///
/// #Safety
/// strs must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsReversed(strs: *const AMstrs) -> AMstrs {
if let Some(strs) = strs.as_ref() {
strs.reversed()
} else {
AMstrs::default()
}
}
/// \memberof AMstrs
/// \brief Creates an iterator at the starting position over the same sequence
/// of UTF-8 strings as the given one.
///
/// \param[in] strs A pointer to an `AMstrs` struct.
/// \return An `AMstrs` struct
/// \pre \p strs `!= NULL`.
/// \internal
///
/// #Safety
/// strs must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsRewound(strs: *const AMstrs) -> AMstrs {
if let Some(strs) = strs.as_ref() {
strs.rewound()
} else {
AMstrs::default()
}
}

View file

@ -1,7 +1,7 @@
mod have;
mod haves;
mod message;
mod state;
pub(crate) use have::AMsyncHave;
pub(crate) use message::{to_sync_message, AMsyncMessage};
pub(crate) use state::AMsyncState;

View file

@ -1,23 +1,23 @@
use automerge as am;
use crate::result::{to_result, AMresult};
use crate::change_hashes::AMchangeHashes;
/// \struct AMsyncHave
/// \installed_headerfile
/// \brief A summary of the changes that the sender of a synchronization
/// message already has.
#[derive(Clone, Eq, PartialEq)]
pub struct AMsyncHave(am::sync::Have);
pub struct AMsyncHave(*const am::sync::Have);
impl AMsyncHave {
pub fn new(have: am::sync::Have) -> Self {
pub fn new(have: &am::sync::Have) -> Self {
Self(have)
}
}
impl AsRef<am::sync::Have> for AMsyncHave {
fn as_ref(&self) -> &am::sync::Have {
&self.0
unsafe { &*self.0 }
}
}
@ -25,18 +25,17 @@ impl AsRef<am::sync::Have> for AMsyncHave {
/// \brief Gets the heads of the sender.
///
/// \param[in] sync_have A pointer to an `AMsyncHave` struct.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p sync_have `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_have `!= NULL`.
/// \internal
///
/// # Safety
/// sync_have must be a valid pointer to an AMsyncHave
#[no_mangle]
pub unsafe extern "C" fn AMsyncHaveLastSync(sync_have: *const AMsyncHave) -> *mut AMresult {
to_result(match sync_have.as_ref() {
Some(sync_have) => sync_have.as_ref().last_sync.as_slice(),
None => Default::default(),
})
pub unsafe extern "C" fn AMsyncHaveLastSync(sync_have: *const AMsyncHave) -> AMchangeHashes {
if let Some(sync_have) = sync_have.as_ref() {
AMchangeHashes::new(&sync_have.as_ref().last_sync)
} else {
AMchangeHashes::default()
}
}

View file

@ -0,0 +1,378 @@
use automerge as am;
use std::collections::BTreeMap;
use std::ffi::c_void;
use std::mem::size_of;
use crate::sync::have::AMsyncHave;
#[repr(C)]
struct Detail {
len: usize,
offset: isize,
ptr: *const c_void,
storage: *mut c_void,
}
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
/// propagate the name of a constant initialized from it so if the
/// constant's name is a symbolic representation of the value it can be
/// converted into a number by post-processing the header it generated.
pub const USIZE_USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
impl Detail {
fn new(
haves: &[am::sync::Have],
offset: isize,
storage: &mut BTreeMap<usize, AMsyncHave>,
) -> Self {
let storage: *mut BTreeMap<usize, AMsyncHave> = storage;
Self {
len: haves.len(),
offset,
ptr: haves.as_ptr() as *const c_void,
storage: storage as *mut c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, len))
}
}
}
pub fn get_index(&self) -> usize {
(self.offset
+ if self.offset < 0 {
self.len as isize
} else {
0
}) as usize
}
pub fn next(&mut self, n: isize) -> Option<*const AMsyncHave> {
if self.is_stopped() {
return None;
}
let slice: &[am::sync::Have] =
unsafe { std::slice::from_raw_parts(self.ptr as *const am::sync::Have, self.len) };
let storage = unsafe { &mut *(self.storage as *mut BTreeMap<usize, AMsyncHave>) };
let index = self.get_index();
let value = match storage.get_mut(&index) {
Some(value) => value,
None => {
storage.insert(index, AMsyncHave::new(&slice[index]));
storage.get_mut(&index).unwrap()
}
};
self.advance(n);
Some(value)
}
pub fn is_stopped(&self) -> bool {
let len = self.len as isize;
self.offset < -len || self.offset == len
}
pub fn prev(&mut self, n: isize) -> Option<*const AMsyncHave> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &[am::sync::Have] =
unsafe { std::slice::from_raw_parts(self.ptr as *const am::sync::Have, self.len) };
let storage = unsafe { &mut *(self.storage as *mut BTreeMap<usize, AMsyncHave>) };
let index = self.get_index();
Some(match storage.get_mut(&index) {
Some(value) => value,
None => {
storage.insert(index, AMsyncHave::new(&slice[index]));
storage.get_mut(&index).unwrap()
}
})
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
storage: self.storage,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
ptr: self.ptr,
storage: self.storage,
}
}
}
impl From<Detail> for [u8; USIZE_USIZE_USIZE_USIZE_] {
fn from(detail: Detail) -> Self {
unsafe {
std::slice::from_raw_parts(
(&detail as *const Detail) as *const u8,
USIZE_USIZE_USIZE_USIZE_,
)
.try_into()
.unwrap()
}
}
}
/// \struct AMsyncHaves
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of synchronization haves.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMsyncHaves {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_USIZE_],
}
impl AMsyncHaves {
pub fn new(haves: &[am::sync::Have], storage: &mut BTreeMap<usize, AMsyncHave>) -> Self {
Self {
detail: Detail::new(haves, 0, storage).into(),
}
}
pub fn advance(&mut self, n: isize) {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.advance(n);
}
pub fn len(&self) -> usize {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
detail.len
}
pub fn next(&mut self, n: isize) -> Option<*const AMsyncHave> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<*const AMsyncHave> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.prev(n)
}
pub fn reversed(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.reversed().into(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
}
}
}
impl AsRef<[am::sync::Have]> for AMsyncHaves {
fn as_ref(&self) -> &[am::sync::Have] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const am::sync::Have, detail.len) }
}
}
impl Default for AMsyncHaves {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMsyncHaves
/// \brief Advances an iterator over a sequence of synchronization haves by at
/// most \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] sync_haves A pointer to an `AMsyncHaves` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p sync_haves `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves must be a valid pointer to an AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesAdvance(sync_haves: *mut AMsyncHaves, n: isize) {
if let Some(sync_haves) = sync_haves.as_mut() {
sync_haves.advance(n);
};
}
/// \memberof AMsyncHaves
/// \brief Tests the equality of two sequences of synchronization haves
/// underlying a pair of iterators.
///
/// \param[in] sync_haves1 A pointer to an `AMsyncHaves` struct.
/// \param[in] sync_haves2 A pointer to an `AMsyncHaves` struct.
/// \return `true` if \p sync_haves1 `==` \p sync_haves2 and `false` otherwise.
/// \pre \p sync_haves1 `!= NULL`.
/// \pre \p sync_haves2 `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves1 must be a valid pointer to an AMsyncHaves
/// sync_haves2 must be a valid pointer to an AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesEqual(
sync_haves1: *const AMsyncHaves,
sync_haves2: *const AMsyncHaves,
) -> bool {
match (sync_haves1.as_ref(), sync_haves2.as_ref()) {
(Some(sync_haves1), Some(sync_haves2)) => sync_haves1.as_ref() == sync_haves2.as_ref(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMsyncHaves
/// \brief Gets the synchronization have at the current position of an iterator
/// over a sequence of synchronization haves and then advances it by at
/// most \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] sync_haves A pointer to an `AMsyncHaves` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMsyncHave` struct that's `NULL` when
/// \p sync_haves was previously advanced past its forward/reverse
/// limit.
/// \pre \p sync_haves `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves must be a valid pointer to an AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesNext(
sync_haves: *mut AMsyncHaves,
n: isize,
) -> *const AMsyncHave {
if let Some(sync_haves) = sync_haves.as_mut() {
if let Some(sync_have) = sync_haves.next(n) {
return sync_have;
}
}
std::ptr::null()
}
/// \memberof AMsyncHaves
/// \brief Advances an iterator over a sequence of synchronization haves by at
/// most \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the synchronization have at its
/// new position.
///
/// \param[in,out] sync_haves A pointer to an `AMsyncHaves` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMsyncHave` struct that's `NULL` when
/// \p sync_haves is presently advanced past its forward/reverse limit.
/// \pre \p sync_haves `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves must be a valid pointer to an AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesPrev(
sync_haves: *mut AMsyncHaves,
n: isize,
) -> *const AMsyncHave {
if let Some(sync_haves) = sync_haves.as_mut() {
if let Some(sync_have) = sync_haves.prev(n) {
return sync_have;
}
}
std::ptr::null()
}
/// \memberof AMsyncHaves
/// \brief Gets the size of the sequence of synchronization haves underlying an
/// iterator.
///
/// \param[in] sync_haves A pointer to an `AMsyncHaves` struct.
/// \return The count of values in \p sync_haves.
/// \pre \p sync_haves `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves must be a valid pointer to an AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesSize(sync_haves: *const AMsyncHaves) -> usize {
if let Some(sync_haves) = sync_haves.as_ref() {
sync_haves.len()
} else {
0
}
}
/// \memberof AMsyncHaves
/// \brief Creates an iterator over the same sequence of synchronization haves
/// as the given one but with the opposite position and direction.
///
/// \param[in] sync_haves A pointer to an `AMsyncHaves` struct.
/// \return An `AMsyncHaves` struct
/// \pre \p sync_haves `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves must be a valid pointer to an AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesReversed(sync_haves: *const AMsyncHaves) -> AMsyncHaves {
if let Some(sync_haves) = sync_haves.as_ref() {
sync_haves.reversed()
} else {
AMsyncHaves::default()
}
}
/// \memberof AMsyncHaves
/// \brief Creates an iterator at the starting position over the same sequence
/// of synchronization haves as the given one.
///
/// \param[in] sync_haves A pointer to an `AMsyncHaves` struct.
/// \return An `AMsyncHaves` struct
/// \pre \p sync_haves `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves must be a valid pointer to an AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesRewound(sync_haves: *const AMsyncHaves) -> AMsyncHaves {
if let Some(sync_haves) = sync_haves.as_ref() {
sync_haves.rewound()
} else {
AMsyncHaves::default()
}
}

View file

@ -3,15 +3,18 @@ use std::cell::RefCell;
use std::collections::BTreeMap;
use crate::change::AMchange;
use crate::change_hashes::AMchangeHashes;
use crate::changes::AMchanges;
use crate::result::{to_result, AMresult};
use crate::sync::have::AMsyncHave;
use crate::sync::haves::AMsyncHaves;
macro_rules! to_sync_message {
($handle:expr) => {{
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::error("Invalid `AMsyncMessage*`").into(),
None => return AMresult::err("Invalid AMsyncMessage pointer").into(),
}
}};
}
@ -48,54 +51,55 @@ impl AsRef<am::sync::Message> for AMsyncMessage {
/// \brief Gets the changes for the recipient to apply.
///
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE` items.
/// \pre \p sync_message `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return An `AMchanges` struct.
/// \pre \p sync_message `!= NULL`.
/// \internal
///
/// # Safety
/// sync_message must be a valid pointer to an AMsyncMessage
#[no_mangle]
pub unsafe extern "C" fn AMsyncMessageChanges(sync_message: *const AMsyncMessage) -> *mut AMresult {
to_result(match sync_message.as_ref() {
Some(sync_message) => sync_message.body.changes.as_slice(),
None => Default::default(),
})
pub unsafe extern "C" fn AMsyncMessageChanges(sync_message: *const AMsyncMessage) -> AMchanges {
if let Some(sync_message) = sync_message.as_ref() {
AMchanges::new(
&sync_message.body.changes,
&mut sync_message.changes_storage.borrow_mut(),
)
} else {
AMchanges::default()
}
}
/// \memberof AMsyncMessage
/// \brief Decodes an array of bytes into a synchronization message.
/// \brief Decodes a sequence of bytes into a synchronization message.
///
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The count of bytes to decode from the array pointed to by
/// \p src.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_SYNC_MESSAGE` item.
/// \pre \p src `!= NULL`
/// \pre `sizeof(`\p src `) > 0`
/// \pre \p count `<= sizeof(`\p src `)`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] count The number of bytes in \p src to decode.
/// \return A pointer to an `AMresult` struct containing an `AMsyncMessage`
/// struct.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// src must be a byte array of length `>= count`
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMsyncMessageDecode(src: *const u8, count: usize) -> *mut AMresult {
let data = std::slice::from_raw_parts(src, count);
to_result(am::sync::Message::decode(data))
let mut data = Vec::new();
data.extend_from_slice(std::slice::from_raw_parts(src, count));
to_result(am::sync::Message::decode(&data))
}
/// \memberof AMsyncMessage
/// \brief Encodes a synchronization message as an array of bytes.
/// \brief Encodes a synchronization message as a sequence of bytes.
///
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_BYTES` item.
/// \pre \p sync_message `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return A pointer to an `AMresult` struct containing an array of bytes as
/// an `AMbyteSpan` struct.
/// \pre \p sync_message `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// sync_message must be a valid pointer to an AMsyncMessage
#[no_mangle]
@ -108,40 +112,41 @@ pub unsafe extern "C" fn AMsyncMessageEncode(sync_message: *const AMsyncMessage)
/// \brief Gets a summary of the changes that the sender already has.
///
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return A pointer to an `AMresult` struct with `AM_SYNC_HAVE` items.
/// \pre \p sync_message `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return An `AMhaves` struct.
/// \pre \p sync_message `!= NULL`.
/// \internal
///
/// # Safety
/// sync_message must be a valid pointer to an AMsyncMessage
#[no_mangle]
pub unsafe extern "C" fn AMsyncMessageHaves(sync_message: *const AMsyncMessage) -> *mut AMresult {
to_result(match sync_message.as_ref() {
Some(sync_message) => sync_message.as_ref().have.as_slice(),
None => Default::default(),
})
pub unsafe extern "C" fn AMsyncMessageHaves(sync_message: *const AMsyncMessage) -> AMsyncHaves {
if let Some(sync_message) = sync_message.as_ref() {
AMsyncHaves::new(
&sync_message.as_ref().have,
&mut sync_message.haves_storage.borrow_mut(),
)
} else {
AMsyncHaves::default()
}
}
/// \memberof AMsyncMessage
/// \brief Gets the heads of the sender.
///
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p sync_message `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_message `!= NULL`.
/// \internal
///
/// # Safety
/// sync_message must be a valid pointer to an AMsyncMessage
#[no_mangle]
pub unsafe extern "C" fn AMsyncMessageHeads(sync_message: *const AMsyncMessage) -> *mut AMresult {
to_result(match sync_message.as_ref() {
Some(sync_message) => sync_message.as_ref().heads.as_slice(),
None => Default::default(),
})
pub unsafe extern "C" fn AMsyncMessageHeads(sync_message: *const AMsyncMessage) -> AMchangeHashes {
if let Some(sync_message) = sync_message.as_ref() {
AMchangeHashes::new(&sync_message.as_ref().heads)
} else {
AMchangeHashes::default()
}
}
/// \memberof AMsyncMessage
@ -149,18 +154,17 @@ pub unsafe extern "C" fn AMsyncMessageHeads(sync_message: *const AMsyncMessage)
/// by the recipient.
///
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p sync_message `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_message `!= NULL`.
/// \internal
///
/// # Safety
/// sync_message must be a valid pointer to an AMsyncMessage
#[no_mangle]
pub unsafe extern "C" fn AMsyncMessageNeeds(sync_message: *const AMsyncMessage) -> *mut AMresult {
to_result(match sync_message.as_ref() {
Some(sync_message) => sync_message.as_ref().need.as_slice(),
None => Default::default(),
})
pub unsafe extern "C" fn AMsyncMessageNeeds(sync_message: *const AMsyncMessage) -> AMchangeHashes {
if let Some(sync_message) = sync_message.as_ref() {
AMchangeHashes::new(&sync_message.as_ref().need)
} else {
AMchangeHashes::default()
}
}

View file

@ -2,15 +2,17 @@ use automerge as am;
use std::cell::RefCell;
use std::collections::BTreeMap;
use crate::change_hashes::AMchangeHashes;
use crate::result::{to_result, AMresult};
use crate::sync::have::AMsyncHave;
use crate::sync::haves::AMsyncHaves;
macro_rules! to_sync_state {
($handle:expr) => {{
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::error("Invalid `AMsyncState*`").into(),
None => return AMresult::err("Invalid AMsyncState pointer").into(),
}
}};
}
@ -54,37 +56,36 @@ impl From<AMsyncState> for *mut AMsyncState {
}
/// \memberof AMsyncState
/// \brief Decodes an array of bytes into a synchronization state.
/// \brief Decodes a sequence of bytes into a synchronization state.
///
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The count of bytes to decode from the array pointed to by
/// \p src.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_SYNC_STATE` item.
/// \pre \p src `!= NULL`
/// \pre `sizeof(`\p src `) > 0`
/// \pre \p count `<= sizeof(`\p src `)`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \param[in] count The number of bytes in \p src to decode.
/// \return A pointer to an `AMresult` struct containing an `AMsyncState`
/// struct.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// src must be a byte array of length `>= count`
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMsyncStateDecode(src: *const u8, count: usize) -> *mut AMresult {
let data = std::slice::from_raw_parts(src, count);
to_result(am::sync::State::decode(data))
let mut data = Vec::new();
data.extend_from_slice(std::slice::from_raw_parts(src, count));
to_result(am::sync::State::decode(&data))
}
/// \memberof AMsyncState
/// \brief Encodes a synchronization state as an array of bytes.
/// \brief Encodes a synchronizaton state as a sequence of bytes.
///
/// \param[in] sync_state A pointer to an `AMsyncState` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_BYTE_SPAN` item.
/// \pre \p sync_state `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return A pointer to an `AMresult` struct containing an array of bytes as
/// an `AMbyteSpan` struct.
/// \pre \p sync_state `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an AMsyncState
#[no_mangle]
@ -99,9 +100,8 @@ pub unsafe extern "C" fn AMsyncStateEncode(sync_state: *const AMsyncState) -> *m
/// \param[in] sync_state1 A pointer to an `AMsyncState` struct.
/// \param[in] sync_state2 A pointer to an `AMsyncState` struct.
/// \return `true` if \p sync_state1 `==` \p sync_state2 and `false` otherwise.
/// \pre \p sync_state1 `!= NULL`
/// \pre \p sync_state2 `!= NULL`
/// \post `!(`\p sync_state1 `&&` \p sync_state2 `) -> false`
/// \pre \p sync_state1 `!= NULL`.
/// \pre \p sync_state2 `!= NULL`.
/// \internal
///
/// #Safety
@ -114,17 +114,18 @@ pub unsafe extern "C" fn AMsyncStateEqual(
) -> bool {
match (sync_state1.as_ref(), sync_state2.as_ref()) {
(Some(sync_state1), Some(sync_state2)) => sync_state1.as_ref() == sync_state2.as_ref(),
(None, None) | (None, Some(_)) | (Some(_), None) => false,
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMsyncState
/// \brief Allocates a new synchronization state and initializes it from
/// default values.
/// \brief Allocates a new synchronization state and initializes it with
/// defaults.
///
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_SYNC_STATE` item.
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMsyncState` struct.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
#[no_mangle]
pub extern "C" fn AMsyncStateInit() -> *mut AMresult {
to_result(am::sync::State::new())
@ -134,36 +135,40 @@ pub extern "C" fn AMsyncStateInit() -> *mut AMresult {
/// \brief Gets the heads that are shared by both peers.
///
/// \param[in] sync_state A pointer to an `AMsyncState` struct.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p sync_state `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_state `!= NULL`.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an AMsyncState
#[no_mangle]
pub unsafe extern "C" fn AMsyncStateSharedHeads(sync_state: *const AMsyncState) -> *mut AMresult {
let sync_state = to_sync_state!(sync_state);
to_result(sync_state.as_ref().shared_heads.as_slice())
pub unsafe extern "C" fn AMsyncStateSharedHeads(sync_state: *const AMsyncState) -> AMchangeHashes {
if let Some(sync_state) = sync_state.as_ref() {
AMchangeHashes::new(&sync_state.as_ref().shared_heads)
} else {
AMchangeHashes::default()
}
}
/// \memberof AMsyncState
/// \brief Gets the heads that were last sent by this peer.
///
/// \param[in] sync_state A pointer to an `AMsyncState` struct.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p sync_state `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_state `!= NULL`.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an AMsyncState
#[no_mangle]
pub unsafe extern "C" fn AMsyncStateLastSentHeads(sync_state: *const AMsyncState) -> *mut AMresult {
let sync_state = to_sync_state!(sync_state);
to_result(sync_state.as_ref().last_sent_heads.as_slice())
pub unsafe extern "C" fn AMsyncStateLastSentHeads(
sync_state: *const AMsyncState,
) -> AMchangeHashes {
if let Some(sync_state) = sync_state.as_ref() {
AMchangeHashes::new(&sync_state.as_ref().last_sent_heads)
} else {
AMchangeHashes::default()
}
}
/// \memberof AMsyncState
@ -171,13 +176,11 @@ pub unsafe extern "C" fn AMsyncStateLastSentHeads(sync_state: *const AMsyncState
///
/// \param[in] sync_state A pointer to an `AMsyncState` struct.
/// \param[out] has_value A pointer to a boolean flag that is set to `true` if
/// the returned `AMitems` struct is relevant, `false` otherwise.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_SYNC_HAVE` items.
/// \pre \p sync_state `!= NULL`
/// \pre \p has_value `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
//// \internal
/// the returned `AMhaves` struct is relevant, `false` otherwise.
/// \return An `AMhaves` struct.
/// \pre \p sync_state `!= NULL`.
/// \pre \p has_value `!= NULL`.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an AMsyncState
@ -186,15 +189,15 @@ pub unsafe extern "C" fn AMsyncStateLastSentHeads(sync_state: *const AMsyncState
pub unsafe extern "C" fn AMsyncStateTheirHaves(
sync_state: *const AMsyncState,
has_value: *mut bool,
) -> *mut AMresult {
) -> AMsyncHaves {
if let Some(sync_state) = sync_state.as_ref() {
if let Some(haves) = &sync_state.as_ref().their_have {
*has_value = true;
return to_result(haves.as_slice());
}
return AMsyncHaves::new(haves, &mut sync_state.their_haves_storage.borrow_mut());
};
};
*has_value = false;
to_result(Vec::<am::sync::Have>::new())
AMsyncHaves::default()
}
/// \memberof AMsyncState
@ -202,31 +205,29 @@ pub unsafe extern "C" fn AMsyncStateTheirHaves(
///
/// \param[in] sync_state A pointer to an `AMsyncState` struct.
/// \param[out] has_value A pointer to a boolean flag that is set to `true` if
/// the returned `AMitems` struct is relevant, `false`
/// otherwise.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p sync_state `!= NULL`
/// \pre \p has_value `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// the returned `AMchangeHashes` struct is relevant, `false`
/// otherwise.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_state `!= NULL`.
/// \pre \p has_value `!= NULL`.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an AMsyncState
/// has_value must be a valid pointer to a bool
/// has_value must be a valid pointer to a bool.
#[no_mangle]
pub unsafe extern "C" fn AMsyncStateTheirHeads(
sync_state: *const AMsyncState,
has_value: *mut bool,
) -> *mut AMresult {
) -> AMchangeHashes {
if let Some(sync_state) = sync_state.as_ref() {
if let Some(change_hashes) = &sync_state.as_ref().their_heads {
*has_value = true;
return to_result(change_hashes.as_slice());
return AMchangeHashes::new(change_hashes);
}
};
*has_value = false;
to_result(Vec::<am::ChangeHash>::new())
AMchangeHashes::default()
}
/// \memberof AMsyncState
@ -234,29 +235,27 @@ pub unsafe extern "C" fn AMsyncStateTheirHeads(
///
/// \param[in] sync_state A pointer to an `AMsyncState` struct.
/// \param[out] has_value A pointer to a boolean flag that is set to `true` if
/// the returned `AMitems` struct is relevant, `false`
/// otherwise.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p sync_state `!= NULL`
/// \pre \p has_value `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// the returned `AMchangeHashes` struct is relevant, `false`
/// otherwise.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_state `!= NULL`.
/// \pre \p has_value `!= NULL`.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an AMsyncState
/// has_value must be a valid pointer to a bool
/// has_value must be a valid pointer to a bool.
#[no_mangle]
pub unsafe extern "C" fn AMsyncStateTheirNeeds(
sync_state: *const AMsyncState,
has_value: *mut bool,
) -> *mut AMresult {
) -> AMchangeHashes {
if let Some(sync_state) = sync_state.as_ref() {
if let Some(change_hashes) = &sync_state.as_ref().their_need {
*has_value = true;
return to_result(change_hashes.as_slice());
return AMchangeHashes::new(change_hashes);
}
};
*has_value = false;
to_result(Vec::<am::ChangeHash>::new())
AMchangeHashes::default()
}

View file

@ -0,0 +1,57 @@
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
find_package(cmocka REQUIRED)
add_executable(
test_${LIBRARY_NAME}
actor_id_tests.c
doc_tests.c
group_state.c
list_tests.c
macro_utils.c
main.c
map_tests.c
stack_utils.c
str_utils.c
ported_wasm/basic_tests.c
ported_wasm/suite.c
ported_wasm/sync_tests.c
)
set_target_properties(test_${LIBRARY_NAME} PROPERTIES LINKER_LANGUAGE C)
# \note An imported library's INTERFACE_INCLUDE_DIRECTORIES property can't
# contain a non-existent path so its build-time include directory
# must be specified for all of its dependent targets instead.
target_include_directories(
test_${LIBRARY_NAME}
PRIVATE "$<BUILD_INTERFACE:${CBINDGEN_INCLUDEDIR}>"
)
target_link_libraries(test_${LIBRARY_NAME} PRIVATE cmocka ${LIBRARY_NAME})
add_dependencies(test_${LIBRARY_NAME} ${LIBRARY_NAME}_artifacts)
if(BUILD_SHARED_LIBS AND WIN32)
add_custom_command(
TARGET test_${LIBRARY_NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_SHARED_LIBRARY_SUFFIX}
${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Copying the DLL built by Cargo into the test directory..."
VERBATIM
)
endif()
add_test(NAME test_${LIBRARY_NAME} COMMAND test_${LIBRARY_NAME})
add_custom_command(
TARGET test_${LIBRARY_NAME}
POST_BUILD
COMMAND
${CMAKE_CTEST_COMMAND} --config $<CONFIG> --output-on-failure
COMMENT
"Running the test(s)..."
VERBATIM
)

View file

@ -0,0 +1,105 @@
#include <math.h>
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
/* third-party */
#include <cmocka.h>
/* local */
#include <automerge-c/automerge.h>
#include "str_utils.h"
typedef struct {
uint8_t* src;
char const* str;
size_t count;
} GroupState;
static int group_setup(void** state) {
GroupState* group_state = test_calloc(1, sizeof(GroupState));
group_state->str = "000102030405060708090a0b0c0d0e0f";
group_state->count = strlen(group_state->str) / 2;
group_state->src = test_malloc(group_state->count);
hex_to_bytes(group_state->str, group_state->src, group_state->count);
*state = group_state;
return 0;
}
static int group_teardown(void** state) {
GroupState* group_state = *state;
test_free(group_state->src);
test_free(group_state);
return 0;
}
static void test_AMactorIdInit() {
AMresult* prior_result = NULL;
AMbyteSpan prior_bytes;
char const* prior_str = NULL;
AMresult* result = NULL;
for (size_t i = 0; i != 11; ++i) {
result = AMactorIdInit();
if (AMresultStatus(result) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(result));
}
assert_int_equal(AMresultSize(result), 1);
AMvalue const value = AMresultValue(result);
assert_int_equal(value.tag, AM_VALUE_ACTOR_ID);
AMbyteSpan const bytes = AMactorIdBytes(value.actor_id);
char const* const str = AMactorIdStr(value.actor_id);
if (prior_result) {
size_t const min_count = fmax(bytes.count, prior_bytes.count);
assert_memory_not_equal(bytes.src, prior_bytes.src, min_count);
assert_string_not_equal(str, prior_str);
AMfree(prior_result);
}
prior_result = result;
prior_bytes = bytes;
prior_str = str;
}
AMfree(result);
}
static void test_AMactorIdInitBytes(void **state) {
GroupState* group_state = *state;
AMresult* const result = AMactorIdInitBytes(group_state->src, group_state->count);
if (AMresultStatus(result) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(result));
}
assert_int_equal(AMresultSize(result), 1);
AMvalue const value = AMresultValue(result);
assert_int_equal(value.tag, AM_VALUE_ACTOR_ID);
AMbyteSpan const bytes = AMactorIdBytes(value.actor_id);
assert_int_equal(bytes.count, group_state->count);
assert_memory_equal(bytes.src, group_state->src, bytes.count);
AMfree(result);
}
static void test_AMactorIdInitStr(void **state) {
GroupState* group_state = *state;
AMresult* const result = AMactorIdInitStr(group_state->str);
if (AMresultStatus(result) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(result));
}
assert_int_equal(AMresultSize(result), 1);
AMvalue const value = AMresultValue(result);
assert_int_equal(value.tag, AM_VALUE_ACTOR_ID);
char const* const str = AMactorIdStr(value.actor_id);
assert_int_equal(strlen(str), group_state->count * 2);
assert_string_equal(str, group_state->str);
AMfree(result);
}
int run_actor_id_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_AMactorIdInit),
cmocka_unit_test(test_AMactorIdInitBytes),
cmocka_unit_test(test_AMactorIdInitStr),
};
return cmocka_run_group_tests(tests, group_setup, group_teardown);
}

View file

@ -0,0 +1,202 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
/* third-party */
#include <cmocka.h>
/* local */
#include <automerge-c/automerge.h>
#include "group_state.h"
#include "stack_utils.h"
#include "str_utils.h"
typedef struct {
GroupState* group_state;
char const* actor_id_str;
uint8_t* actor_id_bytes;
size_t actor_id_size;
} TestState;
static int setup(void** state) {
TestState* test_state = test_calloc(1, sizeof(TestState));
group_setup((void**)&test_state->group_state);
test_state->actor_id_str = "000102030405060708090a0b0c0d0e0f";
test_state->actor_id_size = strlen(test_state->actor_id_str) / 2;
test_state->actor_id_bytes = test_malloc(test_state->actor_id_size);
hex_to_bytes(test_state->actor_id_str, test_state->actor_id_bytes, test_state->actor_id_size);
*state = test_state;
return 0;
}
static int teardown(void** state) {
TestState* test_state = *state;
group_teardown((void**)&test_state->group_state);
test_free(test_state->actor_id_bytes);
test_free(test_state);
return 0;
}
static void test_AMkeys_empty() {
AMresultStack* stack = NULL;
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMstrs forward = AMpush(&stack,
AMkeys(doc, AM_ROOT, NULL),
AM_VALUE_STRS,
cmocka_cb).strs;
assert_int_equal(AMstrsSize(&forward), 0);
AMstrs reverse = AMstrsReversed(&forward);
assert_int_equal(AMstrsSize(&reverse), 0);
assert_null(AMstrsNext(&forward, 1));
assert_null(AMstrsPrev(&forward, 1));
assert_null(AMstrsNext(&reverse, 1));
assert_null(AMstrsPrev(&reverse, 1));
AMfreeStack(&stack);
}
static void test_AMkeys_list() {
AMresultStack* stack = NULL;
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMfree(AMlistPutInt(doc, AM_ROOT, 0, true, 1));
AMfree(AMlistPutInt(doc, AM_ROOT, 1, true, 2));
AMfree(AMlistPutInt(doc, AM_ROOT, 2, true, 3));
AMstrs forward = AMpush(&stack,
AMkeys(doc, AM_ROOT, NULL),
AM_VALUE_STRS,
cmocka_cb).strs;
assert_int_equal(AMstrsSize(&forward), 3);
AMstrs reverse = AMstrsReversed(&forward);
assert_int_equal(AMstrsSize(&reverse), 3);
/* Forward iterator forward. */
char const* str = AMstrsNext(&forward, 1);
assert_ptr_equal(strstr(str, "1@"), str);
str = AMstrsNext(&forward, 1);
assert_ptr_equal(strstr(str, "2@"), str);
str = AMstrsNext(&forward, 1);
assert_ptr_equal(strstr(str, "3@"), str);
assert_null(AMstrsNext(&forward, 1));
/* Forward iterator reverse. */
str = AMstrsPrev(&forward, 1);
assert_ptr_equal(strstr(str, "3@"), str);
str = AMstrsPrev(&forward, 1);
assert_ptr_equal(strstr(str, "2@"), str);
str = AMstrsPrev(&forward, 1);
assert_ptr_equal(strstr(str, "1@"), str);
assert_null(AMstrsPrev(&forward, 1));
/* Reverse iterator forward. */
str = AMstrsNext(&reverse, 1);
assert_ptr_equal(strstr(str, "3@"), str);
str = AMstrsNext(&reverse, 1);
assert_ptr_equal(strstr(str, "2@"), str);
str = AMstrsNext(&reverse, 1);
assert_ptr_equal(strstr(str, "1@"), str);
/* Reverse iterator reverse. */
assert_null(AMstrsNext(&reverse, 1));
str = AMstrsPrev(&reverse, 1);
assert_ptr_equal(strstr(str, "1@"), str);
str = AMstrsPrev(&reverse, 1);
assert_ptr_equal(strstr(str, "2@"), str);
str = AMstrsPrev(&reverse, 1);
assert_ptr_equal(strstr(str, "3@"), str);
assert_null(AMstrsPrev(&reverse, 1));
AMfreeStack(&stack);
}
static void test_AMkeys_map() {
AMresultStack* stack = NULL;
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMfree(AMmapPutInt(doc, AM_ROOT, "one", 1));
AMfree(AMmapPutInt(doc, AM_ROOT, "two", 2));
AMfree(AMmapPutInt(doc, AM_ROOT, "three", 3));
AMstrs forward = AMpush(&stack,
AMkeys(doc, AM_ROOT, NULL),
AM_VALUE_STRS,
cmocka_cb).strs;
assert_int_equal(AMstrsSize(&forward), 3);
AMstrs reverse = AMstrsReversed(&forward);
assert_int_equal(AMstrsSize(&reverse), 3);
/* Forward iterator forward. */
assert_string_equal(AMstrsNext(&forward, 1), "one");
assert_string_equal(AMstrsNext(&forward, 1), "three");
assert_string_equal(AMstrsNext(&forward, 1), "two");
assert_null(AMstrsNext(&forward, 1));
/* Forward iterator reverse. */
assert_string_equal(AMstrsPrev(&forward, 1), "two");
assert_string_equal(AMstrsPrev(&forward, 1), "three");
assert_string_equal(AMstrsPrev(&forward, 1), "one");
assert_null(AMstrsPrev(&forward, 1));
/* Reverse iterator forward. */
assert_string_equal(AMstrsNext(&reverse, 1), "two");
assert_string_equal(AMstrsNext(&reverse, 1), "three");
assert_string_equal(AMstrsNext(&reverse, 1), "one");
assert_null(AMstrsNext(&reverse, 1));
/* Reverse iterator reverse. */
assert_string_equal(AMstrsPrev(&reverse, 1), "one");
assert_string_equal(AMstrsPrev(&reverse, 1), "three");
assert_string_equal(AMstrsPrev(&reverse, 1), "two");
assert_null(AMstrsPrev(&reverse, 1));
AMfreeStack(&stack);
}
static void test_AMputActor_bytes(void **state) {
TestState* test_state = *state;
AMactorId const* actor_id = AMpush(&test_state->group_state->stack,
AMactorIdInitBytes(
test_state->actor_id_bytes,
test_state->actor_id_size),
AM_VALUE_ACTOR_ID,
cmocka_cb).actor_id;
AMfree(AMsetActorId(test_state->group_state->doc, actor_id));
actor_id = AMpush(&test_state->group_state->stack,
AMgetActorId(test_state->group_state->doc),
AM_VALUE_ACTOR_ID,
cmocka_cb).actor_id;
AMbyteSpan const bytes = AMactorIdBytes(actor_id);
assert_int_equal(bytes.count, test_state->actor_id_size);
assert_memory_equal(bytes.src, test_state->actor_id_bytes, bytes.count);
}
static void test_AMputActor_str(void **state) {
TestState* test_state = *state;
AMactorId const* actor_id = AMpush(&test_state->group_state->stack,
AMactorIdInitStr(test_state->actor_id_str),
AM_VALUE_ACTOR_ID,
cmocka_cb).actor_id;
AMfree(AMsetActorId(test_state->group_state->doc, actor_id));
actor_id = AMpush(&test_state->group_state->stack,
AMgetActorId(test_state->group_state->doc),
AM_VALUE_ACTOR_ID,
cmocka_cb).actor_id;
char const* const str = AMactorIdStr(actor_id);
assert_int_equal(strlen(str), test_state->actor_id_size * 2);
assert_string_equal(str, test_state->actor_id_str);
}
static void test_AMspliceText() {
AMresultStack* stack = NULL;
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMfree(AMspliceText(doc, AM_ROOT, 0, 0, "one + "));
AMfree(AMspliceText(doc, AM_ROOT, 4, 2, "two = "));
AMfree(AMspliceText(doc, AM_ROOT, 8, 2, "three"));
char const* const text = AMpush(&stack,
AMtext(doc, AM_ROOT, NULL),
AM_VALUE_STR,
cmocka_cb).str;
assert_string_equal(text, "one two three");
AMfreeStack(&stack);
}
int run_doc_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_AMkeys_empty),
cmocka_unit_test(test_AMkeys_list),
cmocka_unit_test(test_AMkeys_map),
cmocka_unit_test_setup_teardown(test_AMputActor_bytes, setup, teardown),
cmocka_unit_test_setup_teardown(test_AMputActor_str, setup, teardown),
cmocka_unit_test(test_AMspliceText),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}

View file

@ -0,0 +1,27 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stdlib.h>
/* third-party */
#include <cmocka.h>
/* local */
#include "group_state.h"
#include "stack_utils.h"
int group_setup(void** state) {
GroupState* group_state = test_calloc(1, sizeof(GroupState));
group_state->doc = AMpush(&group_state->stack,
AMcreate(NULL),
AM_VALUE_DOC,
cmocka_cb).doc;
*state = group_state;
return 0;
}
int group_teardown(void** state) {
GroupState* group_state = *state;
AMfreeStack(&group_state->stack);
test_free(group_state);
return 0;
}

View file

@ -0,0 +1,16 @@
#ifndef GROUP_STATE_H
#define GROUP_STATE_H
/* local */
#include <automerge-c/automerge.h>
typedef struct {
AMresultStack* stack;
AMdoc* doc;
} GroupState;
int group_setup(void** state);
int group_teardown(void** state);
#endif /* GROUP_STATE_H */

View file

@ -0,0 +1,379 @@
#include <float.h>
#include <limits.h>
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
/* third-party */
#include <cmocka.h>
/* local */
#include <automerge-c/automerge.h>
#include "group_state.h"
#include "macro_utils.h"
#include "stack_utils.h"
static void test_AMlistIncrement(void** state) {
GroupState* group_state = *state;
AMfree(AMlistPutCounter(group_state->doc, AM_ROOT, 0, true, 0));
assert_int_equal(AMpush(&group_state->stack,
AMlistGet(group_state->doc, AM_ROOT, 0, NULL),
AM_VALUE_COUNTER,
cmocka_cb).counter, 0);
AMfree(AMpop(&group_state->stack));
AMfree(AMlistIncrement(group_state->doc, AM_ROOT, 0, 3));
assert_int_equal(AMpush(&group_state->stack,
AMlistGet(group_state->doc, AM_ROOT, 0, NULL),
AM_VALUE_COUNTER,
cmocka_cb).counter, 3);
AMfree(AMpop(&group_state->stack));
}
#define test_AMlistPut(suffix, mode) test_AMlistPut ## suffix ## _ ## mode
#define static_void_test_AMlistPut(suffix, mode, member, scalar_value) \
static void test_AMlistPut ## suffix ## _ ## mode(void **state) { \
GroupState* group_state = *state; \
AMfree(AMlistPut ## suffix(group_state->doc, \
AM_ROOT, \
0, \
!strcmp(#mode, "insert"), \
scalar_value)); \
assert_true(AMpush( \
&group_state->stack, \
AMlistGet(group_state->doc, AM_ROOT, 0, NULL), \
AMvalue_discriminant(#suffix), \
cmocka_cb).member == scalar_value); \
AMfree(AMpop(&group_state->stack)); \
}
#define test_AMlistPutBytes(mode) test_AMlistPutBytes ## _ ## mode
#define static_void_test_AMlistPutBytes(mode, bytes_value) \
static void test_AMlistPutBytes_ ## mode(void **state) { \
static size_t const BYTES_SIZE = sizeof(bytes_value) / sizeof(uint8_t); \
\
GroupState* group_state = *state; \
AMfree(AMlistPutBytes(group_state->doc, \
AM_ROOT, \
0, \
!strcmp(#mode, "insert"), \
bytes_value, \
BYTES_SIZE)); \
AMbyteSpan const bytes = AMpush( \
&group_state->stack, \
AMlistGet(group_state->doc, AM_ROOT, 0, NULL), \
AM_VALUE_BYTES, \
cmocka_cb).bytes; \
assert_int_equal(bytes.count, BYTES_SIZE); \
assert_memory_equal(bytes.src, bytes_value, BYTES_SIZE); \
AMfree(AMpop(&group_state->stack)); \
}
#define test_AMlistPutNull(mode) test_AMlistPutNull_ ## mode
#define static_void_test_AMlistPutNull(mode) \
static void test_AMlistPutNull_ ## mode(void **state) { \
GroupState* group_state = *state; \
AMfree(AMlistPutNull(group_state->doc, \
AM_ROOT, \
0, \
!strcmp(#mode, "insert"))); \
AMresult* const result = AMlistGet(group_state->doc, AM_ROOT, 0, NULL); \
if (AMresultStatus(result) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(result)); \
} \
assert_int_equal(AMresultSize(result), 1); \
assert_int_equal(AMresultValue(result).tag, AM_VALUE_NULL); \
AMfree(result); \
}
#define test_AMlistPutObject(label, mode) test_AMlistPutObject_ ## label ## _ ## mode
#define static_void_test_AMlistPutObject(label, mode) \
static void test_AMlistPutObject_ ## label ## _ ## mode(void **state) { \
GroupState* group_state = *state; \
AMobjId const* const obj_id = AMpush( \
&group_state->stack, \
AMlistPutObject(group_state->doc, \
AM_ROOT, \
0, \
!strcmp(#mode, "insert"), \
AMobjType_tag(#label)), \
AM_VALUE_OBJ_ID, \
cmocka_cb).obj_id; \
assert_non_null(obj_id); \
assert_int_equal(AMobjSize(group_state->doc, obj_id, NULL), 0); \
AMfree(AMpop(&group_state->stack)); \
}
#define test_AMlistPutStr(mode) test_AMlistPutStr ## _ ## mode
#define static_void_test_AMlistPutStr(mode, str_value) \
static void test_AMlistPutStr_ ## mode(void **state) { \
GroupState* group_state = *state; \
AMfree(AMlistPutStr(group_state->doc, \
AM_ROOT, \
0, \
!strcmp(#mode, "insert"), \
str_value)); \
assert_string_equal(AMpush( \
&group_state->stack, \
AMlistGet(group_state->doc, AM_ROOT, 0, NULL), \
AM_VALUE_STR, \
cmocka_cb).str, str_value); \
AMfree(AMpop(&group_state->stack)); \
}
static_void_test_AMlistPut(Bool, insert, boolean, true)
static_void_test_AMlistPut(Bool, update, boolean, true)
static uint8_t const BYTES_VALUE[] = {INT8_MIN, INT8_MAX / 2, INT8_MAX};
static_void_test_AMlistPutBytes(insert, BYTES_VALUE)
static_void_test_AMlistPutBytes(update, BYTES_VALUE)
static_void_test_AMlistPut(Counter, insert, counter, INT64_MAX)
static_void_test_AMlistPut(Counter, update, counter, INT64_MAX)
static_void_test_AMlistPut(F64, insert, f64, DBL_MAX)
static_void_test_AMlistPut(F64, update, f64, DBL_MAX)
static_void_test_AMlistPut(Int, insert, int_, INT64_MAX)
static_void_test_AMlistPut(Int, update, int_, INT64_MAX)
static_void_test_AMlistPutNull(insert)
static_void_test_AMlistPutNull(update)
static_void_test_AMlistPutObject(List, insert)
static_void_test_AMlistPutObject(List, update)
static_void_test_AMlistPutObject(Map, insert)
static_void_test_AMlistPutObject(Map, update)
static_void_test_AMlistPutObject(Text, insert)
static_void_test_AMlistPutObject(Text, update)
static_void_test_AMlistPutStr(insert, "Hello, world!")
static_void_test_AMlistPutStr(update, "Hello, world!")
static_void_test_AMlistPut(Timestamp, insert, timestamp, INT64_MAX)
static_void_test_AMlistPut(Timestamp, update, timestamp, INT64_MAX)
static_void_test_AMlistPut(Uint, insert, uint, UINT64_MAX)
static_void_test_AMlistPut(Uint, update, uint, UINT64_MAX)
static void test_insert_at_index(void** state) {
AMresultStack* stack = *state;
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMobjId const* const list = AMpush(
&stack,
AMlistPutObject(doc, AM_ROOT, 0, true, AM_OBJ_TYPE_LIST),
AM_VALUE_OBJ_ID,
cmocka_cb).obj_id;
/* Insert both at the same index. */
AMfree(AMlistPutUint(doc, list, 0, true, 0));
AMfree(AMlistPutUint(doc, list, 0, true, 1));
assert_int_equal(AMobjSize(doc, list, NULL), 2);
AMstrs const keys = AMpush(&stack,
AMkeys(doc, list, NULL),
AM_VALUE_STRS,
cmocka_cb).strs;
assert_int_equal(AMstrsSize(&keys), 2);
AMlistItems const range = AMpush(&stack,
AMlistRange(doc, list, 0, SIZE_MAX, NULL),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
assert_int_equal(AMlistItemsSize(&range), 2);
}
static void test_get_list_values(void** state) {
AMresultStack* stack = *state;
AMdoc* const doc1 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMobjId const* const list = AMpush(
&stack,
AMmapPutObject(doc1, AM_ROOT, "list", AM_OBJ_TYPE_LIST),
AM_VALUE_OBJ_ID,
cmocka_cb).obj_id;
/* Insert elements. */
AMfree(AMlistPutStr(doc1, list, 0, true, "First"));
AMfree(AMlistPutStr(doc1, list, 0, true, "Second"));
AMfree(AMlistPutStr(doc1, list, 0, true, "Third"));
AMfree(AMlistPutStr(doc1, list, 0, true, "Fourth"));
AMfree(AMlistPutStr(doc1, list, 0, true, "Fifth"));
AMfree(AMlistPutStr(doc1, list, 0, true, "Sixth"));
AMfree(AMlistPutStr(doc1, list, 0, true, "Seventh"));
AMfree(AMlistPutStr(doc1, list, 0, true, "Eighth"));
AMfree(AMcommit(doc1, NULL, NULL));
AMchangeHashes const v1 = AMpush(&stack,
AMgetHeads(doc1),
AM_VALUE_CHANGE_HASHES,
cmocka_cb).change_hashes;
AMdoc* const doc2 = AMpush(&stack,
AMfork(doc1, NULL),
AM_VALUE_DOC,
cmocka_cb).doc;
AMfree(AMlistPutStr(doc1, list, 2, false, "Third V2"));
AMfree(AMcommit(doc1, NULL, NULL));
AMfree(AMlistPutStr(doc2, list, 2, false, "Third V3"));
AMfree(AMcommit(doc2, NULL, NULL));
AMfree(AMmerge(doc1, doc2));
AMlistItems range = AMpush(&stack,
AMlistRange(doc1, list, 0, SIZE_MAX, NULL),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
assert_int_equal(AMlistItemsSize(&range), 8);
AMlistItem const* list_item = NULL;
while ((list_item = AMlistItemsNext(&range, 1)) != NULL) {
AMvalue const val1 = AMlistItemValue(list_item);
AMresult* result = AMlistGet(doc1, list, AMlistItemIndex(list_item), NULL);
AMvalue const val2 = AMresultValue(result);
assert_true(AMvalueEqual(&val1, &val2));
assert_non_null(AMlistItemObjId(list_item));
AMfree(result);
}
range = AMpush(&stack,
AMlistRange(doc1, list, 3, 6, NULL),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
AMlistItems range_back = AMlistItemsReversed(&range);
assert_int_equal(AMlistItemsSize(&range), 3);
assert_int_equal(AMlistItemIndex(AMlistItemsNext(&range, 1)), 3);
assert_int_equal(AMlistItemIndex(AMlistItemsNext(&range_back, 1)), 5);
range = AMlistItemsRewound(&range);
while ((list_item = AMlistItemsNext(&range, 1)) != NULL) {
AMvalue const val1 = AMlistItemValue(list_item);
AMresult* result = AMlistGet(doc1, list, AMlistItemIndex(list_item), NULL);
AMvalue const val2 = AMresultValue(result);
assert_true(AMvalueEqual(&val1, &val2));
assert_non_null(AMlistItemObjId(list_item));
AMfree(result);
}
range = AMpush(&stack,
AMlistRange(doc1, list, 0, SIZE_MAX, &v1),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
assert_int_equal(AMlistItemsSize(&range), 8);
while ((list_item = AMlistItemsNext(&range, 1)) != NULL) {
AMvalue const val1 = AMlistItemValue(list_item);
AMresult* result = AMlistGet(doc1, list, AMlistItemIndex(list_item), &v1);
AMvalue const val2 = AMresultValue(result);
assert_true(AMvalueEqual(&val1, &val2));
assert_non_null(AMlistItemObjId(list_item));
AMfree(result);
}
range = AMpush(&stack,
AMlistRange(doc1, list, 3, 6, &v1),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
range_back = AMlistItemsReversed(&range);
assert_int_equal(AMlistItemsSize(&range), 3);
assert_int_equal(AMlistItemIndex(AMlistItemsNext(&range, 1)), 3);
assert_int_equal(AMlistItemIndex(AMlistItemsNext(&range_back, 1)), 5);
range = AMlistItemsRewound(&range);
while ((list_item = AMlistItemsNext(&range, 1)) != NULL) {
AMvalue const val1 = AMlistItemValue(list_item);
AMresult* result = AMlistGet(doc1, list, AMlistItemIndex(list_item), &v1);
AMvalue const val2 = AMresultValue(result);
assert_true(AMvalueEqual(&val1, &val2));
assert_non_null(AMlistItemObjId(list_item));
AMfree(result);
}
range = AMpush(&stack,
AMlistRange(doc1, list, 0, SIZE_MAX, NULL),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
AMobjItems values = AMpush(&stack,
AMobjValues(doc1, list, NULL),
AM_VALUE_OBJ_ITEMS,
cmocka_cb).obj_items;
assert_int_equal(AMlistItemsSize(&range), AMobjItemsSize(&values));
AMobjItem const* value = NULL;
while ((list_item = AMlistItemsNext(&range, 1)) != NULL &&
(value = AMobjItemsNext(&values, 1)) != NULL) {
AMvalue const val1 = AMlistItemValue(list_item);
AMvalue const val2 = AMobjItemValue(value);
assert_true(AMvalueEqual(&val1, &val2));
assert_true(AMobjIdEqual(AMlistItemObjId(list_item), AMobjItemObjId(value)));
}
range = AMpush(&stack,
AMlistRange(doc1, list, 0, SIZE_MAX, &v1),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
values = AMpush(&stack,
AMobjValues(doc1, list, &v1),
AM_VALUE_OBJ_ITEMS,
cmocka_cb).obj_items;
assert_int_equal(AMlistItemsSize(&range), AMobjItemsSize(&values));
while ((list_item = AMlistItemsNext(&range, 1)) != NULL &&
(value = AMobjItemsNext(&values, 1)) != NULL) {
AMvalue const val1 = AMlistItemValue(list_item);
AMvalue const val2 = AMobjItemValue(value);
assert_true(AMvalueEqual(&val1, &val2));
assert_true(AMobjIdEqual(AMlistItemObjId(list_item), AMobjItemObjId(value)));
}
}
int run_list_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_AMlistIncrement),
cmocka_unit_test(test_AMlistPut(Bool, insert)),
cmocka_unit_test(test_AMlistPut(Bool, update)),
cmocka_unit_test(test_AMlistPutBytes(insert)),
cmocka_unit_test(test_AMlistPutBytes(update)),
cmocka_unit_test(test_AMlistPut(Counter, insert)),
cmocka_unit_test(test_AMlistPut(Counter, update)),
cmocka_unit_test(test_AMlistPut(F64, insert)),
cmocka_unit_test(test_AMlistPut(F64, update)),
cmocka_unit_test(test_AMlistPut(Int, insert)),
cmocka_unit_test(test_AMlistPut(Int, update)),
cmocka_unit_test(test_AMlistPutNull(insert)),
cmocka_unit_test(test_AMlistPutNull(update)),
cmocka_unit_test(test_AMlistPutObject(List, insert)),
cmocka_unit_test(test_AMlistPutObject(List, update)),
cmocka_unit_test(test_AMlistPutObject(Map, insert)),
cmocka_unit_test(test_AMlistPutObject(Map, update)),
cmocka_unit_test(test_AMlistPutObject(Text, insert)),
cmocka_unit_test(test_AMlistPutObject(Text, update)),
cmocka_unit_test(test_AMlistPutStr(insert)),
cmocka_unit_test(test_AMlistPutStr(update)),
cmocka_unit_test(test_AMlistPut(Timestamp, insert)),
cmocka_unit_test(test_AMlistPut(Timestamp, update)),
cmocka_unit_test(test_AMlistPut(Uint, insert)),
cmocka_unit_test(test_AMlistPut(Uint, update)),
cmocka_unit_test_setup_teardown(test_insert_at_index, setup_stack, teardown_stack),
cmocka_unit_test_setup_teardown(test_get_list_values, setup_stack, teardown_stack),
};
return cmocka_run_group_tests(tests, group_setup, group_teardown);
}

View file

@ -0,0 +1,24 @@
#include <string.h>
/* local */
#include "macro_utils.h"
AMvalueVariant AMvalue_discriminant(char const* suffix) {
if (!strcmp(suffix, "Bool")) return AM_VALUE_BOOLEAN;
else if (!strcmp(suffix, "Bytes")) return AM_VALUE_BYTES;
else if (!strcmp(suffix, "Counter")) return AM_VALUE_COUNTER;
else if (!strcmp(suffix, "F64")) return AM_VALUE_F64;
else if (!strcmp(suffix, "Int")) return AM_VALUE_INT;
else if (!strcmp(suffix, "Null")) return AM_VALUE_NULL;
else if (!strcmp(suffix, "Str")) return AM_VALUE_STR;
else if (!strcmp(suffix, "Timestamp")) return AM_VALUE_TIMESTAMP;
else if (!strcmp(suffix, "Uint")) return AM_VALUE_UINT;
else return AM_VALUE_VOID;
}
AMobjType AMobjType_tag(char const* obj_type_label) {
if (!strcmp(obj_type_label, "List")) return AM_OBJ_TYPE_LIST;
else if (!strcmp(obj_type_label, "Map")) return AM_OBJ_TYPE_MAP;
else if (!strcmp(obj_type_label, "Text")) return AM_OBJ_TYPE_TEXT;
else return 0;
}

View file

@ -0,0 +1,24 @@
#ifndef MACRO_UTILS_H
#define MACRO_UTILS_H
/* local */
#include <automerge-c/automerge.h>
/**
* \brief Gets the result value discriminant corresponding to a function name
* suffix.
*
* \param[in] suffix A string.
* \return An `AMvalue` struct discriminant.
*/
AMvalueVariant AMvalue_discriminant(char const* suffix);
/**
* \brief Gets the object type tag corresponding to an object type label.
*
* \param[in] obj_type_label A string.
* \return An `AMobjType` enum tag.
*/
AMobjType AMobjType_tag(char const* obj_type_label);
#endif /* MACRO_UTILS_H */

View file

@ -1,6 +1,6 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <stdint.h>
/* third-party */
@ -8,14 +8,8 @@
extern int run_actor_id_tests(void);
extern int run_byte_span_tests(void);
extern int run_doc_tests(void);
extern int run_enum_string_tests(void);
extern int run_item_tests(void);
extern int run_list_tests(void);
extern int run_map_tests(void);
@ -23,6 +17,11 @@ extern int run_map_tests(void);
extern int run_ported_wasm_suite(void);
int main(void) {
return (run_actor_id_tests() + run_byte_span_tests() + run_doc_tests() + run_enum_string_tests() +
run_item_tests() + run_list_tests() + run_map_tests() + run_ported_wasm_suite());
return (
run_actor_id_tests() +
run_doc_tests() +
run_list_tests() +
run_map_tests() +
run_ported_wasm_suite()
);
}

1164
automerge-c/test/map_tests.c Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <stdint.h>
/* third-party */
@ -11,5 +11,8 @@ extern int run_ported_wasm_basic_tests(void);
extern int run_ported_wasm_sync_tests(void);
int run_ported_wasm_suite(void) {
return (run_ported_wasm_basic_tests() + run_ported_wasm_sync_tests());
return (
run_ported_wasm_basic_tests() +
run_ported_wasm_sync_tests()
);
}

View file

@ -0,0 +1,30 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
/* third-party */
#include <cmocka.h>
/* local */
#include "stack_utils.h"
void cmocka_cb(AMresultStack** stack, uint8_t discriminant) {
assert_non_null(stack);
assert_non_null(*stack);
assert_non_null((*stack)->result);
if (AMresultStatus((*stack)->result) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage((*stack)->result));
}
assert_int_equal(AMresultValue((*stack)->result).tag, discriminant);
}
int setup_stack(void** state) {
*state = NULL;
return 0;
}
int teardown_stack(void** state) {
AMresultStack* stack = *state;
AMfreeStack(&stack);
return 0;
}

View file

@ -0,0 +1,38 @@
#ifndef STACK_UTILS_H
#define STACK_UTILS_H
#include <stdint.h>
/* local */
#include <automerge-c/automerge.h>
/**
* \brief Reports an error through a cmocka assertion.
*
* \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
* \param[in] discriminant An `AMvalueVariant` enum tag.
* \pre \p stack` != NULL`.
*/
void cmocka_cb(AMresultStack** stack, uint8_t discriminant);
/**
* \brief Allocates a result stack for storing the results allocated during one
* or more test cases.
*
* \param[in,out] state A pointer to a pointer to an `AMresultStack` struct.
* \pre \p state` != NULL`.
* \warning The `AMresultStack` struct returned through \p state must be
* deallocated with `teardown_stack()` in order to prevent memory leaks.
*/
int setup_stack(void** state);
/**
* \brief Deallocates a result stack after deallocating any results that were
* stored in it by one or more test cases.
*
* \param[in] state A pointer to a pointer to an `AMresultStack` struct.
* \pre \p state` != NULL`.
*/
int teardown_stack(void** state);
#endif /* STACK_UTILS_H */

View file

@ -1,5 +1,5 @@
#include <stdint.h>
#include <stdio.h>
#include <stdint.h>
/* local */
#include "str_utils.h"

View file

@ -0,0 +1,14 @@
#ifndef STR_UTILS_H
#define STR_UTILS_H
/**
* \brief Converts a hexadecimal string into a sequence of bytes.
*
* \param[in] hex_str A string.
* \param[in] src A pointer to a contiguous sequence of bytes.
* \param[in] count The number of bytes to copy to \p src.
* \pre \p count `<=` length of \p src.
*/
void hex_to_bytes(char const* hex_str, uint8_t* src, size_t const count);
#endif /* STR_UTILS_H */

857
automerge-cli/Cargo.lock generated Normal file
View file

@ -0,0 +1,857 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi",
]
[[package]]
name = "anyhow"
version = "1.0.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd"
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "automerge"
version = "0.1.0"
dependencies = [
"flate2",
"fxhash",
"hex",
"itertools",
"js-sys",
"leb128",
"nonzero_ext",
"rand",
"serde",
"sha2",
"smol_str",
"thiserror",
"tinyvec",
"tracing",
"unicode-segmentation",
"uuid",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "automerge-cli"
version = "0.1.0"
dependencies = [
"anyhow",
"atty",
"automerge",
"clap",
"colored_json",
"combine",
"duct",
"maplit",
"serde_json",
"thiserror",
"tracing-subscriber",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "block-buffer"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "3.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced1892c55c910c1219e98d6fc8d71f6bddba7905866ce740066d8bfea859312"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"indexmap",
"lazy_static",
"os_str_bytes",
"strsim",
"termcolor",
"textwrap",
]
[[package]]
name = "clap_derive"
version = "3.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da95d038ede1a964ce99f49cbe27a7fb538d1da595e4b4f70b8c8f338d17bf16"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "colored_json"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd32eb54d016e203b7c2600e3a7802c75843a92e38ccc4869aefeca21771a64"
dependencies = [
"ansi_term",
"atty",
"libc",
"serde",
"serde_json",
]
[[package]]
name = "combine"
version = "4.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50b727aacc797f9fc28e355d21f34709ac4fc9adecfe470ad07b8f4464f53062"
dependencies = [
"bytes",
"memchr",
]
[[package]]
name = "cpufeatures"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if",
]
[[package]]
name = "crypto-common"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "duct"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fc6a0a59ed0888e0041cf708e66357b7ae1a82f1c67247e1f93b5e0818f7d8d"
dependencies = [
"libc",
"once_cell",
"os_pipe",
"shared_child",
]
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "flate2"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f"
dependencies = [
"cfg-if",
"crc32fast",
"libc",
"miniz_oxide",
]
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder",
]
[[package]]
name = "generic-array"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "heck"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "indexmap"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "itertools"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
[[package]]
name = "js-sys"
version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "leb128"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
[[package]]
name = "libc"
version = "0.2.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4"
[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
[[package]]
name = "maplit"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "memchr"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "miniz_oxide"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
dependencies = [
"adler",
"autocfg",
]
[[package]]
name = "nonzero_ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44a1290799eababa63ea60af0cbc3f03363e328e58f32fb0294798ed3e85f444"
[[package]]
name = "once_cell"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
[[package]]
name = "os_pipe"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb233f06c2307e1f5ce2ecad9f8121cffbbee2c95428f44ea85222e460d0d213"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "os_str_bytes"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
dependencies = [
"memchr",
]
[[package]]
name = "pin-project-lite"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
[[package]]
name = "ppv-lite86"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
]
[[package]]
name = "ryu"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
[[package]]
name = "serde"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sha2"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
dependencies = [
"lazy_static",
]
[[package]]
name = "shared_child"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6be9f7d5565b1483af3e72975e2dee33879b3b86bd48c0929fccf6585d79e65a"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "smallvec"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
[[package]]
name = "smol_str"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61d15c83e300cce35b7c8cd39ff567c1ef42dde6d4a1a38dbdbf9a59902261bd"
dependencies = [
"serde",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "termcolor"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
[[package]]
name = "thiserror"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
dependencies = [
"once_cell",
]
[[package]]
name = "tinyvec"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tracing"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6c650a8ef0cd2dd93736f033d21cbd1224c5a967aa0c258d00fcf7dafef9b9f"
dependencies = [
"cfg-if",
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8276d9a4a3a558d7b7ad5303ad50b53d58264641b82914b7ada36bd762e7a716"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23"
dependencies = [
"lazy_static",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3"
dependencies = [
"lazy_static",
"log",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e0ab7bdc962035a87fba73f3acca9b8a8d0034c2e6f60b84aeaaddddc155dce"
dependencies = [
"ansi_term",
"sharded-slab",
"smallvec",
"thread_local",
"tracing-core",
"tracing-log",
]
[[package]]
name = "typenum"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "unicode-segmentation"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom",
"serde",
]
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "wasm-bindgen"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca"
dependencies = [
"bumpalo",
"lazy_static",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2"
[[package]]
name = "web-sys"
version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

View file

@ -13,18 +13,17 @@ bench = false
doc = false
[dependencies]
clap = {version = "~4", features = ["derive"]}
clap = {version = "~3.1", features = ["derive"]}
serde_json = "^1.0"
anyhow = "1.0"
atty = "^0.2"
thiserror = "^1.0"
combine = "^4.5"
maplit = "^1.0"
colored_json = "^2.1"
tracing-subscriber = "~0.3"
automerge = { path = "../automerge" }
is-terminal = "0.4.1"
termcolor = "1.1.3"
serde = "1.0.150"
[dev-dependencies]
duct = "^0.13"

View file

@ -1,8 +1,6 @@
use automerge as am;
use thiserror::Error;
use crate::{color_json::print_colored_json, SkipVerifyFlag};
#[derive(Error, Debug)]
pub enum ExamineError {
#[error("Error reading change file: {:?}", source)]
@ -22,18 +20,16 @@ pub enum ExamineError {
},
}
pub(crate) fn examine(
pub fn examine(
mut input: impl std::io::Read,
mut output: impl std::io::Write,
skip: SkipVerifyFlag,
is_tty: bool,
) -> Result<(), ExamineError> {
let mut buf: Vec<u8> = Vec::new();
input
.read_to_end(&mut buf)
.map_err(|e| ExamineError::ReadingChanges { source: e })?;
let doc = skip
.load(&buf)
let doc = am::Automerge::load(&buf)
.map_err(|e| ExamineError::ApplyingInitialChanges { source: e })?;
let uncompressed_changes: Vec<_> = doc
.get_changes(&[])
@ -43,7 +39,7 @@ pub(crate) fn examine(
.collect();
if is_tty {
let json_changes = serde_json::to_value(uncompressed_changes).unwrap();
print_colored_json(&json_changes).unwrap();
colored_json::write_colored_json(&json_changes, &mut output).unwrap();
writeln!(output).unwrap();
} else {
let json_changes = serde_json::to_string_pretty(&uncompressed_changes).unwrap();

View file

@ -1,8 +1,5 @@
use anyhow::Result;
use automerge as am;
use automerge::ReadDoc;
use crate::{color_json::print_colored_json, SkipVerifyFlag};
pub(crate) fn map_to_json(doc: &am::Automerge, obj: &am::ObjId) -> serde_json::Value {
let keys = doc.keys(obj);
@ -31,7 +28,7 @@ fn list_to_json(doc: &am::Automerge, obj: &am::ObjId) -> serde_json::Value {
let len = doc.length(obj);
let mut array = Vec::new();
for i in 0..len {
let val = doc.get(obj, i);
let val = doc.get(obj, i as usize);
match val {
Ok(Some((am::Value::Object(o), exid)))
if o == am::ObjType::Map || o == am::ObjType::Table =>
@ -72,23 +69,22 @@ fn scalar_to_json(val: &am::ScalarValue) -> serde_json::Value {
}
}
fn get_state_json(input_data: Vec<u8>, skip: SkipVerifyFlag) -> Result<serde_json::Value> {
let doc = skip.load(&input_data).unwrap(); // FIXME
fn get_state_json(input_data: Vec<u8>) -> Result<serde_json::Value> {
let doc = am::Automerge::load(&input_data).unwrap(); // FIXME
Ok(map_to_json(&doc, &am::ObjId::Root))
}
pub(crate) fn export_json(
pub fn export_json(
mut changes_reader: impl std::io::Read,
mut writer: impl std::io::Write,
skip: SkipVerifyFlag,
is_tty: bool,
) -> Result<()> {
let mut input_data = vec![];
changes_reader.read_to_end(&mut input_data)?;
let state_json = get_state_json(input_data, skip)?;
let state_json = get_state_json(input_data)?;
if is_tty {
print_colored_json(&state_json).unwrap();
colored_json::write_colored_json(&state_json, &mut writer).unwrap();
writeln!(writer).unwrap();
} else {
writeln!(
@ -107,10 +103,7 @@ mod tests {
#[test]
fn cli_export_with_empty_input() {
assert_eq!(
get_state_json(vec![], Default::default()).unwrap(),
serde_json::json!({})
)
assert_eq!(get_state_json(vec![]).unwrap(), serde_json::json!({}))
}
#[test]
@ -124,7 +117,7 @@ mod tests {
let mut backend = initialize_from_json(&initial_state_json).unwrap();
let change_bytes = backend.save();
assert_eq!(
get_state_json(change_bytes, Default::default()).unwrap(),
get_state_json(change_bytes).unwrap(),
serde_json::json!({"sparrows": 15.0})
)
}
@ -151,7 +144,7 @@ mod tests {
*/
let change_bytes = backend.save();
assert_eq!(
get_state_json(change_bytes, Default::default()).unwrap(),
get_state_json(change_bytes).unwrap(),
serde_json::json!({
"birds": {
"wrens": 3.0,

View file

@ -1,15 +1,10 @@
use std::{fs::File, path::PathBuf, str::FromStr};
use anyhow::{anyhow, Result};
use clap::{
builder::{BoolishValueParser, TypedValueParser, ValueParserFactory},
Parser,
};
use is_terminal::IsTerminal;
use clap::Parser;
mod color_json;
//mod change;
mod examine;
mod examine_sync;
mod export;
mod import;
mod merge;
@ -21,50 +16,12 @@ struct Opts {
cmd: Command,
}
#[derive(clap::ValueEnum, Clone, Debug)]
#[derive(Debug)]
enum ExportFormat {
Json,
Toml,
}
#[derive(Copy, Clone, Default, Debug)]
pub(crate) struct SkipVerifyFlag(bool);
impl SkipVerifyFlag {
fn load(&self, buf: &[u8]) -> Result<automerge::Automerge, automerge::AutomergeError> {
if self.0 {
automerge::Automerge::load(buf)
} else {
automerge::Automerge::load_unverified_heads(buf)
}
}
}
#[derive(Clone)]
struct SkipVerifyFlagParser;
impl ValueParserFactory for SkipVerifyFlag {
type Parser = SkipVerifyFlagParser;
fn value_parser() -> Self::Parser {
SkipVerifyFlagParser
}
}
impl TypedValueParser for SkipVerifyFlagParser {
type Value = SkipVerifyFlag;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
BoolishValueParser::new()
.parse_ref(cmd, arg, value)
.map(SkipVerifyFlag)
}
}
impl FromStr for ExportFormat {
type Err = anyhow::Error;
@ -86,15 +43,12 @@ enum Command {
format: ExportFormat,
/// Path that contains Automerge changes
#[clap(parse(from_os_str))]
changes_file: Option<PathBuf>,
/// The file to write to. If omitted assumes stdout
#[clap(long("out"), short('o'))]
#[clap(parse(from_os_str), long("out"), short('o'))]
output_file: Option<PathBuf>,
/// Whether to verify the head hashes of a compressed document
#[clap(long, action = clap::ArgAction::SetFalse)]
skip_verifying_heads: SkipVerifyFlag,
},
Import {
@ -102,37 +56,69 @@ enum Command {
#[clap(long, short, default_value = "json")]
format: ExportFormat,
#[clap(parse(from_os_str))]
input_file: Option<PathBuf>,
/// Path to write Automerge changes to
#[clap(long("out"), short('o'))]
#[clap(parse(from_os_str), long("out"), short('o'))]
changes_file: Option<PathBuf>,
},
/// Read an automerge document and print a JSON representation of the changes in it to stdout
Examine {
/// Read an automerge document from a file or stdin, perform a change on it and write a new
/// document to stdout or the specified output file.
Change {
/// The change script to perform. Change scripts have the form <command> <path> [<JSON value>].
/// The possible commands are 'set', 'insert', 'delete', and 'increment'.
///
/// Paths look like this: $["mapkey"][0]. They always lways start with a '$', then each
/// subsequent segment of the path is either a string in double quotes to index a key in a
/// map, or an integer index to address an array element.
///
/// Examples
///
/// ## set
///
/// > automerge change 'set $["someobject"] {"items": []}' somefile
///
/// ## insert
///
/// > automerge change 'insert $["someobject"]["items"][0] "item1"' somefile
///
/// ## increment
///
/// > automerge change 'increment $["mycounter"]'
///
/// ## delete
///
/// > automerge change 'delete $["someobject"]["items"]' somefile
script: String,
/// The file to change, if omitted will assume stdin
#[clap(parse(from_os_str))]
input_file: Option<PathBuf>,
skip_verifying_heads: SkipVerifyFlag,
/// Path to write Automerge changes to, if omitted will write to stdout
#[clap(parse(from_os_str), long("out"), short('o'))]
output_file: Option<PathBuf>,
},
/// Read an automerge sync messaage and print a JSON representation of it
ExamineSync { input_file: Option<PathBuf> },
/// Read an automerge document and print a JSON representation of the changes in it to stdout
Examine { input_file: Option<PathBuf> },
/// Read one or more automerge documents and output a merged, compacted version of them
Merge {
/// The file to write to. If omitted assumes stdout
#[clap(long("out"), short('o'))]
#[clap(parse(from_os_str), long("out"), short('o'))]
output_file: Option<PathBuf>,
/// The file(s) to compact. If empty assumes stdin
input: Vec<PathBuf>,
},
}
fn open_file_or_stdin(maybe_path: Option<PathBuf>) -> Result<Box<dyn std::io::Read>> {
if std::io::stdin().is_terminal() {
if atty::is(atty::Stream::Stdin) {
if let Some(path) = maybe_path {
Ok(Box::new(File::open(path).unwrap()))
Ok(Box::new(File::open(&path).unwrap()))
} else {
Err(anyhow!(
"Must provide file path if not providing input via stdin"
@ -144,9 +130,9 @@ fn open_file_or_stdin(maybe_path: Option<PathBuf>) -> Result<Box<dyn std::io::Re
}
fn create_file_or_stdout(maybe_path: Option<PathBuf>) -> Result<Box<dyn std::io::Write>> {
if std::io::stdout().is_terminal() {
if atty::is(atty::Stream::Stdout) {
if let Some(path) = maybe_path {
Ok(Box::new(File::create(path).unwrap()))
Ok(Box::new(File::create(&path).unwrap()))
} else {
Err(anyhow!("Must provide file path if not piping to stdout"))
}
@ -163,22 +149,16 @@ fn main() -> Result<()> {
changes_file,
format,
output_file,
skip_verifying_heads,
} => {
let output: Box<dyn std::io::Write> = if let Some(output_file) = output_file {
Box::new(File::create(output_file)?)
Box::new(File::create(&output_file)?)
} else {
Box::new(std::io::stdout())
};
match format {
ExportFormat::Json => {
let mut in_buffer = open_file_or_stdin(changes_file)?;
export::export_json(
&mut in_buffer,
output,
skip_verifying_heads,
std::io::stdout().is_terminal(),
)
export::export_json(&mut in_buffer, output, atty::is(atty::Stream::Stdout))
}
ExportFormat::Toml => unimplemented!(),
}
@ -195,30 +175,23 @@ fn main() -> Result<()> {
}
ExportFormat::Toml => unimplemented!(),
},
Command::Examine {
input_file,
skip_verifying_heads,
Command::Change { ..
//input_file,
//output_file,
//script,
} => {
unimplemented!()
/*
let in_buffer = open_file_or_stdin(input_file)?;
let out_buffer = std::io::stdout();
match examine::examine(
in_buffer,
out_buffer,
skip_verifying_heads,
std::io::stdout().is_terminal(),
) {
Ok(()) => {}
Err(e) => {
eprintln!("Error: {:?}", e);
}
}
Ok(())
let mut out_buffer = create_file_or_stdout(output_file)?;
change::change(in_buffer, &mut out_buffer, script.as_str())
.map_err(|e| anyhow::format_err!("Unable to make changes: {:?}", e))
*/
}
Command::ExamineSync { input_file } => {
Command::Examine { input_file } => {
let in_buffer = open_file_or_stdin(input_file)?;
let out_buffer = std::io::stdout();
match examine_sync::examine_sync(in_buffer, out_buffer, std::io::stdout().is_terminal())
{
match examine::examine(in_buffer, out_buffer, atty::is(atty::Stream::Stdout)) {
Ok(()) => {}
Err(e) => {
eprintln!("Error: {:?}", e);

View file

@ -1,6 +1,3 @@
/node_modules
/yarn.lock
dist
docs/
.vim
deno_dist/

25
automerge-js/README.md Normal file
View file

@ -0,0 +1,25 @@
## Automerge JS
This is a reimplementation of Automerge as a JavaScript wrapper around the "automerge-wasm".
This package is in alpha and feedback in welcome.
The primary differences between using this package and "automerge" are as follows:
1. The low level api needs to plugged in via the use function. The only current implementation of "automerge-wasm" but another could used in theory.
```javascript
import * as Automerge from "automerge-js";
import * as wasm_api from "automerge-wasm";
// browsers require an async wasm load - see automerge-wasm docs
Automerge.use(wasm_api);
```
2. There is no front-end back-end split, and no patch format or patch observer. These concepts don't make sense with the wasm implementation.
3. The basic `Doc<T>` object is now a Proxy object and will behave differently in a repl environment.
4. The 'Text' class is currently very slow and needs to be re-worked.
Beyond this please refer to the Automerge [README](http://github.com/automerge/automerge/) for further information.

View file

@ -0,0 +1,6 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../dist/cjs"
}
}

View file

@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"target": "es6",
"module": "es6",
"outDir": "../dist/mjs"
}
}

View file

@ -54,7 +54,7 @@ yarn e2e buildexamples -e webpack
If you're experimenting with a project which is not in the `examples` folder
you'll need a running registry. `run-registry` builds and publishes
`automerge-js` and `automerge-wasm` and then runs the registry at
`localhost:4873`.
`localhost:4873`.
```
yarn e2e run-registry
@ -63,6 +63,7 @@ yarn e2e run-registry
You can now run `yarn install --registry http://localhost:4873` to experiment
with the built packages.
## Using the `dev` build of `automerge-wasm`
All the commands above take a `-p` flag which can be either `release` or

409
automerge-js/e2e/index.ts Normal file
View file

@ -0,0 +1,409 @@
import {once} from "events"
import {setTimeout} from "timers/promises"
import {spawn, ChildProcess} from "child_process"
import * as child_process from "child_process"
import {command, subcommands, run, array, multioption, option, Type} from "cmd-ts"
import * as path from "path"
import * as fsPromises from "fs/promises"
import fetch from "node-fetch"
const VERDACCIO_DB_PATH = path.normalize(`${__dirname}/verdacciodb`)
const VERDACCIO_CONFIG_PATH = path.normalize(`${__dirname}/verdaccio.yaml`)
const AUTOMERGE_WASM_PATH = path.normalize(`${__dirname}/../../automerge-wasm`)
const AUTOMERGE_JS_PATH = path.normalize(`${__dirname}/..`)
const EXAMPLES_DIR = path.normalize(path.join(__dirname, "../", "examples"))
// The different example projects in "../examples"
type Example = "webpack" | "vite"
// Type to parse strings to `Example` so the types line up for the `buildExamples` commmand
const ReadExample: Type<string, Example> = {
async from(str) {
if (str === "webpack") {
return "webpack"
} else if (str === "vite") {
return "vite"
} else {
throw new Error(`Unknown example type ${str}`)
}
}
}
type Profile = "dev" | "release"
const ReadProfile: Type<string, Profile> = {
async from(str) {
if (str === "dev") {
return "dev"
} else if (str === "release") {
return "release"
} else {
throw new Error(`Unknown profile ${str}`)
}
}
}
const buildjs = command({
name: "buildjs",
args: {
profile: option({
type: ReadProfile,
long: "profile",
short: "p",
defaultValue: () => "dev" as Profile
})
},
handler: ({profile}) => {
console.log("building js")
withPublishedWasm(profile, async (registryUrl: string) => {
await buildAndPublishAutomergeJs(registryUrl)
})
}
})
const buildWasm = command({
name: "buildwasm",
args: {
profile: option({
type: ReadProfile,
long: "profile",
short: "p",
defaultValue: () => "dev" as Profile
})
},
handler: ({profile}) => {
console.log("building automerge-wasm")
withRegistry(
publishAutomergeTypes,
buildAutomergeWasm(profile),
)
}
})
const buildexamples = command({
name: "buildexamples",
args: {
examples: multioption({
long: "example",
short: "e",
type: array(ReadExample),
}),
profile: option({
type: ReadProfile,
long: "profile",
short: "p",
defaultValue: () => "dev" as Profile
})
},
handler: ({examples, profile}) => {
if (examples.length === 0) {
examples = ["webpack", "vite"]
}
buildExamples(examples, profile)
}
})
const runRegistry = command({
name: "run-registry",
args: {
profile: option({
type: ReadProfile,
long: "profile",
short: "p",
defaultValue: () => "dev" as Profile
})
},
handler: ({profile}) => {
withPublishedWasm(profile, async (registryUrl: string) => {
await buildAndPublishAutomergeJs(registryUrl)
console.log("\n************************")
console.log(` Verdaccio NPM registry is running at ${registryUrl}`)
console.log(" press CTRL-C to exit ")
console.log("************************")
await once(process, "SIGINT")
}).catch(e => {
console.error(`Failed: ${e}`)
})
}
})
const app = subcommands({
name: "e2e",
cmds: {buildjs, buildexamples, buildwasm: buildWasm, "run-registry": runRegistry}
})
run(app, process.argv.slice(2))
async function buildExamples(examples: Array<Example>, profile: Profile) {
withPublishedWasm(profile, async (registryUrl) => {
printHeader("building and publishing automerge")
await buildAndPublishAutomergeJs(registryUrl)
for (const example of examples) {
printHeader(`building ${example} example`)
if (example === "webpack") {
const projectPath = path.join(EXAMPLES_DIR, example)
await removeExistingAutomerge(projectPath)
await fsPromises.rm(path.join(projectPath, "yarn.lock"), {force: true})
await spawnAndWait("yarn", ["--cwd", projectPath, "install", "--registry", registryUrl, "--check-files"], {stdio: "inherit"})
await spawnAndWait("yarn", ["--cwd", projectPath, "build"], {stdio: "inherit"})
} else if (example === "vite") {
const projectPath = path.join(EXAMPLES_DIR, example)
await removeExistingAutomerge(projectPath)
await fsPromises.rm(path.join(projectPath, "yarn.lock"), {force: true})
await spawnAndWait("yarn", ["--cwd", projectPath, "install", "--registry", registryUrl, "--check-files"], {stdio: "inherit"})
await spawnAndWait("yarn", ["--cwd", projectPath, "build"], {stdio: "inherit"})
}
}
})
}
type WithRegistryAction = (registryUrl: string) => Promise<void>
async function withRegistry(action: WithRegistryAction, ...actions: Array<WithRegistryAction>) {
// First, start verdaccio
printHeader("Starting verdaccio NPM server")
const verd = await VerdaccioProcess.start()
actions.unshift(action)
for (const action of actions) {
try {
type Step = "verd-died" | "action-completed"
const verdDied: () => Promise<Step> = async () => {
await verd.died()
return "verd-died"
}
const actionComplete: () => Promise<Step> = async () => {
await action("http://localhost:4873")
return "action-completed"
}
const result = await Promise.race([verdDied(), actionComplete()])
if (result === "verd-died") {
throw new Error("verdaccio unexpectedly exited")
}
} catch(e) {
await verd.kill()
throw e
}
}
await verd.kill()
}
async function withPublishedWasm(profile: Profile, action: WithRegistryAction) {
withRegistry(
publishAutomergeTypes,
buildAutomergeWasm(profile),
publishAutomergeWasm,
action
)
}
async function publishAutomergeTypes(registryUrl: string) {
// Publish automerge-types
printHeader("Publishing automerge-types package to verdaccio")
await fsPromises.rm(path.join(VERDACCIO_DB_PATH, "automerge-types"), { recursive: true, force: true} )
await yarnPublish(registryUrl, path.join(AUTOMERGE_WASM_PATH, "types"))
}
function buildAutomergeWasm(profile: Profile): WithRegistryAction {
return async (registryUrl: string) => {
printHeader("building automerge-wasm")
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_WASM_PATH, "--registry", registryUrl, "install"], {stdio: "inherit"})
const cmd = profile === "release" ? "release" : "debug"
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_WASM_PATH, cmd], {stdio: "inherit"})
}
}
async function publishAutomergeWasm(registryUrl: string) {
printHeader("Publishing automerge-wasm to verdaccio")
await fsPromises.rm(path.join(VERDACCIO_DB_PATH, "automerge-wasm"), { recursive: true, force: true} )
await yarnPublish(registryUrl, AUTOMERGE_WASM_PATH)
}
async function buildAndPublishAutomergeJs(registryUrl: string) {
// Build the js package
printHeader("Building automerge")
await removeExistingAutomerge(AUTOMERGE_JS_PATH)
await removeFromVerdaccio("automerge")
await fsPromises.rm(path.join(AUTOMERGE_JS_PATH, "yarn.lock"), {force: true})
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_JS_PATH, "install", "--registry", registryUrl, "--check-files"], {stdio: "inherit"})
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_JS_PATH, "build"], {stdio: "inherit"})
await yarnPublish(registryUrl, AUTOMERGE_JS_PATH)
}
/**
* A running verdaccio process
*
*/
class VerdaccioProcess {
child: ChildProcess
stdout: Array<Buffer>
stderr: Array<Buffer>
constructor(child: ChildProcess) {
this.child = child
// Collect stdout/stderr otherwise the subprocess gets blocked writing
this.stdout = []
this.stderr = []
this.child.on("data", (data) => this.stdout.push(data))
this.child.on("data", (data) => this.stderr.push(data))
const errCallback = (e: any) => {
console.error("!!!!!!!!!ERROR IN VERDACCIO PROCESS!!!!!!!!!")
console.error(" ", e)
if (this.stdout.length > 0) {
console.log("\n**Verdaccio stdout**")
const stdout = Buffer.concat(this.stdout)
process.stdout.write(stdout)
}
if (this.stderr.length > 0) {
console.log("\n**Verdaccio stderr**")
const stdout = Buffer.concat(this.stderr)
process.stdout.write(stdout)
}
process.exit(-1)
}
this.child.on("error", errCallback)
}
/**
* Spawn a verdaccio process and wait for it to respond succesfully to http requests
*
* The returned `VerdaccioProcess` can be used to control the subprocess
*/
static async start() {
const child = spawn("yarn", ["verdaccio", "--config", VERDACCIO_CONFIG_PATH], {env: { FORCE_COLOR: "true"}})
// Forward stdout and stderr whilst waiting for startup to complete
const stdoutCallback = (data: Buffer) => process.stdout.write(data)
const stderrCallback = (data: Buffer) => process.stderr.write(data)
child.stdout.on("data", stdoutCallback)
child.stderr.on("data", stderrCallback)
const errored = once(child, "error")
const healthCheck = async () => {
while (true) {
try {
const resp = await fetch("http://localhost:4873")
if (resp.status === 200) {
return
} else {
console.log(`Healthcheck failed: bad status ${resp.status}`)
}
} catch (e) {
console.error(`Healthcheck failed: ${e}`)
}
await setTimeout(500)
}
}
await Promise.race([healthCheck(), errored])
// Stop forwarding stdout/stderr
child.stdout.off("data", stdoutCallback)
child.stderr.off("data", stderrCallback)
return new VerdaccioProcess(child)
}
/**
* Send a SIGKILL to the process and wait for it to stop
*/
async kill() {
this.child.kill();
const errored = once(this.child, "error")
const finished = once(this.child, "close")
await Promise.race([errored, finished])
}
/**
* A promise which resolves if the subprocess exits for some reason
*/
async died(): Promise<number | null> {
const [exit, _signal] = await once(this.child, "exit")
return exit
}
}
function printHeader(header: string) {
console.log("\n===============================")
console.log(` ${header}`)
console.log("===============================")
}
/**
* Removes the automerge, automerge-wasm, and automerge-js packages from
* `$packageDir/node_modules`
*
* This is useful to force refreshing a package by use in combination with
* `yarn install --check-files`, which checks if a package is present in
* `node_modules` and if it is not forces a reinstall.
*
* @param packageDir - The directory containing the package.json of the target project
*/
async function removeExistingAutomerge(packageDir: string) {
await fsPromises.rm(path.join(packageDir, "node_modules", "automerge-wasm"), {recursive: true, force: true})
await fsPromises.rm(path.join(packageDir, "node_modules", "automerge-types"), {recursive: true, force: true})
await fsPromises.rm(path.join(packageDir, "node_modules", "automerge"), {recursive: true, force: true})
}
type SpawnResult = {
stdout?: Buffer,
stderr?: Buffer,
}
async function spawnAndWait(cmd: string, args: Array<string>, options: child_process.SpawnOptions): Promise<SpawnResult> {
const child = spawn(cmd, args, options)
let stdout = null
let stderr = null
if (child.stdout) {
stdout = []
child.stdout.on("data", data => stdout.push(data))
}
if (child.stderr) {
stderr = []
child.stderr.on("data", data => stderr.push(data))
}
const [exit, _signal] = await once(child, "exit")
if (exit && exit !== 0) {
throw new Error("nonzero exit code")
}
return {
stderr: stderr? Buffer.concat(stderr) : null,
stdout: stdout ? Buffer.concat(stdout) : null
}
}
/**
* Remove a package from the verdaccio registry. This is necessary because we
* often want to _replace_ a version rather than update the version number.
* Obviously this is very bad and verboten in normal circumastances, but the
* whole point here is to be able to test the entire packaging story so it's
* okay I Promise.
*/
async function removeFromVerdaccio(packageName: string) {
await fsPromises.rm(path.join(VERDACCIO_DB_PATH, packageName), {force: true, recursive: true})
}
async function yarnPublish(registryUrl: string, cwd: string) {
await spawnAndWait(
"yarn",
[
"--registry",
registryUrl,
"--cwd",
cwd,
"publish",
"--non-interactive",
],
{
stdio: "inherit",
env: {
FORCE_COLOR: "true",
// This is a fake token, it just has to be the right format
npm_config__auth: "//localhost:4873/:_authToken=Gp2Mgxm4faa/7wp0dMSuRA=="
}
})
}

View file

@ -0,0 +1,6 @@
{
"compilerOptions": {
"types": ["node"]
},
"module": "nodenext"
}

View file

@ -0,0 +1,28 @@
storage: "./verdacciodb"
auth:
htpasswd:
file: ./htpasswd
publish:
allow_offline: true
logs: {type: stdout, format: pretty, level: info}
packages:
"automerge-types":
access: "$all"
publish: "$all"
"automerge-wasm":
access: "$all"
publish: "$all"
"automerge-js":
access: "$all"
publish: "$all"
"*":
access: "$all"
publish: "$all"
proxy: npmjs
"@*/*":
access: "$all"
publish: "$all"
proxy: npmjs
uplinks:
npmjs:
url: https://registry.npmjs.org/

View file

@ -61,9 +61,9 @@
form-data "^3.0.0"
"@types/node@*", "@types/node@^18.7.18":
version "18.7.23"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.23.tgz#75c580983846181ebe5f4abc40fe9dfb2d65665f"
integrity sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg==
version "18.7.18"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.18.tgz#633184f55c322e4fb08612307c274ee6d5ed3154"
integrity sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==
"@verdaccio/commons-api@10.2.0":
version "10.2.0"
@ -1610,9 +1610,9 @@ rimraf@~2.4.0:
glob "^6.0.1"
rxjs@^7.5.2:
version "7.5.7"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39"
integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==
version "7.5.6"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.6.tgz#0446577557862afd6903517ce7cae79ecb9662bc"
integrity sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==
dependencies:
tslib "^2.1.0"
@ -1889,14 +1889,14 @@ typed-emitter@^2.1.0:
rxjs "^7.5.2"
typescript@^4.8.3:
version "4.8.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6"
integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==
version "4.8.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.3.tgz#d59344522c4bc464a65a730ac695007fdb66dd88"
integrity sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==
uglify-js@^3.1.4:
version "3.17.2"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.2.tgz#f55f668b9a64b213977ae688703b6bbb7ca861c6"
integrity sha512-bbxglRjsGQMchfvXZNusUcYgiB9Hx2K4AHYXQy2DITZ9Rd+JzhX7+hoocE5Winr7z2oHvPsekkBwXtigvxevXg==
version "3.17.1"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.1.tgz#1258a2a488147a8266b3034499ce6959978ba7f4"
integrity sha512-+juFBsLLw7AqMaqJ0GFvlsGZwdQfI2ooKQB39PSBgMnMakcFosi9O8jCwE+2/2nMNcc0z63r9mwjoDG8zr+q0Q==
universalify@^0.2.0:
version "0.2.0"

View file

@ -1,15 +1,15 @@
import * as Automerge from "/node_modules/.vite/deps/automerge-js.js?v=6e973f28"
console.log(Automerge)
let doc = Automerge.init()
doc = Automerge.change(doc, d => (d.hello = "from automerge-js"))
console.log(doc)
const result = JSON.stringify(doc)
import * as Automerge from "/node_modules/.vite/deps/automerge-js.js?v=6e973f28";
console.log(Automerge);
let doc = Automerge.init();
doc = Automerge.change(doc, (d) => d.hello = "from automerge-js");
console.log(doc);
const result = JSON.stringify(doc);
if (typeof document !== "undefined") {
const element = document.createElement("div")
element.innerHTML = JSON.stringify(result)
document.body.appendChild(element)
const element = document.createElement("div");
element.innerHTML = JSON.stringify(result);
document.body.appendChild(element);
} else {
console.log("node:", result)
console.log("node:", result);
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9ob21lL2FsZXgvUHJvamVjdHMvYXV0b21lcmdlL2F1dG9tZXJnZS1ycy9hdXRvbWVyZ2UtanMvZXhhbXBsZXMvdml0ZS9zcmMvbWFpbi50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBBdXRvbWVyZ2UgZnJvbSBcImF1dG9tZXJnZS1qc1wiXG5cbi8vIGhlbGxvIHdvcmxkIGNvZGUgdGhhdCB3aWxsIHJ1biBjb3JyZWN0bHkgb24gd2ViIG9yIG5vZGVcblxuY29uc29sZS5sb2coQXV0b21lcmdlKVxubGV0IGRvYyA9IEF1dG9tZXJnZS5pbml0KClcbmRvYyA9IEF1dG9tZXJnZS5jaGFuZ2UoZG9jLCAoZDogYW55KSA9PiBkLmhlbGxvID0gXCJmcm9tIGF1dG9tZXJnZS1qc1wiKVxuY29uc29sZS5sb2coZG9jKVxuY29uc3QgcmVzdWx0ID0gSlNPTi5zdHJpbmdpZnkoZG9jKVxuXG5pZiAodHlwZW9mIGRvY3VtZW50ICE9PSAndW5kZWZpbmVkJykge1xuICAgIC8vIGJyb3dzZXJcbiAgICBjb25zdCBlbGVtZW50ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2Jyk7XG4gICAgZWxlbWVudC5pbm5lckhUTUwgPSBKU09OLnN0cmluZ2lmeShyZXN1bHQpXG4gICAgZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZChlbGVtZW50KTtcbn0gZWxzZSB7XG4gICAgLy8gc2VydmVyXG4gICAgY29uc29sZS5sb2coXCJub2RlOlwiLCByZXN1bHQpXG59XG5cbiJdLCJtYXBwaW5ncyI6IkFBQUEsWUFBWSxlQUFlO0FBSTNCLFFBQVEsSUFBSSxTQUFTO0FBQ3JCLElBQUksTUFBTSxVQUFVLEtBQUs7QUFDekIsTUFBTSxVQUFVLE9BQU8sS0FBSyxDQUFDLE1BQVcsRUFBRSxRQUFRLG1CQUFtQjtBQUNyRSxRQUFRLElBQUksR0FBRztBQUNmLE1BQU0sU0FBUyxLQUFLLFVBQVUsR0FBRztBQUVqQyxJQUFJLE9BQU8sYUFBYSxhQUFhO0FBRWpDLFFBQU0sVUFBVSxTQUFTLGNBQWMsS0FBSztBQUM1QyxVQUFRLFlBQVksS0FBSyxVQUFVLE1BQU07QUFDekMsV0FBUyxLQUFLLFlBQVksT0FBTztBQUNyQyxPQUFPO0FBRUgsVUFBUSxJQUFJLFNBQVMsTUFBTTtBQUMvQjsiLCJuYW1lcyI6W119
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9ob21lL2FsZXgvUHJvamVjdHMvYXV0b21lcmdlL2F1dG9tZXJnZS1ycy9hdXRvbWVyZ2UtanMvZXhhbXBsZXMvdml0ZS9zcmMvbWFpbi50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBBdXRvbWVyZ2UgZnJvbSBcImF1dG9tZXJnZS1qc1wiXG5cbi8vIGhlbGxvIHdvcmxkIGNvZGUgdGhhdCB3aWxsIHJ1biBjb3JyZWN0bHkgb24gd2ViIG9yIG5vZGVcblxuY29uc29sZS5sb2coQXV0b21lcmdlKVxubGV0IGRvYyA9IEF1dG9tZXJnZS5pbml0KClcbmRvYyA9IEF1dG9tZXJnZS5jaGFuZ2UoZG9jLCAoZDogYW55KSA9PiBkLmhlbGxvID0gXCJmcm9tIGF1dG9tZXJnZS1qc1wiKVxuY29uc29sZS5sb2coZG9jKVxuY29uc3QgcmVzdWx0ID0gSlNPTi5zdHJpbmdpZnkoZG9jKVxuXG5pZiAodHlwZW9mIGRvY3VtZW50ICE9PSAndW5kZWZpbmVkJykge1xuICAgIC8vIGJyb3dzZXJcbiAgICBjb25zdCBlbGVtZW50ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2Jyk7XG4gICAgZWxlbWVudC5pbm5lckhUTUwgPSBKU09OLnN0cmluZ2lmeShyZXN1bHQpXG4gICAgZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZChlbGVtZW50KTtcbn0gZWxzZSB7XG4gICAgLy8gc2VydmVyXG4gICAgY29uc29sZS5sb2coXCJub2RlOlwiLCByZXN1bHQpXG59XG5cbiJdLCJtYXBwaW5ncyI6IkFBQUEsWUFBWSxlQUFlO0FBSTNCLFFBQVEsSUFBSSxTQUFTO0FBQ3JCLElBQUksTUFBTSxVQUFVLEtBQUs7QUFDekIsTUFBTSxVQUFVLE9BQU8sS0FBSyxDQUFDLE1BQVcsRUFBRSxRQUFRLG1CQUFtQjtBQUNyRSxRQUFRLElBQUksR0FBRztBQUNmLE1BQU0sU0FBUyxLQUFLLFVBQVUsR0FBRztBQUVqQyxJQUFJLE9BQU8sYUFBYSxhQUFhO0FBRWpDLFFBQU0sVUFBVSxTQUFTLGNBQWMsS0FBSztBQUM1QyxVQUFRLFlBQVksS0FBSyxVQUFVLE1BQU07QUFDekMsV0FBUyxLQUFLLFlBQVksT0FBTztBQUNyQyxPQUFPO0FBRUgsVUFBUSxJQUFJLFNBQVMsTUFBTTtBQUMvQjsiLCJuYW1lcyI6W119

View file

@ -9,7 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"@automerge/automerge": "2.0.0-alpha.7"
"automerge": "1.0.1-preview.8"
},
"devDependencies": {
"typescript": "^4.6.4",

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -4,6 +4,6 @@ export function setupCounter(element: HTMLButtonElement) {
counter = count
element.innerHTML = `count is ${counter}`
}
element.addEventListener("click", () => setCounter(++counter))
element.addEventListener('click', () => setCounter(++counter))
setCounter(0)
}

View file

@ -0,0 +1,18 @@
import * as Automerge from "automerge"
// hello world code that will run correctly on web or node
let doc = Automerge.init()
doc = Automerge.change(doc, (d: any) => d.hello = "from automerge-js")
const result = JSON.stringify(doc)
if (typeof document !== 'undefined') {
// browser
const element = document.createElement('div');
element.innerHTML = JSON.stringify(result)
document.body.appendChild(element);
} else {
// server
console.log("node:", result)
}

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

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