Compare commits

...

8 commits

Author SHA1 Message Date
alexjg
cb409b6ffe
docs: timestamp -> time in automerge.change examples (#548) 2023-03-09 18:10:23 +00:00
Conrad Irwin
b34b46fa16
smaller automerge c (#545)
* Fix automerge-c tests on mac

* Generate significantly smaller automerge-c builds

This cuts the size of libautomerge_core.a from 25Mb to 1.6Mb on macOS
and 53Mb to 2.7Mb on Linux.

As a side-effect of setting codegen-units = 1 for all release builds the
optimized wasm files are also 100kb smaller.
2023-03-09 15:09:43 +00:00
Conrad Irwin
7b747b8341
Error instead of corrupt large op counters (#543)
Since b78211ca6, OpIds have been silently truncated to 2**32. This
causes corruption in the case the op id overflows.

This change converts the silent error to a panic, and guards against the
panic on the codepath found by the fuzzer.
2023-03-07 16:49:04 +00:00
Conrad Irwin
2c1970f664
Fix panic on invalid action (#541)
We make the validation on parsing operations in the encoded changes stricter to avoid a possible panic when applying changes.
2023-03-04 12:09:08 +00:00
christine betts
63b761c0d1
Suppress clippy warning in parse.rs + bump toolchain (#542)
* Fix rust error in parse.rs
* Bump toolchain to 1.67.0
2023-03-03 22:42:40 +00:00
Conrad Irwin
44fa7ac416
Don't panic on missing deps of change chunks (#538)
* Fix doubly-reported ops in load of change chunks

Since c3c04128f5, observers have been
called twice when calling Automerge::load() with change chunks.

* Better handle change chunks with missing deps

Before this change Automerge::load would panic if you passed a change
chunk that was missing a dependency, or multiple change chunks not in
strict dependency order. After this change these cases will error
instead.
2023-02-27 20:12:09 +00:00
Jason Kankiewicz
8de2fa9bd4
C API 2 (#530)
The AMvalue union, AMlistItem struct, AMmapItem struct, and AMobjItem struct are gone, replaced by the AMitem struct.

The AMchangeHashes, AMchanges, AMlistItems, AMmapItems, AMobjItems, AMstrs, and AMsyncHaves iterators are gone, replaced by the AMitems iterator.

The AMitem struct is opaque, getting and setting values is now achieved exclusively through function calls.

The AMitemsNext(), AMitemsPrev(), and AMresultItem() functions return a pointer to an AMitem struct so you ultimately get the same thing whether you're iterating over a sequence or calling AMmapGet() or AMlistGet().

Calling AMitemResult() on an AMitem struct will produce a new AMresult struct referencing its storage so now the AMresult struct for an iterator can be subsequently freed without affecting the AMitem structs that were filtered out of it.

The storage for a set of AMitem structs can be recombined into a single AMresult struct by passing pointers to their corresponding AMresult structs to AMresultCat().

For C/C++ programmers, I've added AMstrCmp(), AMstrdup(), AM{idxType,objType,status,valType}ToString() and AM{idxType,objType,status,valType}FromString(). It's also now possible to pass arbitrary parameters through AMstack{Item,Items,Result}() to a callback function.
2023-02-25 18:47:00 +00:00
Philip Schatz
407faefa6e
A few setup fixes (#529)
* include deno in dependencies

* install javascript dependencies

* remove redundant operation
2023-02-15 09:23:02 +00:00
108 changed files with 9486 additions and 8675 deletions

View file

@ -14,7 +14,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.66.0
toolchain: 1.67.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.66.0
toolchain: 1.67.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.66.0
toolchain: 1.67.0
default: true
- uses: Swatinem/rust-cache@v1
- name: Build rust docs
@ -118,7 +118,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.66.0
toolchain: nightly-2023-01-26
default: true
- uses: Swatinem/rust-cache@v1
- name: Install CMocka
@ -127,6 +127,8 @@ 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
@ -136,7 +138,7 @@ jobs:
strategy:
matrix:
toolchain:
- 1.66.0
- 1.67.0
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
@ -155,7 +157,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.66.0
toolchain: 1.67.0
default: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/ci/build-test
@ -168,7 +170,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.66.0
toolchain: 1.67.0
default: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/ci/build-test

View file

@ -57,7 +57,6 @@ 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)
## Repository Organisation
- `./rust` - the rust rust implementation and also the Rust components of
@ -113,9 +112,16 @@ 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

View file

@ -54,6 +54,7 @@
nodejs
yarn
deno
# c deps
cmake

View file

@ -305,7 +305,7 @@ export function from<T extends Record<string, unknown>>(
* @example A change with a message and a timestamp
*
* ```
* doc1 = automerge.change(doc1, {message: "add another value", timestamp: 1640995200}, d => {
* doc1 = automerge.change(doc1, {message: "add another value", time: 1640995200}, d => {
* d.key2 = "value2"
* })
* ```
@ -316,7 +316,7 @@ export function from<T extends Record<string, unknown>>(
* let patchCallback = patch => {
* patchedPath = patch.path
* }
* doc1 = automerge.change(doc1, {message, "add another value", timestamp: 1640995200, patchCallback}, d => {
* doc1 = automerge.change(doc1, {message, "add another value", time: 1640995200, patchCallback}, d => {
* d.key2 = "value2"
* })
* assert.equal(patchedPath, ["key2"])

View file

@ -10,13 +10,8 @@ members = [
resolver = "2"
[profile.release]
debug = true
lto = true
opt-level = 3
codegen-units = 1
[profile.bench]
debug = true
[profile.release.package.automerge-wasm]
debug = false
opt-level = 3

View file

@ -0,0 +1,250 @@
---
Language: Cpp
# BasedOnStyle: Chromium
AccessModifierOffset: -1
AlignAfterOpenBracket: Align
AlignArrayOfStructures: None
AlignConsecutiveAssignments:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: true
AlignConsecutiveBitFields:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: false
AlignConsecutiveDeclarations:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: false
AlignConsecutiveMacros:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: false
AlignEscapedNewlines: Left
AlignOperands: Align
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortEnumsOnASingleLine: true
AllowShortBlocksOnASingleLine: Never
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Inline
AllowShortLambdasOnASingleLine: All
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: true
AlwaysBreakTemplateDeclarations: Yes
AttributeMacros:
- __capability
BinPackArguments: true
BinPackParameters: false
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: Never
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
BeforeLambdaBody: false
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeConceptDeclarations: Always
BreakBeforeBraces: Attach
BreakBeforeInheritanceComma: false
BreakInheritanceList: BeforeColon
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeColon
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: true
ColumnLimit: 120
CommentPragmas: '^ IWYU pragma:'
QualifierAlignment: Leave
CompactNamespaces: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DeriveLineEnding: true
DerivePointerAlignment: false
DisableFormat: false
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: LogicalBlock
ExperimentalAutoDetectBinPacking: false
PackConstructorInitializers: NextLine
BasedOnStyle: ''
ConstructorInitializerAllOnOneLineOrOnePerLine: false
AllowAllConstructorInitializersOnNextLine: true
FixNamespaceComments: true
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IfMacros:
- KJ_IF_MAYBE
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '^<ext/.*\.h>'
Priority: 2
SortPriority: 0
CaseSensitive: false
- Regex: '^<.*\.h>'
Priority: 1
SortPriority: 0
CaseSensitive: false
- Regex: '^<.*'
Priority: 2
SortPriority: 0
CaseSensitive: false
- Regex: '.*'
Priority: 3
SortPriority: 0
CaseSensitive: false
IncludeIsMainRegex: '([-_](test|unittest))?$'
IncludeIsMainSourceRegex: ''
IndentAccessModifiers: false
IndentCaseLabels: true
IndentCaseBlocks: false
IndentGotoLabels: true
IndentPPDirectives: None
IndentExternBlock: AfterExternBlock
IndentRequiresClause: true
IndentWidth: 4
IndentWrappedFunctionNames: false
InsertBraces: false
InsertTrailingCommas: None
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: false
LambdaBodyIndentation: Signature
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Never
ObjCBlockIndentWidth: 2
ObjCBreakBeforeNestedBlockParam: true
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 1
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakOpenParenthesis: 0
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 200
PenaltyIndentedWhitespace: 0
PointerAlignment: Left
PPIndentWidth: -1
RawStringFormats:
- Language: Cpp
Delimiters:
- cc
- CC
- cpp
- Cpp
- CPP
- 'c++'
- 'C++'
CanonicalDelimiter: ''
BasedOnStyle: google
- Language: TextProto
Delimiters:
- pb
- PB
- proto
- PROTO
EnclosingFunctions:
- EqualsProto
- EquivToProto
- PARSE_PARTIAL_TEXT_PROTO
- PARSE_TEST_PROTO
- PARSE_TEXT_PROTO
- ParseTextOrDie
- ParseTextProtoOrDie
- ParseTestProto
- ParsePartialTestProto
CanonicalDelimiter: pb
BasedOnStyle: google
ReferenceAlignment: Pointer
ReflowComments: true
RemoveBracesLLVM: false
RequiresClausePosition: OwnLine
SeparateDefinitionBlocks: Leave
ShortNamespaceLines: 1
SortIncludes: CaseSensitive
SortJavaStaticImport: Before
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCaseColon: false
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeParensOptions:
AfterControlStatements: true
AfterForeachMacros: true
AfterFunctionDefinitionName: false
AfterFunctionDeclarationName: false
AfterIfMacros: true
AfterOverloadedOperator: false
AfterRequiresInClause: false
AfterRequiresInExpression: false
BeforeNonEmptyParentheses: false
SpaceAroundPointerQualifiers: Default
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 2
SpacesInAngles: Never
SpacesInConditionalStatement: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: -1
SpacesInParentheses: false
SpacesInSquareBrackets: false
SpaceBeforeSquareBrackets: false
BitFieldColonSpacing: Both
Standard: Auto
StatementAttributeLikeMacros:
- Q_EMIT
StatementMacros:
- Q_UNUSED
- QT_REQUIRE_VERSION
TabWidth: 8
UseCRLF: false
UseTab: Never
WhitespaceSensitiveMacros:
- STRINGIZE
- PP_STRINGIZE
- BOOST_PP_STRINGIZE
- NS_SWIFT_NAME
- CF_SWIFT_NAME
...

View file

@ -1,10 +1,10 @@
automerge
automerge.h
automerge.o
*.cmake
build/
CMakeCache.txt
CMakeFiles
CMakePresets.json
Makefile
DartConfiguration.tcl
config.h
CMakeCache.txt
Cargo
out/

View file

@ -1,97 +1,297 @@
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
cmake_minimum_required(VERSION 3.23 FATAL_ERROR)
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
project(automerge-c VERSION 0.1.0
LANGUAGES C
DESCRIPTION "C bindings for the Automerge Rust library.")
# Parse the library name, project name and project version out of Cargo's TOML file.
set(CARGO_LIB_SECTION OFF)
set(LIBRARY_NAME "automerge")
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 0.0.1 LANGUAGES C DESCRIPTION "C bindings for the Automerge Rust backend.")
include(CTest)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
option(BUILD_SHARED_LIBS "Enable the choice of a shared or static library.")
include(CTest)
include(CMakePackageConfigHelpers)
include(GNUInstallDirs)
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
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(CARGO_TARGET_DIR "${CMAKE_BINARY_DIR}/Cargo/target")
set(CBINDGEN_INCLUDEDIR "${CARGO_TARGET_DIR}/${CMAKE_INSTALL_INCLUDEDIR}")
set(CBINDGEN_INCLUDEDIR "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_INCLUDEDIR}")
set(CBINDGEN_TARGET_DIR "${CBINDGEN_INCLUDEDIR}/${PROJECT_NAME}")
add_subdirectory(src)
find_program (
CARGO_CMD
"cargo"
PATHS "$ENV{CARGO_HOME}/bin"
DOC "The Cargo command"
)
# Generate and install the configuration header.
if(NOT CARGO_CMD)
message(FATAL_ERROR "Cargo (Rust package manager) not found! "
"Please install it and/or set the CARGO_HOME "
"environment variable to its path.")
endif()
string(TOLOWER "${CMAKE_BUILD_TYPE}" BUILD_TYPE_LOWER)
# In order to build with -Z build-std, we need to pass target explicitly.
# https://doc.rust-lang.org/cargo/reference/unstable.html#build-std
execute_process (
COMMAND rustc -vV
OUTPUT_VARIABLE RUSTC_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
string(REGEX REPLACE ".*host: ([^ \n]*).*" "\\1"
CARGO_TARGET
${RUSTC_VERSION}
)
if(BUILD_TYPE_LOWER STREQUAL debug)
set(CARGO_BUILD_TYPE "debug")
set(CARGO_FLAG --target=${CARGO_TARGET})
else()
set(CARGO_BUILD_TYPE "release")
if (NOT RUSTC_VERSION MATCHES "nightly")
set(RUSTUP_TOOLCHAIN nightly)
endif()
set(RUSTFLAGS -C\ panic=abort)
set(CARGO_FLAG -Z build-std=std,panic_abort --release --target=${CARGO_TARGET})
endif()
set(CARGO_FEATURES "")
set(CARGO_BINARY_DIR "${CARGO_TARGET_DIR}/${CARGO_TARGET}/${CARGO_BUILD_TYPE}")
set(BINDINGS_NAME "${LIBRARY_NAME}_core")
configure_file(
${CMAKE_MODULE_PATH}/Cargo.toml.in
${CMAKE_SOURCE_DIR}/Cargo.toml
@ONLY
NEWLINE_STYLE LF
)
set(INCLUDE_GUARD_PREFIX "${SYMBOL_PREFIX}")
configure_file(
${CMAKE_MODULE_PATH}/cbindgen.toml.in
${CMAKE_SOURCE_DIR}/cbindgen.toml
@ONLY
NEWLINE_STYLE LF
)
set(CARGO_OUTPUT
${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
${CARGO_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${BINDINGS_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}
)
# \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(
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} RUSTUP_TOOLCHAIN=${RUSTUP_TOOLCHAIN} RUSTFLAGS=${RUSTFLAGS} ${CARGO_CMD} build ${CARGO_FLAG} ${CARGO_FEATURES}
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
MAIN_DEPENDENCY
src/lib.rs
DEPENDS
src/actor_id.rs
src/byte_span.rs
src/change.rs
src/doc.rs
src/doc/list.rs
src/doc/map.rs
src/doc/utils.rs
src/index.rs
src/item.rs
src/items.rs
src/obj.rs
src/result.rs
src/sync.rs
src/sync/have.rs
src/sync/message.rs
src/sync/state.rs
${CMAKE_SOURCE_DIR}/build.rs
${CMAKE_MODULE_PATH}/Cargo.toml.in
${CMAKE_MODULE_PATH}/cbindgen.toml.in
WORKING_DIRECTORY
${CMAKE_SOURCE_DIR}
COMMENT
"Producing the bindings' artifacts with Cargo..."
VERBATIM
)
add_custom_target(${BINDINGS_NAME}_artifacts ALL
DEPENDS ${CARGO_OUTPUT}
)
add_library(${BINDINGS_NAME} STATIC IMPORTED GLOBAL)
target_include_directories(${BINDINGS_NAME} INTERFACE "${CBINDGEN_INCLUDEDIR}")
set_target_properties(
${BINDINGS_NAME}
PROPERTIES
# \note Cargo writes a debug build into a nested directory instead of
# decorating its name.
DEBUG_POSTFIX ""
DEFINE_SYMBOL ""
IMPORTED_IMPLIB ""
IMPORTED_LOCATION "${CARGO_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${BINDINGS_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}"
IMPORTED_NO_SONAME "TRUE"
IMPORTED_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(${BINDINGS_NAME} INTERFACE $<TARGET_PROPERTY:${BINDINGS_NAME},DEFINE_SYMBOL>)
set(UTILS_SUBDIR "utils")
add_custom_command(
OUTPUT
${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h
${CMAKE_BINARY_DIR}/src/${UTILS_SUBDIR}/enum_string.c
COMMAND
${CMAKE_COMMAND} -DPROJECT_NAME=${PROJECT_NAME} -DLIBRARY_NAME=${LIBRARY_NAME} -DSUBDIR=${UTILS_SUBDIR} -P ${CMAKE_SOURCE_DIR}/cmake/enum-string-functions-gen.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h ${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h ${CMAKE_BINARY_DIR}/src/${UTILS_SUBDIR}/enum_string.c
MAIN_DEPENDENCY
${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
DEPENDS
${CMAKE_SOURCE_DIR}/cmake/enum-string-functions-gen.cmake
WORKING_DIRECTORY
${CMAKE_SOURCE_DIR}
COMMENT
"Generating the enum string functions with CMake..."
VERBATIM
)
add_custom_target(${LIBRARY_NAME}_utilities
DEPENDS ${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h
${CMAKE_BINARY_DIR}/src/${UTILS_SUBDIR}/enum_string.c
)
add_library(${LIBRARY_NAME})
target_compile_features(${LIBRARY_NAME} PRIVATE c_std_99)
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}
PUBLIC ${BINDINGS_NAME}
${LIBRARY_DEPENDENCIES}
)
# \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}
PUBLIC "$<BUILD_INTERFACE:${CBINDGEN_INCLUDEDIR};${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}>"
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
)
add_dependencies(${LIBRARY_NAME} ${BINDINGS_NAME}_artifacts)
# Generate 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}")
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
${CBINDGEN_TARGET_DIR}/config.h
@ONLY
NEWLINE_STYLE LF
)
target_sources(${LIBRARY_NAME}
PRIVATE
src/${UTILS_SUBDIR}/result.c
src/${UTILS_SUBDIR}/stack_callback_data.c
src/${UTILS_SUBDIR}/stack.c
src/${UTILS_SUBDIR}/string.c
${CMAKE_BINARY_DIR}/src/${UTILS_SUBDIR}/enum_string.c
PUBLIC
FILE_SET api TYPE HEADERS
BASE_DIRS
${CBINDGEN_INCLUDEDIR}
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}
FILES
${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/result.h
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/stack_callback_data.h
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/stack.h
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/string.h
INTERFACE
FILE_SET config TYPE HEADERS
BASE_DIRS
${CBINDGEN_INCLUDEDIR}
FILES
${CBINDGEN_TARGET_DIR}/config.h
)
install(
FILES ${CMAKE_BINARY_DIR}/config.h
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
TARGETS ${LIBRARY_NAME}
EXPORT ${PROJECT_NAME}-config
FILE_SET api
FILE_SET config
)
# \note Install the Cargo-built core bindings to enable direct linkage.
install(
FILES $<TARGET_PROPERTY:${BINDINGS_NAME},IMPORTED_LOCATION>
DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
install(EXPORT ${PROJECT_NAME}-config
FILE ${PROJECT_NAME}-config.cmake
NAMESPACE "${PROJECT_NAME}::"
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${LIB}
)
if(BUILD_TESTING)
@ -100,42 +300,6 @@ if(BUILD_TESTING)
enable_testing()
endif()
add_subdirectory(docs)
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"
crate-type = ["cdylib", "staticlib"]
name = "automerge_core"
crate-type = ["staticlib"]
bench = false
doc = false

View file

@ -1,22 +1,29 @@
automerge-c exposes an API to C that can either be used directly or as a basis
for other language bindings that have good support for calling into C functions.
# Overview
# Building
automerge-c exposes a C API that can either be used directly or as the basis
for other language bindings that have good support for calling C functions.
See the main README for instructions on getting your environment set up, then
you can use `./scripts/ci/cmake-build Release static` to build automerge-c.
# Installing
It will output two files:
See the main README for instructions on getting your environment set up and then
you can build the automerge-c library and install its constituent files within
a root directory of your choosing (e.g. "/usr/local") like so:
```shell
cmake -E make_directory automerge-c/build
cmake -S automerge-c -B automerge-c/build
cmake --build automerge-c/build
cmake --install automerge-c/build --prefix "/usr/local"
```
Installation is important because the name, location and structure of CMake's
out-of-source build subdirectory is subject to change based on the platform and
the release version; generated headers like `automerge-c/config.h` and
`automerge-c/utils/enum_string.h` are only sure to be found within their
installed locations.
- ./build/Cargo/target/include/automerge-c/automerge.h
- ./build/Cargo/target/release/libautomerge.a
To use these in your application you must arrange for your C compiler to find
these files, either by moving them to the right location on your computer, or
by configuring the compiler to reference these directories.
- `export LDFLAGS=-L./build/Cargo/target/release -lautomerge`
- `export CFLAGS=-I./build/Cargo/target/include`
It's not obvious because they are versioned but the `Cargo.toml` and
`cbindgen.toml` configuration files are also generated in order to ensure that
the project name, project version and library name that they contain match those
specified within the top-level `CMakeLists.txt` file.
If you'd like to cross compile the library for different platforms you can do so
using [cross](https://github.com/cross-rs/cross). For example:
@ -25,134 +32,176 @@ using [cross](https://github.com/cross-rs/cross). For example:
This will output a shared library in the directory `rust/target/aarch64-unknown-linux-gnu/release/`.
You can replace `aarch64-unknown-linux-gnu` with any [cross supported targets](https://github.com/cross-rs/cross#supported-targets). The targets below are known to work, though other targets are expected to work too:
You can replace `aarch64-unknown-linux-gnu` with any
[cross supported targets](https://github.com/cross-rs/cross#supported-targets).
The targets below are known to work, though other targets are expected to work
too:
- `x86_64-apple-darwin`
- `aarch64-apple-darwin`
- `x86_64-unknown-linux-gnu`
- `aarch64-unknown-linux-gnu`
As a caveat, the header file is currently 32/64-bit dependant. You can re-use it
for all 64-bit architectures, but you must generate a specific header for 32-bit
targets.
As a caveat, CMake generates the `automerge.h` header file in terms of the
processor architecture of the computer on which it was built so, for example,
don't use a header generated for a 64-bit processor if your target is a 32-bit
processor.
# Usage
For full reference, read through `automerge.h`, or to get started quickly look
at the
You can build and view the C API's HTML reference documentation like so:
```shell
cmake -E make_directory automerge-c/build
cmake -S automerge-c -B automerge-c/build
cmake --build automerge-c/build --target automerge_docs
firefox automerge-c/build/src/html/index.html
```
To get started quickly, look at the
[examples](https://github.com/automerge/automerge-rs/tree/main/rust/automerge-c/examples).
Almost all operations in automerge-c act on an AMdoc struct which you can get
from `AMcreate()` or `AMload()`. Operations on a given doc are not thread safe
so you must use a mutex or similar to avoid calling more than one function with
the same AMdoc pointer concurrently.
Almost all operations in automerge-c act on an Automerge document
(`AMdoc` struct) which is structurally similar to a JSON document.
As with all functions that either allocate memory, or could fail if given
invalid input, `AMcreate()` returns an `AMresult`. The `AMresult` contains the
returned doc (or error message), and must be freed with `AMfree()` after you are
done to avoid leaking memory.
You can get a document by calling either `AMcreate()` or `AMload()`. Operations
on a given document are not thread-safe so you must use a mutex or similar to
avoid calling more than one function on the same one concurrently.
A C API function that could succeed or fail returns a result (`AMresult` struct)
containing a status code (`AMstatus` enum) and either a sequence of at least one
item (`AMitem` struct) or a read-only view onto a UTF-8 error message string
(`AMbyteSpan` struct).
An item contains up to three components: an index within its parent object
(`AMbyteSpan` struct or `size_t`), a unique identifier (`AMobjId` struct) and a
value.
The result of a successful function call that doesn't produce any values will
contain a single item that is void (`AM_VAL_TYPE_VOID`).
A returned result **must** be passed to `AMresultFree()` once the item(s) or
error message it contains is no longer needed in order to avoid a memory leak.
```
#include <automerge-c/automerge.h>
#include <stdio.h>
#include <stdlib.h>
#include <automerge-c/automerge.h>
#include <automerge-c/utils/string.h>
int main(int argc, char** argv) {
AMresult *docResult = AMcreate(NULL);
if (AMresultStatus(docResult) != AM_STATUS_OK) {
printf("failed to create doc: %s", AMerrorMessage(docResult).src);
char* const err_msg = AMstrdup(AMresultError(docResult), NULL);
printf("failed to create doc: %s", err_msg);
free(err_msg);
goto cleanup;
}
AMdoc *doc = AMresultValue(docResult).doc;
AMdoc *doc;
AMitemToDoc(AMresultItem(docResult), &doc);
// useful code goes here!
cleanup:
AMfree(docResult);
AMresultFree(docResult);
}
```
If you are writing code in C directly, you can use the `AMpush()` helper
function to reduce the boilerplate of error handling and freeing for you (see
examples/quickstart.c).
If you are writing an application in C, the `AMstackItem()`, `AMstackItems()`
and `AMstackResult()` functions enable the lifetimes of anonymous results to be
centrally managed and allow the same validation logic to be reused without
relying upon the `goto` statement (see examples/quickstart.c).
If you are wrapping automerge-c in another language, particularly one that has a
garbage collector, you can call `AMfree` within a finalizer to ensure that memory
is reclaimed when it is no longer needed.
garbage collector, you can call the `AMresultFree()` function within a finalizer
to ensure that memory is reclaimed when it is no longer needed.
An AMdoc wraps an automerge document which are very similar to JSON documents.
Automerge documents consist of a mutable root, which is always a map from string
keys to values. Values can have the following types:
Automerge documents consist of a mutable root which is always a map from string
keys to values. A value can be one of the following types:
- A number of type double / int64_t / uint64_t
- An explicit true / false / nul
- An immutable utf-8 string (AMbyteSpan)
- An immutable array of arbitrary bytes (AMbyteSpan)
- A mutable map from string keys to values (AMmap)
- A mutable list of values (AMlist)
- A mutable string (AMtext)
- An explicit true / false / null
- An immutable UTF-8 string (`AMbyteSpan`).
- An immutable array of arbitrary bytes (`AMbyteSpan`).
- A mutable map from string keys to values.
- A mutable list of values.
- A mutable UTF-8 string.
If you read from a location in the document with no value a value with
`.tag == AM_VALUE_VOID` will be returned, but you cannot write such a value explicitly.
If you read from a location in the document with no value, an item with type
`AM_VAL_TYPE_VOID` will be returned, but you cannot write such a value
explicitly.
Under the hood, automerge references mutable objects by the internal object id,
and `AM_ROOT` is always the object id of the root value.
Under the hood, automerge references a mutable object by its object identifier
where `AM_ROOT` signifies a document's root map object.
There is a function to put each type of value into either a map or a list, and a
function to read the current value from a list. As (in general) collaborators
There are functions to put each type of value into either a map or a list, and
functions to read the current or a historical value from a map or a list. As (in general) collaborators
may edit the document at any time, you cannot guarantee that the type of the
value at a given part of the document will stay the same. As a result reading
from the document will return an `AMvalue` union that you can inspect to
determine its type.
value at a given part of the document will stay the same. As a result, reading
from the document will return an `AMitem` struct that you can inspect to
determine the type of value that it contains.
Strings in automerge-c are represented using an `AMbyteSpan` which contains a
pointer and a length. Strings must be valid utf-8 and may contain null bytes.
As a convenience you can use `AMstr()` to get the representation of a
null-terminated C string as an `AMbyteSpan`.
pointer and a length. Strings must be valid UTF-8 and may contain NUL (`0`)
characters.
For your convenience, you can call `AMstr()` to get the `AMbyteSpan` struct
equivalent of a null-terminated byte string or `AMstrdup()` to get the
representation of an `AMbyteSpan` struct as a null-terminated byte string
wherein its NUL characters have been removed/replaced as you choose.
Putting all of that together, to read and write from the root of the document
you can do this:
```
#include <automerge-c/automerge.h>
#include <stdio.h>
#include <stdlib.h>
#include <automerge-c/automerge.h>
#include <automerge-c/utils/string.h>
int main(int argc, char** argv) {
// ...previous example...
AMdoc *doc = AMresultValue(docResult).doc;
AMdoc *doc;
AMitemToDoc(AMresultItem(docResult), &doc);
AMresult *putResult = AMmapPutStr(doc, AM_ROOT, AMstr("key"), AMstr("value"));
if (AMresultStatus(putResult) != AM_STATUS_OK) {
printf("failed to put: %s", AMerrorMessage(putResult).src);
char* const err_msg = AMstrdup(AMresultError(putResult), NULL);
printf("failed to put: %s", err_msg);
free(err_msg);
goto cleanup;
}
AMresult *getResult = AMmapGet(doc, AM_ROOT, AMstr("key"), NULL);
if (AMresultStatus(getResult) != AM_STATUS_OK) {
printf("failed to get: %s", AMerrorMessage(getResult).src);
char* const err_msg = AMstrdup(AMresultError(putResult), NULL);
printf("failed to get: %s", err_msg);
free(err_msg);
goto cleanup;
}
AMvalue got = AMresultValue(getResult);
if (got.tag != AM_VALUE_STR) {
AMbyteSpan got;
if (AMitemToStr(AMresultItem(getResult), &got)) {
char* const c_str = AMstrdup(got, NULL);
printf("Got %zu-character string \"%s\"", got.count, c_str);
free(c_str);
} else {
printf("expected to read a string!");
goto cleanup;
}
printf("Got %zu-character string `%s`", got.str.count, got.str.src);
cleanup:
AMfree(getResult);
AMfree(putResult);
AMfree(docResult);
AMresultFree(getResult);
AMresultFree(putResult);
AMresultFree(docResult);
}
```
Functions that do not return an `AMresult` (for example `AMmapItemValue()`) do
not allocate memory, but continue to reference memory that was previously
allocated. It's thus important to keep the original `AMresult` alive (in this
case the one returned by `AMmapRange()`) until after you are done with the return
values of these functions.
Functions that do not return an `AMresult` (for example `AMitemKey()`) do
not allocate memory but rather reference memory that was previously
allocated. It's therefore important to keep the original `AMresult` alive (in
this case the one returned by `AMmapRange()`) until after you are finished with
the items that it contains. However, the memory for an individual `AMitem` can
be shared with a new `AMresult` by calling `AMitemResult()` on it. In other
words, a select group of items can be filtered out of a collection and only each
one's corresponding `AMresult` must be kept alive from that point forward; the
originating collection's `AMresult` can be safely freed.
Beyond that, good luck!

View file

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

View file

@ -0,0 +1,22 @@
[package]
name = "@PROJECT_NAME@"
version = "@PROJECT_VERSION@"
authors = ["Orion Henry <orion.henry@gmail.com>", "Jason Kankiewicz <jason.kankiewicz@gmail.com>"]
edition = "2021"
license = "MIT"
rust-version = "1.57.0"
[lib]
name = "@BINDINGS_NAME@"
crate-type = ["staticlib"]
bench = false
doc = false
[dependencies]
@LIBRARY_NAME@ = { path = "../@LIBRARY_NAME@" }
hex = "^0.4.3"
libc = "^0.2"
smol_str = "^0.1.21"
[build-dependencies]
cbindgen = "^0.24"

View file

@ -0,0 +1,48 @@
after_includes = """\n
/**
* \\defgroup enumerations Public Enumerations
* Symbolic names for integer constants.
*/
/**
* \\memberof AMdoc
* \\def AM_ROOT
* \\brief The root object of a document.
*/
#define AM_ROOT NULL
/**
* \\memberof AMdoc
* \\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.
*/
"""
documentation = true
documentation_style = "doxy"
include_guard = "@INCLUDE_GUARD_PREFIX@_H"
includes = []
language = "C"
line_length = 140
no_includes = true
style = "both"
sys_includes = ["stdbool.h", "stddef.h", "stdint.h", "time.h"]
usize_is_size_t = true
[enum]
derive_const_casts = true
enum_class = true
must_use = "MUST_USE_ENUM"
prefix_with_name = true
rename_variants = "ScreamingSnakeCase"
[export]
item_types = ["constants", "enums", "functions", "opaque", "structs", "typedefs"]

View file

@ -1,14 +1,35 @@
#ifndef @SYMBOL_PREFIX@_CONFIG_H
#define @SYMBOL_PREFIX@_CONFIG_H
/* This header is auto-generated by CMake. */
#ifndef @INCLUDE_GUARD_PREFIX@_CONFIG_H
#define @INCLUDE_GUARD_PREFIX@_CONFIG_H
/**
* \file
* \brief Configuration pararameters defined by the build system.
*
* \warning This file is auto-generated by CMake.
*/
/**
* \def @SYMBOL_PREFIX@_VERSION
* \brief Denotes a semantic version of the form {MAJOR}{MINOR}{PATCH} as three,
* two-digit decimal numbers without leading zeros (e.g. 100 is 0.1.0).
*/
#define @SYMBOL_PREFIX@_VERSION @INTEGER_PROJECT_VERSION@
/**
* \def @SYMBOL_PREFIX@_MAJOR_VERSION
* \brief Denotes a semantic major version as a decimal number.
*/
#define @SYMBOL_PREFIX@_MAJOR_VERSION (@SYMBOL_PREFIX@_VERSION / 100000)
/**
* \def @SYMBOL_PREFIX@_MINOR_VERSION
* \brief Denotes a semantic minor version as a decimal number.
*/
#define @SYMBOL_PREFIX@_MINOR_VERSION ((@SYMBOL_PREFIX@_VERSION / 100) % 1000)
/**
* \def @SYMBOL_PREFIX@_PATCH_VERSION
* \brief Denotes a semantic patch version as a decimal number.
*/
#define @SYMBOL_PREFIX@_PATCH_VERSION (@SYMBOL_PREFIX@_VERSION % 100)
#endif /* @SYMBOL_PREFIX@_CONFIG_H */
#endif /* @INCLUDE_GUARD_PREFIX@_CONFIG_H */

View file

@ -0,0 +1,183 @@
# This CMake script is used to generate a header and a source file for utility
# functions that convert the tags of generated enum types into strings and
# strings into the tags of generated enum types.
cmake_minimum_required(VERSION 3.23 FATAL_ERROR)
# Seeks the starting line of the source enum's declaration.
macro(seek_enum_mode)
if (line MATCHES "^(typedef[ \t]+)?enum ")
string(REGEX REPLACE "^enum ([0-9a-zA-Z_]+).*$" "\\1" enum_name "${line}")
set(mode "read_tags")
endif()
endmacro()
# Scans the input for the current enum's tags.
macro(read_tags_mode)
if(line MATCHES "^}")
set(mode "generate")
elseif(line MATCHES "^[A-Z0-9_]+.*$")
string(REGEX REPLACE "^([A-Za-z0-9_]+).*$" "\\1" tmp "${line}")
list(APPEND enum_tags "${tmp}")
endif()
endmacro()
macro(write_header_file)
# Generate a to-string function declaration.
list(APPEND header_body
"/**\n"
" * \\ingroup enumerations\n"
" * \\brief Gets the string representation of an `${enum_name}` enum tag.\n"
" *\n"
" * \\param[in] tag An `${enum_name}` enum tag.\n"
" * \\return A null-terminated byte string.\n"
" */\n"
"char const* ${enum_name}ToString(${enum_name} const tag)\;\n"
"\n")
# Generate a from-string function declaration.
list(APPEND header_body
"/**\n"
" * \\ingroup enumerations\n"
" * \\brief Gets an `${enum_name}` enum tag from its string representation.\n"
" *\n"
" * \\param[out] dest An `${enum_name}` enum tag pointer.\n"
" * \\param[in] src A null-terminated byte string.\n"
" * \\return `true` if \\p src matches the string representation of an\n"
" * `${enum_name}` enum tag, `false` otherwise.\n"
" */\n"
"bool ${enum_name}FromString(${enum_name}* dest, char const* const src)\;\n"
"\n")
endmacro()
macro(write_source_file)
# Generate a to-string function implementation.
list(APPEND source_body
"char const* ${enum_name}ToString(${enum_name} const tag) {\n"
" switch (tag) {\n"
" default:\n"
" return \"???\"\;\n")
foreach(label IN LISTS enum_tags)
list(APPEND source_body
" case ${label}:\n"
" return \"${label}\"\;\n")
endforeach()
list(APPEND source_body
" }\n"
"}\n"
"\n")
# Generate a from-string function implementation.
list(APPEND source_body
"bool ${enum_name}FromString(${enum_name}* dest, char const* const src) {\n")
foreach(label IN LISTS enum_tags)
list(APPEND source_body
" if (!strcmp(src, \"${label}\")) {\n"
" *dest = ${label}\;\n"
" return true\;\n"
" }\n")
endforeach()
list(APPEND source_body
" return false\;\n"
"}\n"
"\n")
endmacro()
function(main)
set(header_body "")
# File header and includes.
list(APPEND header_body
"#ifndef ${include_guard}\n"
"#define ${include_guard}\n"
"/**\n"
" * \\file\n"
" * \\brief Utility functions for converting enum tags into null-terminated\n"
" * byte strings and vice versa.\n"
" *\n"
" * \\warning This file is auto-generated by CMake.\n"
" */\n"
"\n"
"#include <stdbool.h>\n"
"\n"
"#include <${library_include}>\n"
"\n")
set(source_body "")
# File includes.
list(APPEND source_body
"/** \\warning This file is auto-generated by CMake. */\n"
"\n"
"#include \"stdio.h\"\n"
"#include \"string.h\"\n"
"\n"
"#include <${header_include}>\n"
"\n")
set(enum_name "")
set(enum_tags "")
set(mode "seek_enum")
file(STRINGS "${input_path}" lines)
foreach(line IN LISTS lines)
string(REGEX REPLACE "^(.+)(//.*)?" "\\1" line "${line}")
string(STRIP "${line}" line)
if(mode STREQUAL "seek_enum")
seek_enum_mode()
elseif(mode STREQUAL "read_tags")
read_tags_mode()
else()
# The end of the enum declaration was reached.
if(NOT enum_name)
# The end of the file was reached.
return()
endif()
if(NOT enum_tags)
message(FATAL_ERROR "No tags found for `${enum_name}`.")
endif()
string(TOLOWER "${enum_name}" output_stem_prefix)
string(CONCAT output_stem "${output_stem_prefix}" "_string")
cmake_path(REPLACE_EXTENSION output_stem "h" OUTPUT_VARIABLE output_header_basename)
write_header_file()
write_source_file()
set(enum_name "")
set(enum_tags "")
set(mode "seek_enum")
endif()
endforeach()
# File footer.
list(APPEND header_body
"#endif /* ${include_guard} */\n")
message(STATUS "Generating header file \"${output_header_path}\"...")
file(WRITE "${output_header_path}" ${header_body})
message(STATUS "Generating source file \"${output_source_path}\"...")
file(WRITE "${output_source_path}" ${source_body})
endfunction()
if(NOT DEFINED PROJECT_NAME)
message(FATAL_ERROR "Variable PROJECT_NAME is not defined.")
elseif(NOT DEFINED LIBRARY_NAME)
message(FATAL_ERROR "Variable LIBRARY_NAME is not defined.")
elseif(NOT DEFINED SUBDIR)
message(FATAL_ERROR "Variable SUBDIR is not defined.")
elseif(${CMAKE_ARGC} LESS 9)
message(FATAL_ERROR "Too few arguments.")
elseif(${CMAKE_ARGC} GREATER 10)
message(FATAL_ERROR "Too many arguments.")
elseif(NOT EXISTS ${CMAKE_ARGV5})
message(FATAL_ERROR "Input header \"${CMAKE_ARGV7}\" not found.")
endif()
cmake_path(CONVERT "${CMAKE_ARGV7}" TO_CMAKE_PATH_LIST input_path NORMALIZE)
cmake_path(CONVERT "${CMAKE_ARGV8}" TO_CMAKE_PATH_LIST output_header_path NORMALIZE)
cmake_path(CONVERT "${CMAKE_ARGV9}" TO_CMAKE_PATH_LIST output_source_path NORMALIZE)
string(TOLOWER "${PROJECT_NAME}" project_root)
cmake_path(CONVERT "${SUBDIR}" TO_CMAKE_PATH_LIST project_subdir NORMALIZE)
string(TOLOWER "${project_subdir}" project_subdir)
string(TOLOWER "${LIBRARY_NAME}" library_stem)
cmake_path(REPLACE_EXTENSION library_stem "h" OUTPUT_VARIABLE library_basename)
string(JOIN "/" library_include "${project_root}" "${library_basename}")
string(TOUPPER "${PROJECT_NAME}" project_name_upper)
string(TOUPPER "${project_subdir}" include_guard_infix)
string(REGEX REPLACE "/" "_" include_guard_infix "${include_guard_infix}")
string(REGEX REPLACE "-" "_" include_guard_prefix "${project_name_upper}")
string(JOIN "_" include_guard_prefix "${include_guard_prefix}" "${include_guard_infix}")
string(JOIN "/" output_header_prefix "${project_root}" "${project_subdir}")
cmake_path(GET output_header_path STEM output_header_stem)
string(TOUPPER "${output_header_stem}" include_guard_stem)
string(JOIN "_" include_guard "${include_guard_prefix}" "${include_guard_stem}" "H")
cmake_path(GET output_header_path FILENAME output_header_basename)
string(JOIN "/" header_include "${output_header_prefix}" "${output_header_basename}")
main()

View file

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

View file

@ -1,4 +1,6 @@
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
# 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)
if(NOT DEFINED CONDITION)
message(FATAL_ERROR "Variable \"CONDITION\" is not defined.")

View file

@ -0,0 +1,35 @@
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_CURRENT_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"
"${CBINDGEN_TARGET_DIR}/config.h"
"${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h"
"${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/result.h"
"${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/stack_callback_data.h"
"${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/stack.h"
"${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/string.h"
"${CMAKE_SOURCE_DIR}/README.md"
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 either outputs the
# file or depends upon it also or it will just output an error message
# when it can't be found.
add_dependencies(${LIBRARY_NAME}_docs ${BINDINGS_NAME}_artifacts ${LIBRARY_NAME}_utilities)
endif()

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,41 +1,39 @@
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
add_executable(
example_quickstart
${LIBRARY_NAME}_quickstart
quickstart.c
)
set_target_properties(example_quickstart PROPERTIES LINKER_LANGUAGE C)
set_target_properties(${LIBRARY_NAME}_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(
example_quickstart
${LIBRARY_NAME}_quickstart
PRIVATE "$<BUILD_INTERFACE:${CBINDGEN_INCLUDEDIR}>"
)
target_link_libraries(example_quickstart PRIVATE ${LIBRARY_NAME})
target_link_libraries(${LIBRARY_NAME}_quickstart PRIVATE ${LIBRARY_NAME})
add_dependencies(example_quickstart ${LIBRARY_NAME}_artifacts)
add_dependencies(${LIBRARY_NAME}_quickstart ${BINDINGS_NAME}_artifacts)
if(BUILD_SHARED_LIBS AND WIN32)
add_custom_command(
TARGET example_quickstart
TARGET ${LIBRARY_NAME}_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_CURRENT_BINARY_DIR}
${CMAKE_BINARY_DIR}
COMMENT "Copying the DLL built by Cargo into the examples directory..."
VERBATIM
)
endif()
add_custom_command(
TARGET example_quickstart
TARGET ${LIBRARY_NAME}_quickstart
POST_BUILD
COMMAND
example_quickstart
${LIBRARY_NAME}_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 example_quickstart
cmake --build automerge-c/build --target automerge_quickstart
```

View file

@ -3,152 +3,127 @@
#include <string.h>
#include <automerge-c/automerge.h>
#include <automerge-c/utils/enum_string.h>
#include <automerge-c/utils/stack.h>
#include <automerge-c/utils/stack_callback_data.h>
#include <automerge-c/utils/string.h>
static void abort_cb(AMresultStack**, uint8_t);
static bool abort_cb(AMstack**, void*);
/**
* \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, AMstr("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, AMstr("title"), AMstr("Rewrite everything in Clojure")));
AMfree(AMmapPutBool(doc1, card1, AMstr("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, AMstr("title"), AMstr("Rewrite everything in Haskell")));
AMfree(AMmapPutBool(doc1, card2, AMstr("done"), false));
AMfree(AMcommit(doc1, AMstr("Add card"), NULL));
AMstack* stack = NULL;
AMdoc* doc1;
AMitemToDoc(AMstackItem(&stack, AMcreate(NULL), abort_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc1);
AMobjId const* const cards =
AMitemObjId(AMstackItem(&stack, AMmapPutObject(doc1, AM_ROOT, AMstr("cards"), AM_OBJ_TYPE_LIST), abort_cb,
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
AMobjId const* const card1 =
AMitemObjId(AMstackItem(&stack, AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP), abort_cb,
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
AMstackItem(NULL, AMmapPutStr(doc1, card1, AMstr("title"), AMstr("Rewrite everything in Clojure")), abort_cb,
AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMmapPutBool(doc1, card1, AMstr("done"), false), abort_cb, AMexpect(AM_VAL_TYPE_VOID));
AMobjId const* const card2 =
AMitemObjId(AMstackItem(&stack, AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP), abort_cb,
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
AMstackItem(NULL, AMmapPutStr(doc1, card2, AMstr("title"), AMstr("Rewrite everything in Haskell")), abort_cb,
AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMmapPutBool(doc1, card2, AMstr("done"), false), abort_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMcommit(doc1, AMstr("Add card"), NULL), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMdoc* doc2 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, abort_cb).doc;
AMfree(AMmerge(doc2, doc1));
AMdoc* doc2;
AMitemToDoc(AMstackItem(&stack, AMcreate(NULL), abort_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc2);
AMstackItem(NULL, AMmerge(doc2, doc1), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
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;
AMbyteSpan binary;
AMitemToBytes(AMstackItem(&stack, AMsave(doc1), abort_cb, AMexpect(AM_VAL_TYPE_BYTES)), &binary);
AMitemToDoc(AMstackItem(&stack, AMload(binary.src, binary.count), abort_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc2);
AMfree(AMmapPutBool(doc1, card1, AMstr("done"), true));
AMfree(AMcommit(doc1, AMstr("Mark card as done"), NULL));
AMstackItem(NULL, AMmapPutBool(doc1, card1, AMstr("done"), true), abort_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMcommit(doc1, AMstr("Mark card as done"), NULL), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMfree(AMlistDelete(doc2, cards, 0));
AMfree(AMcommit(doc2, AMstr("Delete card"), NULL));
AMstackItem(NULL, AMlistDelete(doc2, cards, 0), abort_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMcommit(doc2, AMstr("Delete card"), NULL), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMfree(AMmerge(doc1, doc2));
AMstackItem(NULL, AMmerge(doc1, doc2), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
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;
AMbyteSpan const msg = AMchangeMessage(change);
char* const c_msg = calloc(1, msg.count + 1);
strncpy(c_msg, msg.src, msg.count);
printf("%s %ld\n", c_msg, AMobjSize(doc1, cards, &heads));
AMitems changes = AMstackItems(&stack, AMgetChanges(doc1, NULL), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE));
AMitem* item = NULL;
while ((item = AMitemsNext(&changes, 1)) != NULL) {
AMchange const* change;
AMitemToChange(item, &change);
AMitems const heads = AMstackItems(&stack, AMitemFromChangeHash(AMchangeHash(change)), abort_cb,
AMexpect(AM_VAL_TYPE_CHANGE_HASH));
char* const c_msg = AMstrdup(AMchangeMessage(change), NULL);
printf("%s %zu\n", c_msg, AMobjSize(doc1, cards, &heads));
free(c_msg);
}
AMfreeStack(&stack);
AMstackFree(&stack);
}
static char const* discriminant_suffix(AMvalueVariant const);
/**
* \brief Prints an error message to `stderr`, deallocates all results in the
* given stack and exits.
* \brief Examines the result at the top of the given stack and, if it's
* invalid, prints an error message to `stderr`, deallocates all results
* in the stack and exits.
*
* \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
* \param[in] discriminant An `AMvalueVariant` enum tag.
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] data A pointer to an owned `AMstackCallbackData` struct or `NULL`.
* \return `true` if the top `AMresult` in \p stack is valid, `false` otherwise.
* \pre \p stack `!= NULL`.
* \post `*stack == NULL`.
*/
static void abort_cb(AMresultStack** stack, uint8_t discriminant) {
static bool abort_cb(AMstack** stack, void* data) {
static char buffer[512] = {0};
char const* suffix = NULL;
if (!stack) {
suffix = "Stack*";
}
else if (!*stack) {
} else if (!*stack) {
suffix = "Stack";
}
else if (!(*stack)->result) {
} else if (!(*stack)->result) {
suffix = "";
}
if (suffix) {
fprintf(stderr, "Null `AMresult%s*`.", suffix);
AMfreeStack(stack);
fprintf(stderr, "Null `AMresult%s*`.\n", suffix);
AMstackFree(stack);
exit(EXIT_FAILURE);
return;
return false;
}
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);
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]) {
AMbyteSpan const msg = AMerrorMessage((*stack)->result);
char* const c_msg = calloc(1, msg.count + 1);
strncpy(c_msg, msg.src, msg.count);
fprintf(stderr, "%s; %s.", buffer, c_msg);
char* const c_msg = AMstrdup(AMresultError((*stack)->result), NULL);
fprintf(stderr, "%s; %s.\n", buffer, c_msg);
free(c_msg);
AMfreeStack(stack);
AMstackFree(stack);
exit(EXIT_FAILURE);
return;
return false;
}
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);
if (data) {
AMstackCallbackData* sc_data = (AMstackCallbackData*)data;
AMvalType const tag = AMitemValType(AMresultItem((*stack)->result));
if (tag != sc_data->bitmask) {
fprintf(stderr, "Unexpected tag `%s` (%d) instead of `%s` at %s:%d.\n", AMvalTypeToString(tag), tag,
AMvalTypeToString(sc_data->bitmask), sc_data->file, sc_data->line);
free(sc_data);
AMstackFree(stack);
exit(EXIT_FAILURE);
return false;
}
/**
* \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;
free(data);
return true;
}

View file

@ -0,0 +1,30 @@
#ifndef AUTOMERGE_C_UTILS_RESULT_H
#define AUTOMERGE_C_UTILS_RESULT_H
/**
* \file
* \brief Utility functions for use with `AMresult` structs.
*/
#include <stdarg.h>
#include <automerge-c/automerge.h>
/**
* \brief Transfers the items within an arbitrary list of results into a
* new result in their order of specification.
* \param[in] count The count of subsequent arguments.
* \param[in] ... A \p count list of arguments, each of which is a pointer to
* an `AMresult` struct whose items will be transferred out of it
* and which is subsequently freed.
* \return A pointer to an `AMresult` struct or `NULL`.
* \pre `𝑥 ` \p ... `, AMresultStatus(𝑥) == AM_STATUS_OK`
* \post `(𝑥 ` \p ... `, AMresultStatus(𝑥) != AM_STATUS_OK) -> NULL`
* \attention All `AMresult` struct pointer arguments are passed to
* `AMresultFree()` regardless of success; use `AMresultCat()`
* instead if you wish to pass them to `AMresultFree()` yourself.
* \warning The returned `AMresult` struct pointer must be passed to
* `AMresultFree()` in order to avoid a memory leak.
*/
AMresult* AMresultFrom(int count, ...);
#endif /* AUTOMERGE_C_UTILS_RESULT_H */

View file

@ -0,0 +1,130 @@
#ifndef AUTOMERGE_C_UTILS_STACK_H
#define AUTOMERGE_C_UTILS_STACK_H
/**
* \file
* \brief Utility data structures and functions for hiding `AMresult` structs,
* managing their lifetimes, and automatically applying custom
* validation logic to the `AMitem` structs that they contain.
*
* \note The `AMstack` struct and its related functions drastically reduce the
* need for boilerplate code and/or `goto` statement usage within a C
* application but a higher-level programming language offers even better
* ways to do the same things.
*/
#include <automerge-c/automerge.h>
/**
* \struct AMstack
* \brief A node in a singly-linked list of result pointers.
*/
typedef struct AMstack {
/** A result to be deallocated. */
AMresult* result;
/** The previous node in the singly-linked list or `NULL`. */
struct AMstack* prev;
} AMstack;
/**
* \memberof AMstack
* \brief The prototype of a function that examines the result at the top of
* the given stack in terms of some arbitrary data.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] data A pointer to arbitrary data or `NULL`.
* \return `true` if the top `AMresult` struct in \p stack is valid, `false`
* otherwise.
* \pre \p stack `!= NULL`.
*/
typedef bool (*AMstackCallback)(AMstack** stack, void* data);
/**
* \memberof AMstack
* \brief Deallocates the storage for a stack of results.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \pre \p stack `!= NULL`
* \post `*stack == NULL`
*/
void AMstackFree(AMstack** stack);
/**
* \memberof AMstack
* \brief Gets a result from the stack after removing it.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] result A pointer to the `AMresult` to be popped or `NULL` to
* select the top result in \p stack.
* \return A pointer to an `AMresult` struct or `NULL`.
* \pre \p stack `!= NULL`
* \warning The returned `AMresult` struct pointer must be passed to
* `AMresultFree()` in order to avoid a memory leak.
*/
AMresult* AMstackPop(AMstack** stack, AMresult const* result);
/**
* \memberof AMstack
* \brief Pushes the given result onto the given stack, calls the given
* callback with the given data to validate it and then either gets the
* result if it's valid or gets `NULL` instead.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] result A pointer to an `AMresult` struct.
* \param[in] callback A pointer to a function with the same signature as
* `AMstackCallback()` or `NULL`.
* \param[in] data A pointer to arbitrary data or `NULL` which is passed to
* \p callback.
* \return \p result or `NULL`.
* \warning If \p stack `== NULL` then \p result is deallocated in order to
* avoid a memory leak.
*/
AMresult* AMstackResult(AMstack** stack, AMresult* result, AMstackCallback callback, void* data);
/**
* \memberof AMstack
* \brief Pushes the given result onto the given stack, calls the given
* callback with the given data to validate it and then either gets the
* first item in the sequence of items within that result if it's valid
* or gets `NULL` instead.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] result A pointer to an `AMresult` struct.
* \param[in] callback A pointer to a function with the same signature as
* `AMstackCallback()` or `NULL`.
* \param[in] data A pointer to arbitrary data or `NULL` which is passed to
* \p callback.
* \return A pointer to an `AMitem` struct or `NULL`.
* \warning If \p stack `== NULL` then \p result is deallocated in order to
* avoid a memory leak.
*/
AMitem* AMstackItem(AMstack** stack, AMresult* result, AMstackCallback callback, void* data);
/**
* \memberof AMstack
* \brief Pushes the given result onto the given stack, calls the given
* callback with the given data to validate it and then either gets an
* `AMitems` struct over the sequence of items within that result if it's
* valid or gets an empty `AMitems` instead.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] result A pointer to an `AMresult` struct.
* \param[in] callback A pointer to a function with the same signature as
* `AMstackCallback()` or `NULL`.
* \param[in] data A pointer to arbitrary data or `NULL` which is passed to
* \p callback.
* \return An `AMitems` struct.
* \warning If \p stack `== NULL` then \p result is deallocated immediately
* in order to avoid a memory leak.
*/
AMitems AMstackItems(AMstack** stack, AMresult* result, AMstackCallback callback, void* data);
/**
* \memberof AMstack
* \brief Gets the count of results that have been pushed onto the stack.
*
* \param[in,out] stack A pointer to an `AMstack` struct.
* \return A 64-bit unsigned integer.
*/
size_t AMstackSize(AMstack const* const stack);
#endif /* AUTOMERGE_C_UTILS_STACK_H */

View file

@ -0,0 +1,53 @@
#ifndef AUTOMERGE_C_UTILS_PUSH_CALLBACK_DATA_H
#define AUTOMERGE_C_UTILS_PUSH_CALLBACK_DATA_H
/**
* \file
* \brief Utility data structures, functions and macros for supplying
* parameters to the custom validation logic applied to `AMitem`
* structs.
*/
#include <automerge-c/automerge.h>
/**
* \struct AMstackCallbackData
* \brief A data structure for passing the parameters of an item value test
* to an implementation of the `AMstackCallback` function prototype.
*/
typedef struct {
/** A bitmask of `AMvalType` tags. */
AMvalType bitmask;
/** A null-terminated file path string. */
char const* file;
/** The ordinal number of a line within a file. */
int line;
} AMstackCallbackData;
/**
* \memberof AMstackCallbackData
* \brief Allocates a new `AMstackCallbackData` struct and initializes its
* members from their corresponding arguments.
*
* \param[in] bitmask A bitmask of `AMvalType` tags.
* \param[in] file A null-terminated file path string.
* \param[in] line The ordinal number of a line within a file.
* \return A pointer to a disowned `AMstackCallbackData` struct.
* \warning The returned pointer must be passed to `free()` to avoid a memory
* leak.
*/
AMstackCallbackData* AMstackCallbackDataInit(AMvalType const bitmask, char const* const file, int const line);
/**
* \memberof AMstackCallbackData
* \def AMexpect
* \brief Allocates a new `AMstackCallbackData` struct and initializes it from
* an `AMvalueType` bitmask.
*
* \param[in] bitmask A bitmask of `AMvalType` tags.
* \return A pointer to a disowned `AMstackCallbackData` struct.
* \warning The returned pointer must be passed to `free()` to avoid a memory
* leak.
*/
#define AMexpect(bitmask) AMstackCallbackDataInit(bitmask, __FILE__, __LINE__)
#endif /* AUTOMERGE_C_UTILS_PUSH_CALLBACK_DATA_H */

View file

@ -0,0 +1,29 @@
#ifndef AUTOMERGE_C_UTILS_STRING_H
#define AUTOMERGE_C_UTILS_STRING_H
/**
* \file
* \brief Utility functions for use with `AMbyteSpan` structs that provide
* UTF-8 string views.
*/
#include <automerge-c/automerge.h>
/**
* \memberof AMbyteSpan
* \brief Returns a pointer to a null-terminated byte string which is a
* duplicate of the given UTF-8 string view except for the substitution
* of its NUL (0) characters with the specified null-terminated byte
* string.
*
* \param[in] str A UTF-8 string view as an `AMbyteSpan` struct.
* \param[in] nul A null-terminated byte string to substitute for NUL characters
* or `NULL` to substitute `"\\0"` for NUL characters.
* \return A disowned null-terminated byte string.
* \pre \p str.src `!= NULL`
* \pre \p str.count `<= sizeof(`\p str.src `)`
* \warning The returned pointer must be passed to `free()` to avoid a memory
* leak.
*/
char* AMstrdup(AMbyteSpan const str, char const* nul);
#endif /* AUTOMERGE_C_UTILS_STRING_H */

View file

@ -1,250 +0,0 @@
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()

View file

@ -1,4 +1,5 @@
use automerge as am;
use libc::c_int;
use std::cell::RefCell;
use std::cmp::Ordering;
use std::str::FromStr;
@ -11,7 +12,7 @@ macro_rules! to_actor_id {
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::err("Invalid AMactorId pointer").into(),
None => return AMresult::error("Invalid `AMactorId*`").into(),
}
}};
}
@ -57,11 +58,11 @@ impl AsRef<am::ActorId> for AMactorId {
}
/// \memberof AMactorId
/// \brief Gets the value of an actor identifier as a sequence of bytes.
/// \brief Gets the value of an actor identifier as an array of bytes.
///
/// \param[in] actor_id A pointer to an `AMactorId` struct.
/// \pre \p actor_id `!= NULL`.
/// \return An `AMbyteSpan` struct.
/// \return An `AMbyteSpan` struct for an array of bytes.
/// \pre \p actor_id `!= NULL`
/// \internal
///
/// # Safety
@ -82,8 +83,8 @@ pub unsafe extern "C" fn AMactorIdBytes(actor_id: *const AMactorId) -> AMbyteSpa
/// \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`.
/// \pre \p actor_id1 `!= NULL`
/// \pre \p actor_id2 `!= NULL`
/// \internal
///
/// #Safety
@ -93,7 +94,7 @@ pub unsafe extern "C" fn AMactorIdBytes(actor_id: *const AMactorId) -> AMbyteSpa
pub unsafe extern "C" fn AMactorIdCmp(
actor_id1: *const AMactorId,
actor_id2: *const AMactorId,
) -> isize {
) -> c_int {
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,
@ -101,65 +102,69 @@ pub unsafe extern "C" fn AMactorIdCmp(
Ordering::Greater => 1,
},
(None, Some(_)) => -1,
(Some(_), None) => 1,
(None, None) => 0,
(Some(_), None) => 1,
}
}
/// \memberof AMactorId
/// \brief Allocates a new actor identifier and initializes it with a random
/// UUID.
/// \brief Allocates a new actor identifier and initializes it from a random
/// UUID value.
///
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_ACTOR_ID` item.
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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.
/// \brief Allocates a new actor identifier and initializes it from an array of
/// bytes value.
///
/// \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.
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The count of bytes to copy from the array pointed to by
/// \p src.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_ACTOR_ID` 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.
/// \internal
///
/// # Safety
/// src must be a byte array of size `>= count`
/// src must be a byte array of length `>= 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);
pub unsafe extern "C" fn AMactorIdFromBytes(src: *const u8, count: usize) -> *mut AMresult {
if !src.is_null() {
let value = std::slice::from_raw_parts(src, count);
to_result(Ok::<am::ActorId, am::InvalidActorId>(am::ActorId::from(
slice,
value,
)))
} else {
AMresult::error("Invalid uint8_t*").into()
}
}
/// \memberof AMactorId
/// \brief Allocates a new actor identifier and initializes it from a
/// hexadecimal string.
/// hexadecimal UTF-8 string view value.
///
/// \param[in] hex_str A UTF-8 string view as an `AMbyteSpan` struct.
/// \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.
/// \param[in] value A UTF-8 string view as an `AMbyteSpan` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_ACTOR_ID` item.
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
/// hex_str must be a valid pointer to an AMbyteSpan
#[no_mangle]
pub unsafe extern "C" fn AMactorIdInitStr(hex_str: AMbyteSpan) -> *mut AMresult {
pub unsafe extern "C" fn AMactorIdFromStr(value: AMbyteSpan) -> *mut AMresult {
use am::AutomergeError::InvalidActorId;
to_result(match (&hex_str).try_into() {
to_result(match (&value).try_into() {
Ok(s) => match am::ActorId::from_str(s) {
Ok(actor_id) => Ok(actor_id),
Err(_) => Err(InvalidActorId(String::from(s))),
@ -169,11 +174,12 @@ pub unsafe extern "C" fn AMactorIdInitStr(hex_str: AMbyteSpan) -> *mut AMresult
}
/// \memberof AMactorId
/// \brief Gets the value of an actor identifier as a hexadecimal string.
/// \brief Gets the value of an actor identifier as a UTF-8 hexadecimal string
/// view.
///
/// \param[in] actor_id A pointer to an `AMactorId` struct.
/// \pre \p actor_id `!= NULL`.
/// \return A UTF-8 string view as an `AMbyteSpan` struct.
/// \pre \p actor_id `!= NULL`
/// \internal
///
/// # Safety

View file

@ -1,14 +1,17 @@
use automerge as am;
use libc::strlen;
use std::cmp::Ordering;
use std::convert::TryFrom;
use std::os::raw::c_char;
use libc::{c_int, strlen};
use smol_str::SmolStr;
macro_rules! to_str {
($span:expr) => {{
let result: Result<&str, am::AutomergeError> = (&$span).try_into();
($byte_span:expr) => {{
let result: Result<&str, am::AutomergeError> = (&$byte_span).try_into();
match result {
Ok(s) => s,
Err(e) => return AMresult::err(&e.to_string()).into(),
Err(e) => return AMresult::error(&e.to_string()).into(),
}
}};
}
@ -17,16 +20,17 @@ pub(crate) use to_str;
/// \struct AMbyteSpan
/// \installed_headerfile
/// \brief A view onto a contiguous sequence of bytes.
/// \brief A view onto an array of bytes.
#[repr(C)]
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.
/// A pointer to the first byte of an array of bytes.
/// \warning \p src is only valid until the array of bytes to which it
/// points is freed.
/// \note If the `AMbyteSpan` came from within an `AMitem` struct then
/// \p src will be freed when the pointer to the `AMresult` struct
/// containing the `AMitem` struct is passed to `AMresultFree()`.
pub src: *const u8,
/// The number of bytes in the array.
/// The count of bytes in the array.
pub count: usize,
}
@ -52,9 +56,7 @@ impl PartialEq for AMbyteSpan {
} else if self.src == other.src {
return true;
}
let slice = unsafe { std::slice::from_raw_parts(self.src, self.count) };
let other_slice = unsafe { std::slice::from_raw_parts(other.src, other.count) };
slice == other_slice
<&[u8]>::from(self) == <&[u8]>::from(other)
}
}
@ -72,10 +74,15 @@ impl From<&am::ActorId> for AMbyteSpan {
impl From<&mut am::ActorId> for AMbyteSpan {
fn from(actor: &mut am::ActorId) -> Self {
let slice = actor.to_bytes();
actor.as_ref().into()
}
}
impl From<&am::ChangeHash> for AMbyteSpan {
fn from(change_hash: &am::ChangeHash) -> Self {
Self {
src: slice.as_ptr(),
count: slice.len(),
src: change_hash.0.as_ptr(),
count: change_hash.0.len(),
}
}
}
@ -93,12 +100,9 @@ impl From<*const c_char> for AMbyteSpan {
}
}
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<&SmolStr> for AMbyteSpan {
fn from(smol_str: &SmolStr) -> Self {
smol_str.as_bytes().into()
}
}
@ -111,13 +115,39 @@ impl From<&[u8]> for AMbyteSpan {
}
}
impl From<&AMbyteSpan> for &[u8] {
fn from(byte_span: &AMbyteSpan) -> Self {
unsafe { std::slice::from_raw_parts(byte_span.src, byte_span.count) }
}
}
impl From<&AMbyteSpan> for Vec<u8> {
fn from(byte_span: &AMbyteSpan) -> Self {
<&[u8]>::from(byte_span).to_vec()
}
}
impl TryFrom<&AMbyteSpan> for am::ChangeHash {
type Error = am::AutomergeError;
fn try_from(byte_span: &AMbyteSpan) -> Result<Self, Self::Error> {
use am::AutomergeError::InvalidChangeHashBytes;
let slice: &[u8] = byte_span.into();
match slice.try_into() {
Ok(change_hash) => Ok(change_hash),
Err(e) => Err(InvalidChangeHashBytes(e)),
}
}
}
impl TryFrom<&AMbyteSpan> for &str {
type Error = am::AutomergeError;
fn try_from(span: &AMbyteSpan) -> Result<Self, Self::Error> {
fn try_from(byte_span: &AMbyteSpan) -> Result<Self, Self::Error> {
use am::AutomergeError::InvalidCharacter;
let slice = unsafe { std::slice::from_raw_parts(span.src, span.count) };
let slice = byte_span.into();
match std::str::from_utf8(slice) {
Ok(str_) => Ok(str_),
Err(e) => Err(InvalidCharacter(e.valid_up_to())),
@ -125,17 +155,69 @@ impl TryFrom<&AMbyteSpan> for &str {
}
}
/// \brief Creates an AMbyteSpan from a pointer + length
/// \memberof AMbyteSpan
/// \brief Creates a view onto an array of bytes.
///
/// \param[in] src A pointer to a span of bytes
/// \param[in] count The number of bytes in the span
/// \return An `AMbyteSpan` struct
/// \param[in] src A pointer to an array of bytes or `NULL`.
/// \param[in] count The count of bytes to view from the array pointed to by
/// \p src.
/// \return An `AMbyteSpan` struct.
/// \pre \p count `<= sizeof(`\p src `)`
/// \post `(`\p src `== NULL) -> (AMbyteSpan){NULL, 0}`
/// \internal
///
/// #Safety
/// AMbytes does not retain the underlying storage, so you must discard the
/// return value before freeing the bytes.
/// src must be a byte array of length `>= count` or `std::ptr::null()`
#[no_mangle]
pub unsafe extern "C" fn AMbytes(src: *const u8, count: usize) -> AMbyteSpan {
AMbyteSpan { src, count }
AMbyteSpan {
src,
count: if src.is_null() { 0 } else { count },
}
}
/// \memberof AMbyteSpan
/// \brief Creates a view onto a C string.
///
/// \param[in] c_str A null-terminated byte string or `NULL`.
/// \return An `AMbyteSpan` struct.
/// \pre Each byte in \p c_str encodes one UTF-8 character.
/// \internal
///
/// #Safety
/// c_str must be a null-terminated array of `std::os::raw::c_char` or `std::ptr::null()`.
#[no_mangle]
pub unsafe extern "C" fn AMstr(c_str: *const c_char) -> AMbyteSpan {
c_str.into()
}
/// \memberof AMbyteSpan
/// \brief Compares two UTF-8 string views lexicographically.
///
/// \param[in] lhs A UTF-8 string view as an `AMbyteSpan` struct.
/// \param[in] rhs A UTF-8 string view as an `AMbyteSpan` struct.
/// \return Negative value if \p lhs appears before \p rhs in lexicographical order.
/// Zero if \p lhs and \p rhs compare equal.
/// Positive value if \p lhs appears after \p rhs in lexicographical order.
/// \pre \p lhs.src `!= NULL`
/// \pre \p lhs.count `<= sizeof(`\p lhs.src `)`
/// \pre \p rhs.src `!= NULL`
/// \pre \p rhs.count `<= sizeof(`\p rhs.src `)`
/// \internal
///
/// #Safety
/// lhs.src must be a byte array of length >= lhs.count
/// rhs.src must be a a byte array of length >= rhs.count
#[no_mangle]
pub unsafe extern "C" fn AMstrCmp(lhs: AMbyteSpan, rhs: AMbyteSpan) -> c_int {
match (<&str>::try_from(&lhs), <&str>::try_from(&rhs)) {
(Ok(lhs), Ok(rhs)) => match lhs.cmp(rhs) {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
},
(Err(_), Ok(_)) => -1,
(Err(_), Err(_)) => 0,
(Ok(_), Err(_)) => 1,
}
}

View file

@ -2,7 +2,6 @@ use automerge as am;
use std::cell::RefCell;
use crate::byte_span::AMbyteSpan;
use crate::change_hashes::AMchangeHashes;
use crate::result::{to_result, AMresult};
macro_rules! to_change {
@ -10,7 +9,7 @@ macro_rules! to_change {
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::err("Invalid AMchange pointer").into(),
None => return AMresult::error("Invalid `AMchange*`").into(),
}
}};
}
@ -21,14 +20,14 @@ macro_rules! to_change {
#[derive(Eq, PartialEq)]
pub struct AMchange {
body: *mut am::Change,
changehash: RefCell<Option<am::ChangeHash>>,
change_hash: RefCell<Option<am::ChangeHash>>,
}
impl AMchange {
pub fn new(change: &mut am::Change) -> Self {
Self {
body: change,
changehash: Default::default(),
change_hash: Default::default(),
}
}
@ -40,12 +39,12 @@ impl AMchange {
}
pub fn hash(&self) -> AMbyteSpan {
let mut changehash = self.changehash.borrow_mut();
if let Some(changehash) = changehash.as_ref() {
changehash.into()
let mut change_hash = self.change_hash.borrow_mut();
if let Some(change_hash) = change_hash.as_ref() {
change_hash.into()
} else {
let hash = unsafe { (*self.body).hash() };
let ptr = changehash.insert(hash);
let ptr = change_hash.insert(hash);
AMbyteSpan {
src: ptr.0.as_ptr(),
count: hash.as_ref().len(),
@ -70,11 +69,10 @@ 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.
/// \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.
/// \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.
/// \internal
///
/// # Safety
@ -90,8 +88,8 @@ pub unsafe extern "C" fn AMchangeActorId(change: *const AMchange) -> *mut AMresu
/// \memberof AMchange
/// \brief Compresses the raw bytes of a change.
///
/// \param[in,out] change A pointer to an `AMchange` struct.
/// \pre \p change `!= NULL`.
/// \param[in] change A pointer to an `AMchange` struct.
/// \pre \p change `!= NULL`
/// \internal
///
/// # Safety
@ -107,18 +105,20 @@ 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 `AMchangeHashes` struct or `NULL`.
/// \pre \p change `!= NULL`.
/// \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.
/// \internal
///
/// # Safety
/// change must be a valid pointer to an AMchange
#[no_mangle]
pub unsafe extern "C" fn AMchangeDeps(change: *const AMchange) -> AMchangeHashes {
match change.as_ref() {
Some(change) => AMchangeHashes::new(change.as_ref().deps()),
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(),
}
})
}
/// \memberof AMchange
@ -126,7 +126,7 @@ pub unsafe extern "C" fn AMchangeDeps(change: *const AMchange) -> AMchangeHashes
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return An `AMbyteSpan` struct.
/// \pre \p change `!= NULL`.
/// \pre \p change `!= NULL`
/// \internal
///
/// # Safety
@ -141,32 +141,33 @@ pub unsafe extern "C" fn AMchangeExtraBytes(change: *const AMchange) -> AMbyteSp
}
/// \memberof AMchange
/// \brief Loads a sequence of bytes into a change.
/// \brief Allocates a new change and initializes it from an array of bytes value.
///
/// \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 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.
/// \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.
/// \internal
///
/// # Safety
/// src must be a byte array of size `>= count`
/// src must be a byte array of length `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMchangeFromBytes(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::Change::from_bytes(data))
let data = std::slice::from_raw_parts(src, count);
to_result(am::Change::from_bytes(data.to_vec()))
}
/// \memberof AMchange
/// \brief Gets the hash of a change.
///
/// \param[in] change A pointer to an `AMchange` struct.
/// \return A change hash as an `AMbyteSpan` struct.
/// \pre \p change `!= NULL`.
/// \return An `AMbyteSpan` struct for a change hash.
/// \pre \p change `!= NULL`
/// \internal
///
/// # Safety
@ -183,8 +184,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 A boolean.
/// \pre \p change `!= NULL`.
/// \return `true` if \p change is empty, `false` otherwise.
/// \pre \p change `!= NULL`
/// \internal
///
/// # Safety
@ -198,12 +199,37 @@ 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
@ -221,8 +247,8 @@ 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 A UTF-8 string view as an `AMbyteSpan` struct.
/// \pre \p change `!= NULL`.
/// \return An `AMbyteSpan` struct for a UTF-8 string.
/// \pre \p change `!= NULL`
/// \internal
///
/// # Safety
@ -240,7 +266,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
@ -259,7 +285,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
@ -267,10 +293,9 @@ 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() {
change.as_ref().len()
} else {
0
return change.as_ref().len();
}
0
}
/// \memberof AMchange
@ -278,7 +303,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
@ -297,7 +322,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
@ -315,8 +340,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.
/// \pre \p change `!= NULL`.
/// \return An `AMbyteSpan` struct for an array of bytes.
/// \pre \p change `!= NULL`
/// \internal
///
/// # Safety
@ -329,28 +354,3 @@ pub unsafe extern "C" fn AMchangeRawBytes(change: *const AMchange) -> AMbyteSpan
Default::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

@ -1,400 +0,0 @@
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();
}
}
Default::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();
}
}
Default::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 {
Default::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 {
Default::default()
}
}

View file

@ -1,399 +0,0 @@
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 {
Default::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 {
Default::default()
}
}

View file

@ -6,43 +6,23 @@ use std::ops::{Deref, DerefMut};
use crate::actor_id::{to_actor_id, AMactorId};
use crate::byte_span::{to_str, AMbyteSpan};
use crate::change_hashes::AMchangeHashes;
use crate::items::AMitems;
use crate::obj::{to_obj_id, AMobjId, AMobjType};
use crate::result::{to_result, AMresult, AMvalue};
use crate::result::{to_result, AMresult};
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_doc, to_doc_mut};
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)
}};
}
use crate::doc::utils::{clamp, to_doc, to_doc_mut, to_items};
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(),
None => return AMresult::error("Invalid `AMsyncState*`").into(),
}
}};
}
@ -57,6 +37,10 @@ impl AMdoc {
pub fn new(auto_commit: am::AutoCommit) -> Self {
Self(auto_commit)
}
pub fn is_equal_to(&mut self, other: &mut Self) -> bool {
self.document().get_heads() == other.document().get_heads()
}
}
impl AsRef<am::AutoCommit> for AMdoc {
@ -82,38 +66,38 @@ impl DerefMut for AMdoc {
/// \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.
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] items A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE`
/// items.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p items `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// changes must be a valid pointer to an AMchanges.
/// items must be a valid pointer to an AMitems.
#[no_mangle]
pub unsafe extern "C" fn AMapplyChanges(
doc: *mut AMdoc,
changes: *const AMchanges,
) -> *mut AMresult {
pub unsafe extern "C" fn AMapplyChanges(doc: *mut AMdoc, items: *const AMitems) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let changes = to_changes!(changes);
to_result(doc.apply_changes(changes.as_ref().to_vec()))
let items = to_items!(items);
match Vec::<am::Change>::try_from(items) {
Ok(changes) => to_result(doc.apply_changes(changes)),
Err(e) => AMresult::error(&e.to_string()).into(),
}
}
/// \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.
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_DOC` item.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -129,10 +113,9 @@ pub unsafe extern "C" fn AMclone(doc: *const AMdoc) -> *mut AMresult {
///
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_DOC` item.
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -149,15 +132,15 @@ pub unsafe extern "C" fn AMcreate(actor_id: *const AMactorId) -> *mut AMresult {
/// \brief Commits the current operations on a document with an optional
/// message and/or *nix timestamp (milliseconds).
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] message A UTF-8 string view as an `AMbyteSpan` struct.
/// \param[in] timestamp A pointer to a 64-bit integer or `NULL`.
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
/// with one element if there were operations to commit, or void if
/// there were no operations to commit.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \return A pointer to an `AMresult` struct with one `AM_VAL_TYPE_CHANGE_HASH`
/// item if there were operations to commit or an `AM_VAL_TYPE_VOID` item
/// if there were no operations to commit.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -183,24 +166,24 @@ pub unsafe extern "C" fn AMcommit(
/// \brief Creates an empty change with an optional message and/or *nix
/// timestamp (milliseconds).
///
/// This is useful if you wish to create a "merge commit" which has as its
/// dependents the current heads of the document but you don't have any
/// operations to add to the document.
/// \details This is useful if you wish to create a "merge commit" which has as
/// its dependents the current heads of the document but you don't have
/// any operations to add to the document.
///
/// \note If there are outstanding uncommitted changes to the document
/// then two changes will be created: one for creating the outstanding changes
/// and one for the empty change. The empty change will always be the
/// latest change in the document after this call and the returned hash will be
/// the hash of that empty change.
/// then two changes will be created: one for creating the outstanding
/// changes and one for the empty change. The empty change will always be
/// the latest change in the document after this call and the returned
/// hash will be the hash of that empty change.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] message A UTF-8 string view as an `AMbyteSpan` struct.
/// \param[in] timestamp A pointer to a 64-bit integer 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.
/// \return A pointer to an `AMresult` struct with one `AM_VAL_TYPE_CHANGE_HASH`
/// item.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -226,11 +209,11 @@ pub unsafe extern "C" fn AMemptyChange(
/// \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.
/// \param[in] doc1 A pointer to an `AMdoc` struct.
/// \param[in] doc2 A pointer to an `AMdoc` struct.
/// \return `true` if \p doc1 `==` \p doc2 and `false` otherwise.
/// \pre \p doc1 `!= NULL`.
/// \pre \p doc2 `!= NULL`.
/// \pre \p doc1 `!= NULL`
/// \pre \p doc2 `!= NULL`
/// \internal
///
/// #Safety
@ -239,33 +222,36 @@ pub unsafe extern "C" fn AMemptyChange(
#[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,
(Some(doc1), Some(doc2)) => doc1.is_equal_to(doc2),
(None, None) | (None, Some(_)) | (Some(_), None) => false,
}
}
/// \memberof AMdoc
/// \brief Forks this document at the current or a historical point for use by
/// \brief Forks this document at its 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.
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select a historical point or `NULL` to select its
/// current point.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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()
/// heads must be a valid pointer to an AMitems or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMfork(doc: *mut AMdoc, heads: *const AMchangeHashes) -> *mut AMresult {
pub unsafe extern "C" fn AMfork(doc: *mut AMdoc, heads: *const AMitems) -> *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())),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => to_result(doc.fork_at(&heads)),
Err(e) => AMresult::error(&e.to_string()).into(),
},
}
}
@ -273,14 +259,14 @@ pub unsafe extern "C" fn AMfork(doc: *mut AMdoc, heads: *const AMchangeHashes) -
/// \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.
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] sync_state A pointer to an `AMsyncState` struct.
/// \return A pointer to an `AMresult` struct with either an
/// `AM_VAL_TYPE_SYNC_MESSAGE` or `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p sync_state `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -300,11 +286,10 @@ pub unsafe extern "C" fn AMgenerateSyncMessage(
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_ACTOR_ID` item.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -320,20 +305,22 @@ pub unsafe extern "C" fn AMgetActorId(doc: *const AMdoc) -> *mut AMresult {
/// \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] 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.
/// \param[in] count The count of bytes to copy from the array pointed to by
/// \p src.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_CHANGE` item.
/// \pre \p doc `!= NULL`
/// \pre \p src `!= NULL`
/// \pre `sizeof(`\p src') >= AM_CHANGE_HASH_SIZE`
/// \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
/// doc must be a valid pointer to an AMdoc
/// src must be a byte array of size `>= automerge::types::HASH_SIZE`
/// src must be a byte array of length `>= automerge::types::HASH_SIZE`
#[no_mangle]
pub unsafe extern "C" fn AMgetChangeByHash(
doc: *mut AMdoc,
@ -344,48 +331,48 @@ pub unsafe extern "C" fn AMgetChangeByHash(
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(),
Err(e) => AMresult::error(&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.
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] have_deps A pointer to an `AMitems` struct with
/// `AM_VAL_TYPE_CHANGE_HASH` items or `NULL`.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE` items.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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 {
pub unsafe extern "C" fn AMgetChanges(doc: *mut AMdoc, have_deps: *const AMitems) -> *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,
Some(have_deps) => match Vec::<am::ChangeHash>::try_from(have_deps) {
Ok(change_hashes) => change_hashes,
Err(e) => return AMresult::error(&e.to_string()).into(),
},
None => Vec::<am::ChangeHash>::new(),
};
to_result(doc.get_changes(have_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.
/// \param[in] doc1 A pointer to an `AMdoc` struct.
/// \param[in] doc2 A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE` items.
/// \pre \p doc1 `!= NULL`
/// \pre \p doc2 `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -401,12 +388,11 @@ pub unsafe extern "C" fn AMgetChangesAdded(doc1: *mut AMdoc, doc2: *mut AMdoc) -
/// \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.
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -423,41 +409,42 @@ pub unsafe extern "C" fn AMgetHeads(doc: *mut AMdoc) -> *mut AMresult {
/// \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.
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items or `NULL`.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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()
/// heads must be a valid pointer to an AMitems or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMgetMissingDeps(
doc: *mut AMdoc,
heads: *const AMchangeHashes,
) -> *mut AMresult {
pub unsafe extern "C" fn AMgetMissingDeps(doc: *mut AMdoc, heads: *const AMitems) -> *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,
None => Vec::<am::ChangeHash>::new(),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => heads,
Err(e) => {
return AMresult::error(&e.to_string()).into();
}
},
};
to_result(doc.get_missing_deps(heads))
to_result(doc.get_missing_deps(heads.as_slice()))
}
/// \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.
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing either an
/// `AM_VAL_TYPE_CHANGE` or `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -473,29 +460,33 @@ pub unsafe extern "C" fn AMgetLastLocalChange(doc: *mut AMdoc) -> *mut AMresult
///
/// \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.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select historical keys or `NULL` to select current
/// keys.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_STR` items.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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()
/// heads must be a valid pointer to an AMitems or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMkeys(
doc: *const AMdoc,
obj_id: *const AMobjId,
heads: *const AMchangeHashes,
heads: *const AMitems,
) -> *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())),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => to_result(doc.keys_at(obj_id, &heads)),
Err(e) => AMresult::error(&e.to_string()).into(),
},
}
}
@ -504,42 +495,43 @@ pub unsafe extern "C" fn AMkeys(
/// 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.
/// \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_DOC` 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.
/// \internal
///
/// # Safety
/// src must be a byte array of size `>= count`
/// src must be a byte array of length `>= 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))
let data = 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] 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.
/// \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_UINT` item.
/// \pre \p doc `!= NULL`
/// \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
/// doc must be a valid pointer to an AMdoc
/// src must be a byte array of size `>= count`
/// src must be a byte array of length `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMloadIncremental(
doc: *mut AMdoc,
@ -547,23 +539,21 @@ pub unsafe extern "C" fn AMloadIncremental(
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))
let data = 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.
/// \param[in] dest A pointer to an `AMdoc` struct.
/// \param[in] src A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct with `AM_VAL_TYPE_CHANGE_HASH` items.
/// \pre \p dest `!= NULL`
/// \pre \p src `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -580,40 +570,47 @@ pub unsafe extern "C" fn AMmerge(dest: *mut AMdoc, src: *mut AMdoc) -> *mut AMre
///
/// \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`.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select a historical size or `NULL` to select its
/// current size.
/// \return The count of items in the object identified by \p obj_id.
/// \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()
/// heads must be a valid pointer to an AMitems or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMobjSize(
doc: *const AMdoc,
obj_id: *const AMobjId,
heads: *const AMchangeHashes,
heads: *const AMitems,
) -> 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()),
None => {
return doc.length(obj_id);
}
Some(heads) => {
if let Ok(heads) = <Vec<am::ChangeHash>>::try_from(heads) {
return doc.length_at(obj_id, &heads);
}
}
}
}
} else {
0
}
}
/// \memberof AMdoc
/// \brief Gets the type 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`.
/// \return An `AMobjType`.
/// \pre \p doc `!= NULL`.
/// \return An `AMobjType` tag or `0`.
/// \pre \p doc `!= NULL`
/// \pre \p obj_id `!= NULL`
/// \internal
///
/// # Safety
@ -623,44 +620,45 @@ pub unsafe extern "C" fn AMobjSize(
pub unsafe extern "C" fn AMobjObjType(doc: *const AMdoc, obj_id: *const AMobjId) -> AMobjType {
if let Some(doc) = doc.as_ref() {
let obj_id = to_obj_id!(obj_id);
match doc.object_type(obj_id) {
Err(_) => AMobjType::Void,
Ok(obj_type) => obj_type.into(),
if let Ok(obj_type) = doc.object_type(obj_id) {
return (&obj_type).into();
}
} else {
AMobjType::Void
}
Default::default()
}
/// \memberof AMdoc
/// \brief Gets the current or historical values of an object within its entire
/// range.
/// \brief Gets the current or historical items of an entire 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
/// 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.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select its historical items or `NULL` to select
/// its current items.
/// \return A pointer to an `AMresult` struct with an `AMitems` struct.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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()
/// heads must be a valid pointer to an AMitems or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMobjValues(
pub unsafe extern "C" fn AMobjItems(
doc: *const AMdoc,
obj_id: *const AMobjId,
heads: *const AMchangeHashes,
heads: *const AMitems,
) -> *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())),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => to_result(doc.values_at(obj_id, &heads)),
Err(e) => AMresult::error(&e.to_string()).into(),
},
}
}
@ -670,7 +668,7 @@ pub unsafe extern "C" fn AMobjValues(
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return The count of pending operations for \p doc.
/// \pre \p doc `!= NULL`.
/// \pre \p doc `!= NULL`
/// \internal
///
/// # Safety
@ -678,23 +676,22 @@ pub unsafe extern "C" fn AMobjValues(
#[no_mangle]
pub unsafe extern "C" fn AMpendingOps(doc: *const AMdoc) -> usize {
if let Some(doc) = doc.as_ref() {
doc.pending_ops()
} else {
0
return doc.pending_ops();
}
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] doc A pointer to an `AMdoc` struct.
/// \param[in] 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`.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p sync_state `!= NULL`
/// \pre \p sync_message `!= NULL`
/// \internal
///
/// # Safety
@ -720,9 +717,9 @@ pub unsafe extern "C" fn AMreceiveSyncMessage(
/// \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.
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return The count of pending operations for \p doc that were cancelled.
/// \pre \p doc `!= NULL`.
/// \pre \p doc `!= NULL`
/// \internal
///
/// # Safety
@ -730,21 +727,19 @@ pub unsafe extern "C" fn AMreceiveSyncMessage(
#[no_mangle]
pub unsafe extern "C" fn AMrollback(doc: *mut AMdoc) -> usize {
if let Some(doc) = doc.as_mut() {
doc.rollback()
} else {
0
return doc.rollback();
}
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.
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_BYTES` item.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -759,12 +754,11 @@ pub unsafe extern "C" fn AMsave(doc: *mut AMdoc) -> *mut AMresult {
/// \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.
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_BYTES` item.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -778,13 +772,13 @@ pub unsafe extern "C" fn AMsaveIncremental(doc: *mut AMdoc) -> *mut AMresult {
/// \memberof AMdoc
/// \brief Puts the actor identifier of a document.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] 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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p actor_id `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -805,76 +799,65 @@ pub unsafe extern "C" fn AMsetActorId(
/// \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] 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
/// \param[in] del The number of values 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.
/// \param[in] values A copy of an `AMitems` struct from which values will be
/// spliced <b>starting at its current position</b>; call
/// `AMitemsRewound()` on a used `AMitems` first to ensure
/// that all of its values are spliced in. Pass `(AMitems){0}`
/// when zero values should be spliced in.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \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 pointer must be passed to
/// `AMresultFree()` in order to avoid 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()
/// values must be a valid pointer to an AMitems 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,
values: AMitems,
) -> *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);
let pos = clamp!(pos, len, "pos");
let del = clamp!(del, len, "del");
match Vec::<am::ScalarValue>::try_from(&values) {
Ok(vals) => to_result(doc.splice(obj_id, pos, del, vals)),
Err(e) => AMresult::error(&e.to_string()).into(),
}
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] 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 view as an `AMbyteSpan` struct.
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \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 pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -891,8 +874,8 @@ pub unsafe extern "C" fn AMspliceText(
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 pos = clamp!(pos, len, "pos");
let del = clamp!(del, len, "del");
to_result(doc.splice_text(obj_id, pos, del, to_str!(text)))
}
@ -901,28 +884,32 @@ pub unsafe extern "C" fn AMspliceText(
///
/// \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.
/// \param[in] heads A pointer to an `AMitems` struct containing
/// `AM_VAL_TYPE_CHANGE_HASH` items to select a historical string
/// or `NULL` to select the current string.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_STR` item.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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()
/// heads must be a valid pointer to an AMitems or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMtext(
doc: *const AMdoc,
obj_id: *const AMobjId,
heads: *const AMchangeHashes,
heads: *const AMitems,
) -> *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())),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => to_result(doc.text_at(obj_id, &heads)),
Err(e) => AMresult::error(&e.to_string()).into(),
},
}
}

View file

@ -3,47 +3,44 @@ use automerge::transaction::Transactable;
use automerge::ReadDoc;
use crate::byte_span::{to_str, AMbyteSpan};
use crate::change_hashes::AMchangeHashes;
use crate::doc::{to_doc, to_doc_mut, to_obj_id, AMdoc};
use crate::obj::{to_obj_type, AMobjId, AMobjType};
use crate::doc::{to_doc, to_doc_mut, AMdoc};
use crate::items::AMitems;
use crate::obj::{to_obj_id, to_obj_type, AMobjId, AMobjType};
use crate::result::{to_result, AMresult};
pub mod item;
pub mod items;
macro_rules! adjust {
($index:expr, $insert:expr, $len:expr) => {{
($pos: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();
if $pos > end && $pos != usize::MAX {
return AMresult::error(&format!("Invalid pos {}", $pos)).into();
}
(std::cmp::min($index, end), insert)
(std::cmp::min($pos, end), insert)
}};
}
macro_rules! to_range {
($begin:expr, $end:expr) => {{
if $begin > $end {
return AMresult::err(&format!("Invalid range [{}-{})", $begin, $end)).into();
return AMresult::error(&format!("Invalid range [{}-{})", $begin, $end)).into();
};
($begin..$end)
}};
}
/// \memberof AMdoc
/// \brief Deletes an index in a list object.
/// \brief Deletes an item from a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \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.
/// \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.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -53,101 +50,109 @@ macro_rules! to_range {
pub unsafe extern "C" fn AMlistDelete(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
pos: 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))
let (pos, _) = adjust!(pos, false, doc.length(obj_id));
to_result(doc.delete(obj_id, pos))
}
/// \memberof AMdoc
/// \brief Gets the current or historical value at an index in a list object.
/// \brief Gets a current or historical item within 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.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select a historical item at \p pos or `NULL`
/// to select the current item at \p pos.
/// \return A pointer to an `AMresult` struct with an `AMitem` struct.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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()
/// heads must be a valid pointer to an AMitems or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistGet(
doc: *const AMdoc,
obj_id: *const AMobjId,
index: usize,
heads: *const AMchangeHashes,
pos: usize,
heads: *const AMitems,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, _) = adjust!(index, false, doc.length(obj_id));
to_result(match heads.as_ref() {
None => doc.get(obj_id, index),
Some(heads) => doc.get_at(obj_id, index, heads.as_ref()),
})
let (pos, _) = adjust!(pos, false, doc.length(obj_id));
match heads.as_ref() {
None => to_result(doc.get(obj_id, pos)),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => to_result(doc.get_at(obj_id, pos, &heads)),
Err(e) => AMresult::error(&e.to_string()).into(),
},
}
}
/// \memberof AMdoc
/// \brief Gets all of the historical values at an index in a list object until
/// its current one or a specific one.
/// \brief Gets all of the historical items at a position within 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.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select a historical last item or `NULL` to select
/// the current last item.
/// \return A pointer to an `AMresult` struct with an `AMitems` struct.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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()
/// heads must be a valid pointer to an AMitems or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistGetAll(
doc: *const AMdoc,
obj_id: *const AMobjId,
index: usize,
heads: *const AMchangeHashes,
pos: usize,
heads: *const AMitems,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, _) = adjust!(index, false, doc.length(obj_id));
let (pos, _) = adjust!(pos, 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())),
None => to_result(doc.get_all(obj_id, pos)),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => to_result(doc.get_all_at(obj_id, pos, &heads)),
Err(e) => AMresult::error(&e.to_string()).into(),
},
}
}
/// \memberof AMdoc
/// \brief Increments a counter at an index in a list object by the given
/// value.
/// \brief Increments a counter value in an item within a list object by the
/// given value.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \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] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item.
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -157,32 +162,33 @@ pub unsafe extern "C" fn AMlistGetAll(
pub unsafe extern "C" fn AMlistIncrement(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
pos: 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))
let (pos, _) = adjust!(pos, false, doc.length(obj_id));
to_result(doc.increment(obj_id, pos, value))
}
/// \memberof AMdoc
/// \brief Puts a boolean as the value at an index in a list object.
/// \brief Puts a boolean value into an item within a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \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 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] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -192,84 +198,85 @@ pub unsafe extern "C" fn AMlistIncrement(
pub unsafe extern "C" fn AMlistPutBool(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
pos: 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 (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
let value = am::ScalarValue::Boolean(value);
to_result(if insert {
doc.insert(obj_id, index, value)
doc.insert(obj_id, pos, value)
} else {
doc.put(obj_id, index, value)
doc.put(obj_id, pos, value)
})
}
/// \memberof AMdoc
/// \brief Puts a sequence of bytes as the value at an index in a list object.
/// \brief Puts an array of bytes value at a position within a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \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 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.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \param[in] value A view onto the array of bytes to copy from as an
/// `AMbyteSpan` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \pre \p value.src `!= NULL`
/// \pre `0 <` \p value.count `<= sizeof(`\p value.src `)`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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`
/// value.src must be a byte array of length >= value.count
#[no_mangle]
pub unsafe extern "C" fn AMlistPutBytes(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
pos: usize,
insert: bool,
val: AMbyteSpan,
value: AMbyteSpan,
) -> *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(val.src, val.count));
let (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
let value: Vec<u8> = (&value).into();
to_result(if insert {
doc.insert(obj_id, index, value)
doc.insert(obj_id, pos, value)
} else {
doc.put(obj_id, index, value)
doc.put(obj_id, pos, value)
})
}
/// \memberof AMdoc
/// \brief Puts a CRDT counter as the value at an index in a list object.
/// \brief Puts a CRDT counter value into an item within a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \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 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] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -279,38 +286,39 @@ pub unsafe extern "C" fn AMlistPutBytes(
pub unsafe extern "C" fn AMlistPutCounter(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
pos: 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 (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
let value = am::ScalarValue::Counter(value.into());
to_result(if insert {
doc.insert(obj_id, index, value)
doc.insert(obj_id, pos, value)
} else {
doc.put(obj_id, index, value)
doc.put(obj_id, pos, value)
})
}
/// \memberof AMdoc
/// \brief Puts a float as the value at an index in a list object.
/// \brief Puts a float value into an item within a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \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 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] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -320,37 +328,38 @@ pub unsafe extern "C" fn AMlistPutCounter(
pub unsafe extern "C" fn AMlistPutF64(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
pos: 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));
let (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
to_result(if insert {
doc.insert(obj_id, index, value)
doc.insert(obj_id, pos, value)
} else {
doc.put(obj_id, index, value)
doc.put(obj_id, pos, value)
})
}
/// \memberof AMdoc
/// \brief Puts a signed integer as the value at an index in a list object.
/// \brief Puts a signed integer value into an item within a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \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 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] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -360,36 +369,37 @@ pub unsafe extern "C" fn AMlistPutF64(
pub unsafe extern "C" fn AMlistPutInt(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
pos: 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 (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
to_result(if insert {
doc.insert(obj_id, index, value)
doc.insert(obj_id, pos, value)
} else {
doc.put(obj_id, index, value)
doc.put(obj_id, pos, value)
})
}
/// \memberof AMdoc
/// \brief Puts null as the value at an index in a list object.
/// \brief Puts a null value into an item within a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \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 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.
/// \param[in] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -399,38 +409,37 @@ pub unsafe extern "C" fn AMlistPutInt(
pub unsafe extern "C" fn AMlistPutNull(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
pos: 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));
let (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
to_result(if insert {
doc.insert(obj_id, index, ())
doc.insert(obj_id, pos, ())
} else {
doc.put(obj_id, index, ())
doc.put(obj_id, pos, ())
})
}
/// \memberof AMdoc
/// \brief Puts an empty object as the value at an index in a list object.
/// \brief Puts an empty object value into an item within a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \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 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] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \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`.
/// \pre \p obj_type != `AM_OBJ_TYPE_VOID`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_OBJ_TYPE` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -440,82 +449,85 @@ pub unsafe extern "C" fn AMlistPutNull(
pub unsafe extern "C" fn AMlistPutObject(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
pos: 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 = to_obj_type!(obj_type);
let (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
let obj_type = to_obj_type!(obj_type);
to_result(if insert {
doc.insert_object(obj_id, index, object)
(doc.insert_object(obj_id, pos, obj_type), obj_type)
} else {
doc.put_object(obj_id, index, object)
(doc.put_object(obj_id, pos, obj_type), obj_type)
})
}
/// \memberof AMdoc
/// \brief Puts a UTF-8 string as the value at an index in a list object.
/// \brief Puts a UTF-8 string value into an item within a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \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 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] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \param[in] value A UTF-8 string view as an `AMbyteSpan` struct.
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \pre \p value.src `!= NULL`
/// \pre `0 <` \p value.count `<= sizeof(`\p value.src `)`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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`
/// value.src must be a byte array of length >= value.count
#[no_mangle]
pub unsafe extern "C" fn AMlistPutStr(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
pos: usize,
insert: bool,
value: AMbyteSpan,
) -> *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 (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
let value = to_str!(value);
to_result(if insert {
doc.insert(obj_id, index, value)
doc.insert(obj_id, pos, value)
} else {
doc.put(obj_id, index, value)
doc.put(obj_id, pos, value)
})
}
/// \memberof AMdoc
/// \brief Puts a *nix timestamp (milliseconds) as the value at an index in a
/// \brief Puts a *nix timestamp (milliseconds) value into an item within a
/// list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \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 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] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -525,38 +537,39 @@ pub unsafe extern "C" fn AMlistPutStr(
pub unsafe extern "C" fn AMlistPutTimestamp(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
pos: 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 (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
let value = am::ScalarValue::Timestamp(value);
to_result(if insert {
doc.insert(obj_id, index, value)
doc.insert(obj_id, pos, value)
} else {
doc.put(obj_id, index, value)
doc.put(obj_id, pos, value)
})
}
/// \memberof AMdoc
/// \brief Puts an unsigned integer as the value at an index in a list object.
/// \brief Puts an unsigned integer value into an item within a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \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 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] pos The position of an item within the list object identified by
/// \p obj_id or `SIZE_MAX` to indicate its last item if
/// \p insert `== false` or one past its last item if
/// \p insert `== true`.
/// \param[in] insert A flag for inserting a new item for \p value before
/// \p pos instead of putting \p value into the item at
/// \p pos.
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id `)` or \p pos `== SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid a memory leak.
/// \internal
///
/// # Safety
@ -566,56 +579,58 @@ pub unsafe extern "C" fn AMlistPutTimestamp(
pub unsafe extern "C" fn AMlistPutUint(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
pos: 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));
let (pos, insert) = adjust!(pos, insert, doc.length(obj_id));
to_result(if insert {
doc.insert(obj_id, index, value)
doc.insert(obj_id, pos, value)
} else {
doc.put(obj_id, index, value)
doc.put(obj_id, pos, value)
})
}
/// \memberof AMdoc
/// \brief Gets the current or historical indices and values of the list object
/// within the given range.
/// \brief Gets the current or historical items in 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.
/// \param[in] begin The first pos in a range of indices.
/// \param[in] end At least one past the last pos in a range of indices.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select historical items or `NULL` to select
/// current items.
/// \return A pointer to an `AMresult` struct with an `AMitems` struct.
/// \pre \p doc `!= NULL`
/// \pre \p begin `<=` \p end `<= SIZE_MAX`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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()
/// heads must be a valid pointer to an AMitems 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,
heads: *const AMitems,
) -> *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())),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => to_result(doc.list_range_at(obj_id, range, &heads)),
Err(e) => AMresult::error(&e.to_string()).into(),
},
}
}

View file

@ -1,97 +0,0 @@
use automerge as am;
use crate::obj::AMobjId;
use crate::result::AMvalue;
/// \struct AMlistItem
/// \installed_headerfile
/// \brief An item in a list object.
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>,
}
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,
}
}
}
impl PartialEq for AMlistItem {
fn eq(&self, other: &Self) -> bool {
self.index == other.index && self.obj_id == other.obj_id && self.value == other.value
}
}
/*
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).into()
} else {
AMvalue::Void
}
}

View file

@ -1,348 +0,0 @@
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 {
Default::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 {
Default::default()
}
}

View file

@ -3,31 +3,29 @@ use automerge::transaction::Transactable;
use automerge::ReadDoc;
use crate::byte_span::{to_str, AMbyteSpan};
use crate::change_hashes::AMchangeHashes;
use crate::doc::{to_doc, to_doc_mut, to_obj_id, AMdoc};
use crate::obj::{to_obj_type, AMobjId, AMobjType};
use crate::doc::{to_doc, to_doc_mut, AMdoc};
use crate::items::AMitems;
use crate::obj::{to_obj_id, to_obj_type, AMobjId, AMobjType};
use crate::result::{to_result, AMresult};
pub mod item;
pub mod items;
/// \memberof AMdoc
/// \brief Deletes a key in a map object.
/// \brief Deletes an item from a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \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 view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \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.
/// \param[in] key The UTF-8 string view key of an item within the map object
/// identified by \p obj_id as an `AMbyteSpan` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapDelete(
doc: *mut AMdoc,
@ -40,96 +38,107 @@ pub unsafe extern "C" fn AMmapDelete(
}
/// \memberof AMdoc
/// \brief Gets the current or historical value for a key in a map object.
/// \brief Gets a current or historical item within 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 view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \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.
/// \param[in] key The UTF-8 string view key of an item within the map object
/// identified by \p obj_id as an `AMbyteSpan` struct.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select a historical item at \p key or `NULL`
/// to select the current item at \p key.
/// \return A pointer to an `AMresult` struct with an `AMitem` struct.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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()
/// key.src must be a byte array of length >= key.count
/// heads must be a valid pointer to an AMitems or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMmapGet(
doc: *const AMdoc,
obj_id: *const AMobjId,
key: AMbyteSpan,
heads: *const AMchangeHashes,
heads: *const AMitems,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let key = to_str!(key);
match heads.as_ref() {
None => to_result(doc.get(obj_id, key)),
Some(heads) => to_result(doc.get_at(obj_id, key, heads.as_ref())),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => to_result(doc.get_at(obj_id, key, &heads)),
Err(e) => AMresult::error(&e.to_string()).into(),
},
}
}
/// \memberof AMdoc
/// \brief Gets all of the historical values for a key in a map object until
/// \brief Gets all of the historical items at a key within 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 view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \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.
/// \param[in] key The UTF-8 string view key of an item within the map object
/// identified by \p obj_id as an `AMbyteSpan` struct.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select a historical last item or `NULL` to
/// select the current last item.
/// \return A pointer to an `AMresult` struct with an `AMItems` struct.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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()
/// key.src must be a byte array of length >= key.count
/// heads must be a valid pointer to an AMitems or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMmapGetAll(
doc: *const AMdoc,
obj_id: *const AMobjId,
key: AMbyteSpan,
heads: *const AMchangeHashes,
heads: *const AMitems,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let key = to_str!(key);
match heads.as_ref() {
None => to_result(doc.get_all(obj_id, key)),
Some(heads) => to_result(doc.get_all_at(obj_id, key, heads.as_ref())),
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => to_result(doc.get_all_at(obj_id, key, &heads)),
Err(e) => AMresult::error(&e.to_string()).into(),
},
}
}
/// \memberof AMdoc
/// \brief Increments a counter for a key in a map object by the given value.
/// \brief Increments a counter at a key in a map object by the given value.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \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 view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \param[in] key The UTF-8 string view key of an item within the map object
/// identified by \p obj_id as an `AMbyteSpan` struct.
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapIncrement(
doc: *mut AMdoc,
@ -145,21 +154,22 @@ pub unsafe extern "C" fn AMmapIncrement(
/// \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] 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 view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \param[in] key The UTF-8 string view key of an item within the map object
/// identified by \p obj_id as an `AMbyteSpan` struct.
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutBool(
doc: *mut AMdoc,
@ -173,59 +183,58 @@ pub unsafe extern "C" fn AMmapPutBool(
}
/// \memberof AMdoc
/// \brief Puts a sequence of bytes as the value of a key in a map object.
/// \brief Puts an array of bytes value at a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \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 view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \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.
/// \param[in] key The UTF-8 string view key of an item within the map object
/// identified by \p obj_id as an `AMbyteSpan` struct.
/// \param[in] value A view onto an array of bytes as an `AMbyteSpan` struct.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= NULL`
/// \pre \p value.src `!= NULL`
/// \pre `0 <` \p value.count `<= sizeof(`\p value.src `)`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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`
/// key.src must be a byte array of length >= key.count
/// value.src must be a byte array of length >= value.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutBytes(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: AMbyteSpan,
val: AMbyteSpan,
value: AMbyteSpan,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let key = to_str!(key);
let mut vec = Vec::new();
vec.extend_from_slice(std::slice::from_raw_parts(val.src, val.count));
to_result(doc.put(to_obj_id!(obj_id), key, vec))
to_result(doc.put(to_obj_id!(obj_id), key, Vec::<u8>::from(&value)))
}
/// \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] 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 view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutCounter(
doc: *mut AMdoc,
@ -245,20 +254,21 @@ pub unsafe extern "C" fn AMmapPutCounter(
/// \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] 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 view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutNull(
doc: *mut AMdoc,
@ -273,23 +283,22 @@ pub unsafe extern "C" fn AMmapPutNull(
/// \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] 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 view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \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`.
/// \pre \p obj_type != `AM_OBJ_TYPE_VOID`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_OBJ_TYPE` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutObject(
doc: *mut AMdoc,
@ -299,27 +308,29 @@ pub unsafe extern "C" fn AMmapPutObject(
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let key = to_str!(key);
to_result(doc.put_object(to_obj_id!(obj_id), key, to_obj_type!(obj_type)))
let obj_type = to_obj_type!(obj_type);
to_result((doc.put_object(to_obj_id!(obj_id), key, obj_type), obj_type))
}
/// \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] 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 view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutF64(
doc: *mut AMdoc,
@ -335,21 +346,22 @@ pub unsafe extern "C" fn AMmapPutF64(
/// \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] 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 view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutInt(
doc: *mut AMdoc,
@ -365,21 +377,22 @@ pub unsafe extern "C" fn AMmapPutInt(
/// \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] 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 view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \param[in] value A UTF-8 string view as an `AMbyteSpan` struct.
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutStr(
doc: *mut AMdoc,
@ -395,21 +408,22 @@ pub unsafe extern "C" fn AMmapPutStr(
/// \brief Puts a *nix timestamp (milliseconds) as the value of a key in a map
/// object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \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 view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutTimestamp(
doc: *mut AMdoc,
@ -425,21 +439,22 @@ pub unsafe extern "C" fn AMmapPutTimestamp(
/// \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] 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 view key for the map object identified by
/// \p obj_id as an `AMbyteSpan` struct.
/// \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.
/// \return A pointer to an `AMresult` struct with an `AM_VAL_TYPE_VOID` item.
/// \pre \p doc `!= NULL`
/// \pre \p key.src `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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.src must be a byte array of length >= key.count
#[no_mangle]
pub unsafe extern "C" fn AMmapPutUint(
doc: *mut AMdoc,
@ -453,71 +468,82 @@ pub unsafe extern "C" fn AMmapPutUint(
}
/// \memberof AMdoc
/// \brief Gets the current or historical keys and values of the map object
/// within the given range.
/// \brief Gets the current or historical items 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 `AMstr(NULL)` to indicate the
/// absolute first key.
/// \param[in] end The key one past the last key in a subrange or `AMstr(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`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \param[in] end The key one past the last key in a subrange or `AMstr(NULL)`
/// to indicate one past the absolute last key.
/// \param[in] heads A pointer to an `AMitems` struct with `AM_VAL_TYPE_CHANGE_HASH`
/// items to select historical items or `NULL` to select
/// current items.
/// \return A pointer to an `AMresult` struct with an `AMitems` struct.
/// \pre \p doc `!= NULL`
/// \warning The returned `AMresult` struct pointer must be passed to
/// `AMresultFree()` in order to avoid 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()
/// begin.src must be a byte array of length >= begin.count or std::ptr::null()
/// end.src must be a byte array of length >= end.count or std::ptr::null()
/// heads must be a valid pointer to an AMitems or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMmapRange(
doc: *const AMdoc,
obj_id: *const AMobjId,
begin: AMbyteSpan,
end: AMbyteSpan,
heads: *const AMchangeHashes,
heads: *const AMitems,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let heads = match heads.as_ref() {
None => None,
Some(heads) => match <Vec<am::ChangeHash>>::try_from(heads) {
Ok(heads) => Some(heads),
Err(e) => {
return AMresult::error(&e.to_string()).into();
}
},
};
match (begin.is_null(), end.is_null()) {
(false, false) => {
let (begin, end) = (to_str!(begin).to_string(), to_str!(end).to_string());
if begin > end {
return AMresult::err(&format!("Invalid range [{}-{})", begin, end)).into();
return AMresult::error(&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()))
if let Some(heads) = heads {
to_result(doc.map_range_at(obj_id, bounds, &heads))
} else {
to_result(doc.map_range(obj_id, bounds))
}
}
(false, true) => {
let bounds = to_str!(begin).to_string()..;
if let Some(heads) = heads.as_ref() {
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
if let Some(heads) = heads {
to_result(doc.map_range_at(obj_id, bounds, &heads))
} else {
to_result(doc.map_range(obj_id, bounds))
}
}
(true, false) => {
let bounds = ..to_str!(end).to_string();
if let Some(heads) = heads.as_ref() {
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
if let Some(heads) = heads {
to_result(doc.map_range_at(obj_id, bounds, &heads))
} else {
to_result(doc.map_range(obj_id, bounds))
}
}
(true, true) => {
let bounds = ..;
if let Some(heads) = heads.as_ref() {
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
if let Some(heads) = heads {
to_result(doc.map_range_at(obj_id, bounds, &heads))
} else {
to_result(doc.map_range(obj_id, bounds))
}

View file

@ -1,98 +0,0 @@
use automerge as am;
use crate::byte_span::AMbyteSpan;
use crate::obj::AMobjId;
use crate::result::AMvalue;
/// \struct AMmapItem
/// \installed_headerfile
/// \brief An item in a map object.
pub struct AMmapItem {
/// The key of an item in a map object.
key: String,
/// 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>,
}
impl AMmapItem {
pub fn new(key: &'static str, value: am::Value<'static>, obj_id: am::ObjId) -> Self {
Self {
key: key.to_string(),
obj_id: AMobjId::new(obj_id),
value,
}
}
}
impl PartialEq for AMmapItem {
fn eq(&self, other: &Self) -> bool {
self.key == other.key && self.obj_id == other.obj_id && self.value == other.value
}
}
/*
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 An `AMbyteSpan` view of a UTF-8 string.
/// \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) -> AMbyteSpan {
if let Some(map_item) = map_item.as_ref() {
map_item.key.as_bytes().into()
} else {
Default::default()
}
}
/// \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).into()
} else {
AMvalue::Void
}
}

View file

@ -1,340 +0,0 @@
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 {
Default::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 {
Default::default()
}
}

View file

@ -1,9 +1,20 @@
macro_rules! clamp {
($index:expr, $len:expr, $param_name:expr) => {{
if $index > $len && $index != usize::MAX {
return AMresult::error(&format!("Invalid {} {}", $param_name, $index)).into();
}
std::cmp::min($index, $len)
}};
}
pub(crate) use clamp;
macro_rules! to_doc {
($handle:expr) => {{
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::err("Invalid AMdoc pointer").into(),
None => return AMresult::error("Invalid `AMdoc*`").into(),
}
}};
}
@ -15,9 +26,21 @@ macro_rules! to_doc_mut {
let handle = $handle.as_mut();
match handle {
Some(b) => b,
None => return AMresult::err("Invalid AMdoc pointer").into(),
None => return AMresult::error("Invalid `AMdoc*`").into(),
}
}};
}
pub(crate) use to_doc_mut;
macro_rules! to_items {
($handle:expr) => {{
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::error("Invalid `AMitems*`").into(),
}
}};
}
pub(crate) use to_items;

View file

@ -0,0 +1,84 @@
use automerge as am;
use std::any::type_name;
use smol_str::SmolStr;
use crate::byte_span::AMbyteSpan;
/// \struct AMindex
/// \installed_headerfile
/// \brief An item index.
#[derive(PartialEq)]
pub enum AMindex {
/// A UTF-8 string key variant.
Key(SmolStr),
/// A 64-bit unsigned integer position variant.
Pos(usize),
}
impl TryFrom<&AMindex> for AMbyteSpan {
type Error = am::AutomergeError;
fn try_from(item: &AMindex) -> Result<Self, Self::Error> {
use am::AutomergeError::InvalidValueType;
use AMindex::*;
if let Key(key) = item {
return Ok(key.into());
}
Err(InvalidValueType {
expected: type_name::<SmolStr>().to_string(),
unexpected: type_name::<usize>().to_string(),
})
}
}
impl TryFrom<&AMindex> for usize {
type Error = am::AutomergeError;
fn try_from(item: &AMindex) -> Result<Self, Self::Error> {
use am::AutomergeError::InvalidValueType;
use AMindex::*;
if let Pos(pos) = item {
return Ok(*pos);
}
Err(InvalidValueType {
expected: type_name::<usize>().to_string(),
unexpected: type_name::<SmolStr>().to_string(),
})
}
}
/// \ingroup enumerations
/// \enum AMidxType
/// \installed_headerfile
/// \brief The type of an item's index.
#[derive(PartialEq, Eq)]
#[repr(u8)]
pub enum AMidxType {
/// The default tag, not a type signifier.
Default = 0,
/// A UTF-8 string view key.
Key,
/// A 64-bit unsigned integer position.
Pos,
}
impl Default for AMidxType {
fn default() -> Self {
Self::Default
}
}
impl From<&AMindex> for AMidxType {
fn from(index: &AMindex) -> Self {
use AMindex::*;
match index {
Key(_) => Self::Key,
Pos(_) => Self::Pos,
}
}
}

1963
rust/automerge-c/src/item.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,401 @@
use automerge as am;
use std::ffi::c_void;
use std::marker::PhantomData;
use std::mem::size_of;
use crate::item::AMitem;
use crate::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(items: &[AMitem], offset: isize) -> Self {
Self {
len: items.len(),
offset,
ptr: items.as_ptr() 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<&mut AMitem> {
if self.is_stopped() {
return None;
}
let slice: &mut [AMitem] =
unsafe { std::slice::from_raw_parts_mut(self.ptr as *mut AMitem, self.len) };
let value = &mut 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<&mut AMitem> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &mut [AMitem] =
unsafe { std::slice::from_raw_parts_mut(self.ptr as *mut AMitem, self.len) };
Some(&mut 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 AMitems
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of `AMitem` structs.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMitems<'a> {
/// 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_],
phantom: PhantomData<&'a mut AMresult>,
}
impl<'a> AMitems<'a> {
pub fn new(items: &[AMitem]) -> Self {
Self {
detail: Detail::new(items, 0).into(),
phantom: PhantomData,
}
}
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<&mut AMitem> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<&mut AMitem> {
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(),
phantom: PhantomData,
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
phantom: PhantomData,
}
}
}
impl<'a> AsRef<[AMitem]> for AMitems<'a> {
fn as_ref(&self) -> &[AMitem] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const AMitem, detail.len) }
}
}
impl<'a> Default for AMitems<'a> {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_],
phantom: PhantomData,
}
}
}
impl TryFrom<&AMitems<'_>> for Vec<am::Change> {
type Error = am::AutomergeError;
fn try_from(items: &AMitems<'_>) -> Result<Self, Self::Error> {
let mut changes = Vec::<am::Change>::with_capacity(items.len());
for item in items.as_ref().iter() {
match <&am::Change>::try_from(item.as_ref()) {
Ok(change) => {
changes.push(change.clone());
}
Err(e) => {
return Err(e);
}
}
}
Ok(changes)
}
}
impl TryFrom<&AMitems<'_>> for Vec<am::ChangeHash> {
type Error = am::AutomergeError;
fn try_from(items: &AMitems<'_>) -> Result<Self, Self::Error> {
let mut change_hashes = Vec::<am::ChangeHash>::with_capacity(items.len());
for item in items.as_ref().iter() {
match <&am::ChangeHash>::try_from(item.as_ref()) {
Ok(change_hash) => {
change_hashes.push(*change_hash);
}
Err(e) => {
return Err(e);
}
}
}
Ok(change_hashes)
}
}
impl TryFrom<&AMitems<'_>> for Vec<am::ScalarValue> {
type Error = am::AutomergeError;
fn try_from(items: &AMitems<'_>) -> Result<Self, Self::Error> {
let mut scalars = Vec::<am::ScalarValue>::with_capacity(items.len());
for item in items.as_ref().iter() {
match <&am::ScalarValue>::try_from(item.as_ref()) {
Ok(scalar) => {
scalars.push(scalar.clone());
}
Err(e) => {
return Err(e);
}
}
}
Ok(scalars)
}
}
/// \memberof AMitems
/// \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] items A pointer to an `AMitems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p items `!= NULL`
/// \internal
///
/// #Safety
/// items must be a valid pointer to an AMitems
#[no_mangle]
pub unsafe extern "C" fn AMitemsAdvance(items: *mut AMitems, n: isize) {
if let Some(items) = items.as_mut() {
items.advance(n);
};
}
/// \memberof AMitems
/// \brief Tests the equality of two sequences of object items underlying a
/// pair of iterators.
///
/// \param[in] items1 A pointer to an `AMitems` struct.
/// \param[in] items2 A pointer to an `AMitems` struct.
/// \return `true` if \p items1 `==` \p items2 and `false` otherwise.
/// \pre \p items1 `!= NULL`
/// \pre \p items1 `!= NULL`
/// \post `!(`\p items1 `&&` \p items2 `) -> false`
/// \internal
///
/// #Safety
/// items1 must be a valid pointer to an AMitems
/// items2 must be a valid pointer to an AMitems
#[no_mangle]
pub unsafe extern "C" fn AMitemsEqual(items1: *const AMitems, items2: *const AMitems) -> bool {
match (items1.as_ref(), items2.as_ref()) {
(Some(items1), Some(items2)) => items1.as_ref() == items2.as_ref(),
(None, None) | (None, Some(_)) | (Some(_), None) => false,
}
}
/// \memberof AMitems
/// \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] items A pointer to an `AMitems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMitem` struct that's `NULL` when \p items
/// was previously advanced past its forward/reverse limit.
/// \pre \p items `!= NULL`
/// \internal
///
/// #Safety
/// items must be a valid pointer to an AMitems
#[no_mangle]
pub unsafe extern "C" fn AMitemsNext(items: *mut AMitems, n: isize) -> *mut AMitem {
if let Some(items) = items.as_mut() {
if let Some(item) = items.next(n) {
return item;
}
}
std::ptr::null_mut()
}
/// \memberof AMitems
/// \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] items A pointer to an `AMitems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMitem` struct that's `NULL` when \p items
/// is presently advanced past its forward/reverse limit.
/// \pre \p items `!= NULL`
/// \internal
///
/// #Safety
/// items must be a valid pointer to an AMitems
#[no_mangle]
pub unsafe extern "C" fn AMitemsPrev(items: *mut AMitems, n: isize) -> *mut AMitem {
if let Some(items) = items.as_mut() {
if let Some(obj_item) = items.prev(n) {
return obj_item;
}
}
std::ptr::null_mut()
}
/// \memberof AMitems
/// \brief Gets the size of the sequence underlying an iterator.
///
/// \param[in] items A pointer to an `AMitems` struct.
/// \return The count of items in \p items.
/// \pre \p items `!= NULL`
/// \internal
///
/// #Safety
/// items must be a valid pointer to an AMitems
#[no_mangle]
pub unsafe extern "C" fn AMitemsSize(items: *const AMitems) -> usize {
if let Some(items) = items.as_ref() {
return items.len();
}
0
}
/// \memberof AMitems
/// \brief Creates an iterator over the same sequence of items as the
/// given one but with the opposite position and direction.
///
/// \param[in] items A pointer to an `AMitems` struct.
/// \return An `AMitems` struct
/// \pre \p items `!= NULL`
/// \internal
///
/// #Safety
/// items must be a valid pointer to an AMitems
#[no_mangle]
pub unsafe extern "C" fn AMitemsReversed(items: *const AMitems) -> AMitems {
if let Some(items) = items.as_ref() {
return items.reversed();
}
Default::default()
}
/// \memberof AMitems
/// \brief Creates an iterator at the starting position over the same sequence
/// of items as the given one.
///
/// \param[in] items A pointer to an `AMitems` struct.
/// \return An `AMitems` struct
/// \pre \p items `!= NULL`
/// \internal
///
/// #Safety
/// items must be a valid pointer to an AMitems
#[no_mangle]
pub unsafe extern "C" fn AMitemsRewound(items: *const AMitems) -> AMitems {
if let Some(items) = items.as_ref() {
return items.rewound();
}
Default::default()
}

View file

@ -1,11 +1,12 @@
mod actor_id;
mod byte_span;
mod change;
mod change_hashes;
mod changes;
mod doc;
mod index;
mod item;
mod items;
mod obj;
mod result;
mod result_stack;
mod strs;
mod sync;
// include!(concat!(env!("OUT_DIR"), "/enum_string_functions.rs"));

View file

@ -1,12 +1,10 @@
use automerge as am;
use std::any::type_name;
use std::cell::RefCell;
use std::ops::Deref;
use crate::actor_id::AMactorId;
pub mod item;
pub mod items;
macro_rules! to_obj_id {
($handle:expr) => {{
match $handle.as_ref() {
@ -19,12 +17,11 @@ macro_rules! to_obj_id {
pub(crate) use to_obj_id;
macro_rules! to_obj_type {
($am_obj_type:expr) => {{
match $am_obj_type {
AMobjType::Map => am::ObjType::Map,
AMobjType::List => am::ObjType::List,
AMobjType::Text => am::ObjType::Text,
AMobjType::Void => return AMresult::err("Invalid AMobjType value").into(),
($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(),
}
}};
}
@ -79,11 +76,11 @@ impl Deref for AMobjId {
}
/// \memberof AMobjId
/// \brief Gets the actor identifier of an object identifier.
/// \brief Gets the actor identifier component 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
@ -97,11 +94,11 @@ pub unsafe extern "C" fn AMobjIdActorId(obj_id: *const AMobjId) -> *const AMacto
}
/// \memberof AMobjId
/// \brief Gets the counter of an object identifier.
/// \brief Gets the counter component 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
@ -124,8 +121,9 @@ 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_id2 `!= NULL`.
/// \pre \p obj_id1 `!= NULL`
/// \pre \p obj_id1 `!= NULL`
/// \post `!(`\p obj_id1 `&&` \p obj_id2 `) -> false`
/// \internal
///
/// #Safety
@ -135,26 +133,28 @@ 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, Some(_)) | (Some(_), None) | (None, None) => false,
(None, None) | (None, Some(_)) | (Some(_), None) => false,
}
}
/// \memberof AMobjId
/// \brief Gets the index of an object identifier.
/// \brief Gets the index component 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() {
am::ObjId::Id(_, _, index) => *index,
am::ObjId::Root => 0,
Id(_, _, index) => *index,
Root => 0,
}
} else {
usize::MAX
@ -163,26 +163,54 @@ 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 {
/// A void.
/// \note This tag is unalphabetized to evaluate as false.
Void = 0,
/// The default tag, not a type signifier.
Default = 0,
/// A list.
List,
List = 1,
/// A key-value map.
Map,
/// A list of Unicode graphemes.
Text,
}
impl From<am::ObjType> for AMobjType {
fn from(o: am::ObjType) -> Self {
impl Default for AMobjType {
fn default() -> Self {
Self::Default
}
}
impl From<&am::ObjType> for AMobjType {
fn from(o: &am::ObjType) -> Self {
use am::ObjType::*;
match o {
am::ObjType::Map | am::ObjType::Table => AMobjType::Map,
am::ObjType::List => AMobjType::List,
am::ObjType::Text => AMobjType::Text,
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(),
}),
}
}
}

View file

@ -1,73 +0,0 @@
use automerge as am;
use crate::obj::AMobjId;
use crate::result::AMvalue;
/// \struct AMobjItem
/// \installed_headerfile
/// \brief An item in an object.
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>,
}
impl AMobjItem {
pub fn new(value: am::Value<'static>, obj_id: am::ObjId) -> Self {
Self {
obj_id: AMobjId::new(obj_id),
value,
}
}
}
impl PartialEq for AMobjItem {
fn eq(&self, other: &Self) -> bool {
self.obj_id == other.obj_id && self.value == other.value
}
}
impl From<&AMobjItem> for (am::Value<'static>, am::ObjId) {
fn from(obj_item: &AMobjItem) -> Self {
(obj_item.value.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).into()
} else {
AMvalue::Void
}
}

View file

@ -1,341 +0,0 @@
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 {
Default::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 {
Default::default()
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,156 +0,0 @@
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
}

View file

@ -1,359 +0,0 @@
use std::cmp::Ordering;
use std::ffi::c_void;
use std::mem::size_of;
use std::os::raw::c_char;
use crate::byte_span::AMbyteSpan;
/// \brief Creates a string view from a C string.
///
/// \param[in] c_str A UTF-8 C string.
/// \return A UTF-8 string view as an `AMbyteSpan` struct.
/// \internal
///
/// #Safety
/// c_str must be a null-terminated array of `c_char`
#[no_mangle]
pub unsafe extern "C" fn AMstr(c_str: *const c_char) -> AMbyteSpan {
c_str.into()
}
#[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(strings: &[String], offset: isize) -> Self {
Self {
len: strings.len(),
offset,
ptr: 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<AMbyteSpan> {
if self.is_stopped() {
return None;
}
let slice: &[String] =
unsafe { std::slice::from_raw_parts(self.ptr as *const String, self.len) };
let value = slice[self.get_index()].as_bytes().into();
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<AMbyteSpan> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &[String] =
unsafe { std::slice::from_raw_parts(self.ptr as *const String, self.len) };
Some(slice[self.get_index()].as_bytes().into())
}
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(strings: &[String]) -> Self {
Self {
detail: Detail::new(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<AMbyteSpan> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<AMbyteSpan> {
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<[String]> for AMstrs {
fn as_ref(&self) -> &[String] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const String, 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 view as an `AMbyteSpan` struct that's `AMstr(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) -> AMbyteSpan {
if let Some(strs) = strs.as_mut() {
if let Some(key) = strs.next(n) {
return key;
}
}
Default::default()
}
/// \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 view as an `AMbyteSpan` struct that's `AMstr(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) -> AMbyteSpan {
if let Some(strs) = strs.as_mut() {
if let Some(key) = strs.prev(n) {
return key;
}
}
Default::default()
}
/// \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 {
Default::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::change_hashes::AMchangeHashes;
use crate::result::{to_result, AMresult};
/// \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(*const am::sync::Have);
pub struct AMsyncHave(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 {
unsafe { &*self.0 }
&self.0
}
}
@ -25,17 +25,18 @@ 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 An `AMchangeHashes` struct.
/// \pre \p sync_have `!= NULL`.
/// \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.
/// \internal
///
/// # Safety
/// sync_have must be a valid pointer to an AMsyncHave
#[no_mangle]
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 {
Default::default()
}
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(),
})
}

View file

@ -1,378 +0,0 @@
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 {
Default::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 {
Default::default()
}
}

View file

@ -3,18 +3,15 @@ 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::err("Invalid AMsyncMessage pointer").into(),
None => return AMresult::error("Invalid `AMsyncMessage*`").into(),
}
}};
}
@ -51,55 +48,52 @@ 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 An `AMchanges` struct.
/// \pre \p sync_message `!= NULL`.
/// \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.
/// \internal
///
/// # Safety
/// sync_message must be a valid pointer to an AMsyncMessage
#[no_mangle]
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 {
Default::default()
}
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(),
})
}
/// \memberof AMsyncMessage
/// \brief Decodes a sequence of bytes into a synchronization message.
/// \brief Decodes an array of bytes into a synchronization message.
///
/// \param[in] src A pointer to an array of bytes.
/// \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.
/// \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.
/// \internal
///
/// # Safety
/// src must be a byte array of size `>= count`
/// src must be a byte array of length `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMsyncMessageDecode(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::sync::Message::decode(&data))
let data = std::slice::from_raw_parts(src, count);
to_result(am::sync::Message::decode(data))
}
/// \memberof AMsyncMessage
/// \brief Encodes a synchronization message as a sequence of bytes.
/// \brief Encodes a synchronization message as an array of bytes.
///
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \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.
/// \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.
/// \internal
///
/// # Safety
@ -114,41 +108,40 @@ 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 An `AMhaves` struct.
/// \pre \p sync_message `!= NULL`.
/// \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.
/// \internal
///
/// # Safety
/// sync_message must be a valid pointer to an AMsyncMessage
#[no_mangle]
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 {
Default::default()
}
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(),
})
}
/// \memberof AMsyncMessage
/// \brief Gets the heads of the sender.
///
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_message `!= NULL`.
/// \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.
/// \internal
///
/// # Safety
/// sync_message must be a valid pointer to an AMsyncMessage
#[no_mangle]
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 {
Default::default()
}
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(),
})
}
/// \memberof AMsyncMessage
@ -156,17 +149,18 @@ pub unsafe extern "C" fn AMsyncMessageHeads(sync_message: *const AMsyncMessage)
/// by the recipient.
///
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_message `!= NULL`.
/// \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.
/// \internal
///
/// # Safety
/// sync_message must be a valid pointer to an AMsyncMessage
#[no_mangle]
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 {
Default::default()
}
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(),
})
}

View file

@ -2,17 +2,15 @@ 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::err("Invalid AMsyncState pointer").into(),
None => return AMresult::error("Invalid `AMsyncState*`").into(),
}
}};
}
@ -56,36 +54,35 @@ impl From<AMsyncState> for *mut AMsyncState {
}
/// \memberof AMsyncState
/// \brief Decodes a sequence of bytes into a synchronization state.
/// \brief Decodes an array of bytes into a synchronization state.
///
/// \param[in] src A pointer to an array of bytes.
/// \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.
/// \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.
/// \internal
///
/// # Safety
/// src must be a byte array of size `>= count`
/// src must be a byte array of length `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMsyncStateDecode(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::sync::State::decode(&data))
let data = std::slice::from_raw_parts(src, count);
to_result(am::sync::State::decode(data))
}
/// \memberof AMsyncState
/// \brief Encodes a synchronizaton state as a sequence of bytes.
/// \brief Encodes a synchronization state as an array of bytes.
///
/// \param[in] sync_state A pointer to an `AMsyncState` struct.
/// \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.
/// \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.
/// \internal
///
/// # Safety
@ -102,8 +99,9 @@ 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`.
/// \pre \p sync_state1 `!= NULL`
/// \pre \p sync_state2 `!= NULL`
/// \post `!(`\p sync_state1 `&&` \p sync_state2 `) -> false`
/// \internal
///
/// #Safety
@ -116,18 +114,17 @@ 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, Some(_)) | (Some(_), None) | (None, None) => false,
(None, None) | (None, Some(_)) | (Some(_), None) => false,
}
}
/// \memberof AMsyncState
/// \brief Allocates a new synchronization state and initializes it with
/// defaults.
/// \brief Allocates a new synchronization state and initializes it from
/// default values.
///
/// \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.
/// \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.
#[no_mangle]
pub extern "C" fn AMsyncStateInit() -> *mut AMresult {
to_result(am::sync::State::new())
@ -137,40 +134,36 @@ 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 An `AMchangeHashes` struct.
/// \pre \p sync_state `!= NULL`.
/// \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.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an AMsyncState
#[no_mangle]
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 {
Default::default()
}
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())
}
/// \memberof AMsyncState
/// \brief Gets the heads that were last sent by this peer.
///
/// \param[in] sync_state A pointer to an `AMsyncState` struct.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_state `!= NULL`.
/// \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.
/// \internal
///
/// # Safety
/// sync_state must be a valid pointer to an AMsyncState
#[no_mangle]
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 {
Default::default()
}
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())
}
/// \memberof AMsyncState
@ -178,11 +171,13 @@ pub unsafe extern "C" fn AMsyncStateLastSentHeads(
///
/// \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 `AMhaves` struct is relevant, `false` otherwise.
/// \return An `AMhaves` struct.
/// \pre \p sync_state `!= NULL`.
/// \pre \p has_value `!= NULL`.
/// \internal
/// 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
///
/// # Safety
/// sync_state must be a valid pointer to an AMsyncState
@ -191,15 +186,15 @@ pub unsafe extern "C" fn AMsyncStateLastSentHeads(
pub unsafe extern "C" fn AMsyncStateTheirHaves(
sync_state: *const AMsyncState,
has_value: *mut bool,
) -> AMsyncHaves {
) -> *mut AMresult {
if let Some(sync_state) = sync_state.as_ref() {
if let Some(haves) = &sync_state.as_ref().their_have {
*has_value = true;
return AMsyncHaves::new(haves, &mut sync_state.their_haves_storage.borrow_mut());
};
return to_result(haves.as_slice());
}
};
*has_value = false;
Default::default()
to_result(Vec::<am::sync::Have>::new())
}
/// \memberof AMsyncState
@ -207,29 +202,31 @@ 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 `AMchangeHashes` struct is relevant, `false`
/// the returned `AMitems` struct is relevant, `false`
/// otherwise.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_state `!= NULL`.
/// \pre \p has_value `!= NULL`.
/// \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.
/// \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,
) -> AMchangeHashes {
) -> *mut AMresult {
if let Some(sync_state) = sync_state.as_ref() {
if let Some(change_hashes) = &sync_state.as_ref().their_heads {
*has_value = true;
return AMchangeHashes::new(change_hashes);
return to_result(change_hashes.as_slice());
}
};
*has_value = false;
Default::default()
to_result(Vec::<am::ChangeHash>::new())
}
/// \memberof AMsyncState
@ -237,27 +234,29 @@ 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 `AMchangeHashes` struct is relevant, `false`
/// the returned `AMitems` struct is relevant, `false`
/// otherwise.
/// \return An `AMchangeHashes` struct.
/// \pre \p sync_state `!= NULL`.
/// \pre \p has_value `!= NULL`.
/// \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.
/// \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,
) -> AMchangeHashes {
) -> *mut AMresult {
if let Some(sync_state) = sync_state.as_ref() {
if let Some(change_hashes) = &sync_state.as_ref().their_need {
*has_value = true;
return AMchangeHashes::new(change_hashes);
return to_result(change_hashes.as_slice());
}
};
*has_value = false;
Default::default()
to_result(Vec::<am::ChangeHash>::new())
}

View file

@ -0,0 +1,33 @@
#include <stdarg.h>
#include <automerge-c/utils/result.h>
AMresult* AMresultFrom(int count, ...) {
AMresult* result = NULL;
bool is_ok = true;
va_list args;
va_start(args, count);
for (int i = 0; i != count; ++i) {
AMresult* src = va_arg(args, AMresult*);
AMresult* dest = result;
is_ok = (AMresultStatus(src) == AM_STATUS_OK);
if (is_ok) {
if (dest) {
result = AMresultCat(dest, src);
is_ok = (AMresultStatus(result) == AM_STATUS_OK);
AMresultFree(dest);
AMresultFree(src);
} else {
result = src;
}
} else {
AMresultFree(src);
}
}
va_end(args);
if (!is_ok) {
AMresultFree(result);
result = NULL;
}
return result;
}

View file

@ -0,0 +1,106 @@
#include <stdio.h>
#include <stdlib.h>
#include <automerge-c/utils/stack.h>
#include <automerge-c/utils/string.h>
void AMstackFree(AMstack** stack) {
if (stack) {
while (*stack) {
AMresultFree(AMstackPop(stack, NULL));
}
}
}
AMresult* AMstackPop(AMstack** stack, const AMresult* result) {
if (!stack) {
return NULL;
}
AMstack** prev = stack;
if (result) {
while (*prev && ((*prev)->result != result)) {
*prev = (*prev)->prev;
}
}
if (!*prev) {
return NULL;
}
AMstack* target = *prev;
*prev = target->prev;
AMresult* popped = target->result;
free(target);
return popped;
}
AMresult* AMstackResult(AMstack** stack, AMresult* result, AMstackCallback callback, void* data) {
if (!stack) {
if (callback) {
/* Create a local stack so that the callback can still examine the
* result. */
AMstack node = {.result = result, .prev = NULL};
AMstack* stack = &node;
callback(&stack, data);
} else {
/* \note There is no reason to call this function when both the
* stack and the callback are null. */
fprintf(stderr, "ERROR: NULL AMstackCallback!\n");
}
/* \note Nothing can be returned without a stack regardless of
* whether or not the callback validated the result. */
AMresultFree(result);
return NULL;
}
/* Always push the result onto the stack, even if it's null, so that the
* callback can examine it. */
AMstack* next = calloc(1, sizeof(AMstack));
*next = (AMstack){.result = result, .prev = *stack};
AMstack* top = next;
*stack = top;
if (callback) {
if (!callback(stack, data)) {
/* The result didn't pass the callback's examination. */
return NULL;
}
} else {
/* Report an obvious error. */
if (result) {
AMbyteSpan const err_msg = AMresultError(result);
if (err_msg.src && err_msg.count) {
/* \note The callback may be null because the result is supposed
* to be examined externally so return it despite an
* error. */
char* const cstr = AMstrdup(err_msg, NULL);
fprintf(stderr, "WARNING: %s.\n", cstr);
free(cstr);
}
} else {
/* \note There's no reason to call this function when both the
* result and the callback are null. */
fprintf(stderr, "ERROR: NULL AMresult*!\n");
return NULL;
}
}
return result;
}
AMitem* AMstackItem(AMstack** stack, AMresult* result, AMstackCallback callback, void* data) {
AMitems items = AMstackItems(stack, result, callback, data);
return AMitemsNext(&items, 1);
}
AMitems AMstackItems(AMstack** stack, AMresult* result, AMstackCallback callback, void* data) {
return (AMstackResult(stack, result, callback, data)) ? AMresultItems(result) : (AMitems){0};
}
size_t AMstackSize(AMstack const* const stack) {
if (!stack) {
return 0;
}
size_t count = 0;
AMstack const* prev = stack;
while (prev) {
++count;
prev = prev->prev;
}
return count;
}

View file

@ -0,0 +1,9 @@
#include <stdlib.h>
#include <automerge-c/utils/stack_callback_data.h>
AMstackCallbackData* AMstackCallbackDataInit(AMvalType const bitmask, char const* const file, int const line) {
AMstackCallbackData* data = malloc(sizeof(AMstackCallbackData));
*data = (AMstackCallbackData){.bitmask = bitmask, .file = file, .line = line};
return data;
}

View file

@ -0,0 +1,46 @@
#include <stdlib.h>
#include <string.h>
#include <automerge-c/utils/string.h>
char* AMstrdup(AMbyteSpan const str, char const* nul) {
if (!str.src) {
return NULL;
} else if (!str.count) {
return strdup("");
}
nul = (nul) ? nul : "\\0";
size_t const nul_len = strlen(nul);
char* dup = NULL;
size_t dup_len = 0;
char const* begin = str.src;
char const* end = begin;
for (size_t i = 0; i != str.count; ++i, ++end) {
if (!*end) {
size_t const len = end - begin;
size_t const alloc_len = dup_len + len + nul_len;
if (dup) {
dup = realloc(dup, alloc_len + 1);
} else {
dup = malloc(alloc_len + 1);
}
memcpy(dup + dup_len, begin, len);
memcpy(dup + dup_len + len, nul, nul_len);
dup[alloc_len] = '\0';
begin = end + 1;
dup_len = alloc_len;
}
}
if (begin != end) {
size_t const len = end - begin;
size_t const alloc_len = dup_len + len;
if (dup) {
dup = realloc(dup, alloc_len + 1);
} else {
dup = malloc(alloc_len + 1);
}
memcpy(dup + dup_len, begin, len);
dup[alloc_len] = '\0';
}
return dup;
}

View file

@ -1,53 +1,51 @@
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
find_package(cmocka REQUIRED)
find_package(cmocka CONFIG REQUIRED)
add_executable(
test_${LIBRARY_NAME}
${LIBRARY_NAME}_test
actor_id_tests.c
base_state.c
byte_span_tests.c
cmocka_utils.c
enum_string_tests.c
doc_state.c
doc_tests.c
group_state.c
item_tests.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)
set_target_properties(${LIBRARY_NAME}_test 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}>"
)
if(WIN32)
set(CMOCKA "cmocka::cmocka")
else()
set(CMOCKA "cmocka")
endif()
target_link_libraries(test_${LIBRARY_NAME} PRIVATE cmocka ${LIBRARY_NAME})
target_link_libraries(${LIBRARY_NAME}_test PRIVATE ${CMOCKA} ${LIBRARY_NAME})
add_dependencies(test_${LIBRARY_NAME} ${LIBRARY_NAME}_artifacts)
add_dependencies(${LIBRARY_NAME}_test ${BINDINGS_NAME}_artifacts)
if(BUILD_SHARED_LIBS AND WIN32)
add_custom_command(
TARGET test_${LIBRARY_NAME}
TARGET ${LIBRARY_NAME}_test
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..."
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:${LIBRARY_NAME}> $<TARGET_FILE_DIR:${LIBRARY_NAME}_test>
COMMENT "Copying the DLL into the tests directory..."
VERBATIM
)
endif()
add_test(NAME test_${LIBRARY_NAME} COMMAND test_${LIBRARY_NAME})
add_test(NAME ${LIBRARY_NAME}_test COMMAND ${LIBRARY_NAME}_test)
add_custom_command(
TARGET test_${LIBRARY_NAME}
TARGET ${LIBRARY_NAME}_test
POST_BUILD
COMMAND
${CMAKE_CTEST_COMMAND} --config $<CONFIG> --output-on-failure

View file

@ -14,99 +14,126 @@
#include "cmocka_utils.h"
#include "str_utils.h"
/**
* \brief State for a group of cmocka test cases.
*/
typedef struct {
/** An actor ID as an array of bytes. */
uint8_t* src;
AMbyteSpan str;
/** The count of bytes in \p src. */
size_t count;
} GroupState;
/** A stack of results. */
AMstack* stack;
/** An actor ID as a hexadecimal string. */
AMbyteSpan str;
} DocState;
static int group_setup(void** state) {
GroupState* group_state = test_calloc(1, sizeof(GroupState));
group_state->str.src = "000102030405060708090a0b0c0d0e0f";
group_state->str.count = strlen(group_state->str.src);
group_state->count = group_state->str.count / 2;
group_state->src = test_malloc(group_state->count);
hex_to_bytes(group_state->str.src, group_state->src, group_state->count);
*state = group_state;
DocState* doc_state = test_calloc(1, sizeof(DocState));
doc_state->str = AMstr("000102030405060708090a0b0c0d0e0f");
doc_state->count = doc_state->str.count / 2;
doc_state->src = test_calloc(doc_state->count, sizeof(uint8_t));
hex_to_bytes(doc_state->str.src, doc_state->src, doc_state->count);
*state = doc_state;
return 0;
}
static int group_teardown(void** state) {
GroupState* group_state = *state;
test_free(group_state->src);
test_free(group_state);
DocState* doc_state = *state;
test_free(doc_state->src);
AMstackFree(&doc_state->stack);
test_free(doc_state);
return 0;
}
static void test_AMactorIdInit() {
static void test_AMactorIdFromBytes(void** state) {
DocState* doc_state = *state;
AMstack** stack_ptr = &doc_state->stack;
/* Non-empty string. */
AMresult* result = AMstackResult(stack_ptr, AMactorIdFromBytes(doc_state->src, doc_state->count), NULL, NULL);
if (AMresultStatus(result) != AM_STATUS_OK) {
fail_msg_view("%s", AMresultError(result));
}
assert_int_equal(AMresultSize(result), 1);
AMitem* const item = AMresultItem(result);
assert_int_equal(AMitemValType(item), AM_VAL_TYPE_ACTOR_ID);
AMactorId const* actor_id;
assert_true(AMitemToActorId(item, &actor_id));
AMbyteSpan const bytes = AMactorIdBytes(actor_id);
assert_int_equal(bytes.count, doc_state->count);
assert_memory_equal(bytes.src, doc_state->src, bytes.count);
/* Empty array. */
/** \todo Find out if this is intentionally allowed. */
result = AMstackResult(stack_ptr, AMactorIdFromBytes(doc_state->src, 0), NULL, NULL);
if (AMresultStatus(result) != AM_STATUS_OK) {
fail_msg_view("%s", AMresultError(result));
}
/* NULL array. */
result = AMstackResult(stack_ptr, AMactorIdFromBytes(NULL, doc_state->count), NULL, NULL);
if (AMresultStatus(result) == AM_STATUS_OK) {
fail_msg("AMactorId from NULL.");
}
}
static void test_AMactorIdFromStr(void** state) {
DocState* doc_state = *state;
AMstack** stack_ptr = &doc_state->stack;
AMresult* result = AMstackResult(stack_ptr, AMactorIdFromStr(doc_state->str), NULL, NULL);
if (AMresultStatus(result) != AM_STATUS_OK) {
fail_msg_view("%s", AMresultError(result));
}
assert_int_equal(AMresultSize(result), 1);
AMitem* const item = AMresultItem(result);
assert_int_equal(AMitemValType(item), AM_VAL_TYPE_ACTOR_ID);
/* The hexadecimal string should've been decoded as identical bytes. */
AMactorId const* actor_id;
assert_true(AMitemToActorId(item, &actor_id));
AMbyteSpan const bytes = AMactorIdBytes(actor_id);
assert_int_equal(bytes.count, doc_state->count);
assert_memory_equal(bytes.src, doc_state->src, bytes.count);
/* The bytes should've been encoded as an identical hexadecimal string. */
assert_true(AMitemToActorId(item, &actor_id));
AMbyteSpan const str = AMactorIdStr(actor_id);
assert_int_equal(str.count, doc_state->str.count);
assert_memory_equal(str.src, doc_state->str.src, str.count);
}
static void test_AMactorIdInit(void** state) {
DocState* doc_state = *state;
AMstack** stack_ptr = &doc_state->stack;
AMresult* prior_result = NULL;
AMbyteSpan prior_bytes = {NULL, 0};
AMbyteSpan prior_str = {NULL, 0};
AMresult* result = NULL;
for (size_t i = 0; i != 11; ++i) {
result = AMactorIdInit();
AMresult* result = AMstackResult(stack_ptr, AMactorIdInit(), NULL, NULL);
if (AMresultStatus(result) != AM_STATUS_OK) {
fail_msg_view("%s", AMerrorMessage(result));
fail_msg_view("%s", AMresultError(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);
AMbyteSpan const str = AMactorIdStr(value.actor_id);
AMitem* const item = AMresultItem(result);
assert_int_equal(AMitemValType(item), AM_VAL_TYPE_ACTOR_ID);
AMactorId const* actor_id;
assert_true(AMitemToActorId(item, &actor_id));
AMbyteSpan const bytes = AMactorIdBytes(actor_id);
assert_true(AMitemToActorId(item, &actor_id));
AMbyteSpan const str = AMactorIdStr(actor_id);
if (prior_result) {
size_t const max_byte_count = fmax(bytes.count, prior_bytes.count);
assert_memory_not_equal(bytes.src, prior_bytes.src, max_byte_count);
size_t const max_char_count = fmax(str.count, prior_str.count);
assert_memory_not_equal(str.src, prior_str.src, max_char_count);
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_view("%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_view("%s", AMerrorMessage(result));
}
assert_int_equal(AMresultSize(result), 1);
AMvalue const value = AMresultValue(result);
assert_int_equal(value.tag, AM_VALUE_ACTOR_ID);
/* The hexadecimal string should've been decoded as identical bytes. */
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);
/* The bytes should've been encoded as an identical hexadecimal string. */
AMbyteSpan const str = AMactorIdStr(value.actor_id);
assert_int_equal(str.count, group_state->str.count);
assert_memory_equal(str.src, group_state->str.src, str.count);
AMfree(result);
}
int run_actor_id_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_AMactorIdFromBytes),
cmocka_unit_test(test_AMactorIdFromStr),
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,17 @@
#include <stdlib.h>
/* local */
#include "base_state.h"
int setup_base(void** state) {
BaseState* base_state = calloc(1, sizeof(BaseState));
*state = base_state;
return 0;
}
int teardown_base(void** state) {
BaseState* base_state = *state;
AMstackFree(&base_state->stack);
free(base_state);
return 0;
}

View file

@ -0,0 +1,39 @@
#ifndef TESTS_BASE_STATE_H
#define TESTS_BASE_STATE_H
#include <stdint.h>
/* local */
#include <automerge-c/automerge.h>
#include <automerge-c/utils/stack.h>
/**
* \struct BaseState
* \brief The shared state for one or more cmocka test cases.
*/
typedef struct {
/** A stack of results. */
AMstack* stack;
} BaseState;
/**
* \memberof BaseState
* \brief Sets up the shared state for one or more cmocka test cases.
*
* \param[in,out] state A pointer to a pointer to a `BaseState` struct.
* \pre \p state `!= NULL`.
* \warning The `BaseState` struct returned through \p state must be
* passed to `teardown_base()` in order to avoid a memory leak.
*/
int setup_base(void** state);
/**
* \memberof BaseState
* \brief Tears down the shared state for one or more cmocka test cases.
*
* \param[in] state A pointer to a pointer to a `BaseState` struct.
* \pre \p state `!= NULL`.
*/
int teardown_base(void** state);
#endif /* TESTS_BASE_STATE_H */

View file

@ -0,0 +1,119 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* third-party */
#include <cmocka.h>
/* local */
#include <automerge-c/automerge.h>
#include <automerge-c/utils/string.h>
static void test_AMbytes(void** state) {
static char const DATA[] = {0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf};
AMbyteSpan bytes = AMbytes(DATA, sizeof(DATA));
assert_int_equal(bytes.count, sizeof(DATA));
assert_memory_equal(bytes.src, DATA, bytes.count);
assert_ptr_equal(bytes.src, DATA);
/* Empty view */
bytes = AMbytes(DATA, 0);
assert_int_equal(bytes.count, 0);
assert_ptr_equal(bytes.src, DATA);
/* Invalid array */
bytes = AMbytes(NULL, SIZE_MAX);
assert_int_not_equal(bytes.count, SIZE_MAX);
assert_int_equal(bytes.count, 0);
assert_ptr_equal(bytes.src, NULL);
}
static void test_AMstr(void** state) {
AMbyteSpan str = AMstr("abcdefghijkl");
assert_int_equal(str.count, strlen("abcdefghijkl"));
assert_memory_equal(str.src, "abcdefghijkl", str.count);
/* Empty string */
static char const* const EMPTY = "";
str = AMstr(EMPTY);
assert_int_equal(str.count, 0);
assert_ptr_equal(str.src, EMPTY);
/* Invalid string */
str = AMstr(NULL);
assert_int_equal(str.count, 0);
assert_ptr_equal(str.src, NULL);
}
static void test_AMstrCmp(void** state) {
/* Length ordering */
assert_int_equal(AMstrCmp(AMstr("abcdef"), AMstr("abcdefghijkl")), -1);
assert_int_equal(AMstrCmp(AMstr("abcdefghijkl"), AMstr("abcdefghijkl")), 0);
assert_int_equal(AMstrCmp(AMstr("abcdefghijkl"), AMstr("abcdef")), 1);
/* Lexicographical ordering */
assert_int_equal(AMstrCmp(AMstr("abcdef"), AMstr("ghijkl")), -1);
assert_int_equal(AMstrCmp(AMstr("ghijkl"), AMstr("abcdef")), 1);
/* Case ordering */
assert_int_equal(AMstrCmp(AMstr("ABCDEFGHIJKL"), AMstr("abcdefghijkl")), -1);
assert_int_equal(AMstrCmp(AMstr("ABCDEFGHIJKL"), AMstr("ABCDEFGHIJKL")), 0);
assert_int_equal(AMstrCmp(AMstr("abcdefghijkl"), AMstr("ABCDEFGHIJKL")), 1);
assert_int_equal(AMstrCmp(AMstr("ABCDEFGHIJKL"), AMstr("abcdef")), -1);
assert_int_equal(AMstrCmp(AMstr("abcdef"), AMstr("ABCDEFGHIJKL")), 1);
assert_int_equal(AMstrCmp(AMstr("GHIJKL"), AMstr("abcdef")), -1);
assert_int_equal(AMstrCmp(AMstr("abcdef"), AMstr("GHIJKL")), 1);
/* NUL character inclusion */
static char const SRC[] = {'a', 'b', 'c', 'd', 'e', 'f', '\0', 'g', 'h', 'i', 'j', 'k', 'l'};
static AMbyteSpan const NUL_STR = {.src = SRC, .count = 13};
assert_int_equal(AMstrCmp(AMstr("abcdef"), NUL_STR), -1);
assert_int_equal(AMstrCmp(NUL_STR, NUL_STR), 0);
assert_int_equal(AMstrCmp(NUL_STR, AMstr("abcdef")), 1);
/* Empty string */
assert_int_equal(AMstrCmp(AMstr(""), AMstr("abcdefghijkl")), -1);
assert_int_equal(AMstrCmp(AMstr(""), AMstr("")), 0);
assert_int_equal(AMstrCmp(AMstr("abcdefghijkl"), AMstr("")), 1);
/* Invalid string */
assert_int_equal(AMstrCmp(AMstr(NULL), AMstr("abcdefghijkl")), -1);
assert_int_equal(AMstrCmp(AMstr(NULL), AMstr(NULL)), 0);
assert_int_equal(AMstrCmp(AMstr("abcdefghijkl"), AMstr(NULL)), 1);
}
static void test_AMstrdup(void** state) {
static char const SRC[] = {'a', 'b', 'c', '\0', 'd', 'e', 'f', '\0', 'g', 'h', 'i', '\0', 'j', 'k', 'l'};
static AMbyteSpan const NUL_STR = {.src = SRC, .count = 15};
/* Default substitution ("\\0") for NUL */
char* dup = AMstrdup(NUL_STR, NULL);
assert_int_equal(strlen(dup), 18);
assert_string_equal(dup, "abc\\0def\\0ghi\\0jkl");
free(dup);
/* Arbitrary substitution for NUL */
dup = AMstrdup(NUL_STR, ":-O");
assert_int_equal(strlen(dup), 21);
assert_string_equal(dup, "abc:-Odef:-Oghi:-Ojkl");
free(dup);
/* Empty substitution for NUL */
dup = AMstrdup(NUL_STR, "");
assert_int_equal(strlen(dup), 12);
assert_string_equal(dup, "abcdefghijkl");
free(dup);
/* Empty string */
dup = AMstrdup(AMstr(""), NULL);
assert_int_equal(strlen(dup), 0);
assert_string_equal(dup, "");
free(dup);
/* Invalid string */
assert_null(AMstrdup(AMstr(NULL), NULL));
}
int run_byte_span_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_AMbytes),
cmocka_unit_test(test_AMstr),
cmocka_unit_test(test_AMstrCmp),
cmocka_unit_test(test_AMstrdup),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}

View file

@ -0,0 +1,88 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdlib.h>
/* third-party */
#include <automerge-c/utils/enum_string.h>
#include <automerge-c/utils/stack_callback_data.h>
#include <automerge-c/utils/string.h>
#include <cmocka.h>
/* local */
#include "cmocka_utils.h"
/**
* \brief Assert that the given expression is true and report failure in terms
* of a line number within a file.
*
* \param[in] c An expression.
* \param[in] file A file's full path string.
* \param[in] line A line number.
*/
#define assert_true_where(c, file, line) _assert_true(cast_ptr_to_largest_integral_type(c), #c, file, line)
/**
* \brief Assert that the given pointer is non-NULL and report failure in terms
* of a line number within a file.
*
* \param[in] c An expression.
* \param[in] file A file's full path string.
* \param[in] line A line number.
*/
#define assert_non_null_where(c, file, line) assert_true_where(c, file, line)
/**
* \brief Forces the test to fail immediately and quit, printing the reason in
* terms of a line number within a file.
*
* \param[in] msg A message string into which \p str is interpolated.
* \param[in] str An owned string.
* \param[in] file A file's full path string.
* \param[in] line A line number.
*/
#define fail_msg_where(msg, str, file, line) \
do { \
print_error("ERROR: " msg "\n", str); \
_fail(file, line); \
} while (0)
/**
* \brief Forces the test to fail immediately and quit, printing the reason in
* terms of a line number within a file.
*
* \param[in] msg A message string into which \p view.src is interpolated.
* \param[in] view A UTF-8 string view as an `AMbyteSpan` struct.
* \param[in] file A file's full path string.
* \param[in] line A line number.
*/
#define fail_msg_view_where(msg, view, file, line) \
do { \
char* const str = AMstrdup(view, NULL); \
print_error("ERROR: " msg "\n", str); \
free(str); \
_fail(file, line); \
} while (0)
bool cmocka_cb(AMstack** stack, void* data) {
assert_non_null(data);
AMstackCallbackData* const sc_data = (AMstackCallbackData*)data;
assert_non_null_where(stack, sc_data->file, sc_data->line);
assert_non_null_where(*stack, sc_data->file, sc_data->line);
assert_non_null_where((*stack)->result, sc_data->file, sc_data->line);
if (AMresultStatus((*stack)->result) != AM_STATUS_OK) {
fail_msg_view_where("%s", AMresultError((*stack)->result), sc_data->file, sc_data->line);
return false;
}
/* Test that the types of all item values are members of the mask. */
AMitems items = AMresultItems((*stack)->result);
AMitem* item = NULL;
while ((item = AMitemsNext(&items, 1)) != NULL) {
AMvalType const tag = AMitemValType(item);
if (!(tag & sc_data->bitmask)) {
fail_msg_where("Unexpected value type `%s`.", AMvalTypeToString(tag), sc_data->file, sc_data->line);
return false;
}
}
return true;
}

View file

@ -1,22 +1,42 @@
#ifndef CMOCKA_UTILS_H
#define CMOCKA_UTILS_H
#ifndef TESTS_CMOCKA_UTILS_H
#define TESTS_CMOCKA_UTILS_H
#include <stdlib.h>
#include <string.h>
/* third-party */
#include <automerge-c/utils/string.h>
#include <cmocka.h>
/* local */
#include "base_state.h"
/**
* \brief Forces the test to fail immediately and quit, printing the reason.
*
* \param[in] view A string view as an `AMbyteSpan` struct.
* \param[in] msg A message string into which \p view.src is interpolated.
* \param[in] view A UTF-8 string view as an `AMbyteSpan` struct.
*/
#define fail_msg_view(msg, view) do { \
char* const c_str = test_calloc(1, view.count + 1); \
strncpy(c_str, view.src, view.count); \
print_error(msg, c_str); \
test_free(c_str); \
#define fail_msg_view(msg, view) \
do { \
char* const c_str = AMstrdup(view, NULL); \
print_error("ERROR: " msg "\n", c_str); \
free(c_str); \
fail(); \
} while (0)
#endif /* CMOCKA_UTILS_H */
/**
* \brief Validates the top result in a stack based upon the parameters
* specified within the given data structure and reports violations
* using cmocka assertions.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] data A pointer to an owned `AMpushData` struct.
* \return `true` if the top `AMresult` struct in \p stack is valid, `false`
* otherwise.
* \pre \p stack `!= NULL`.
* \pre \p data `!= NULL`.
*/
bool cmocka_cb(AMstack** stack, void* data);
#endif /* TESTS_CMOCKA_UTILS_H */

View file

@ -0,0 +1,27 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stdlib.h>
/* third-party */
#include <cmocka.h>
/* local */
#include <automerge-c/utils/stack_callback_data.h>
#include "cmocka_utils.h"
#include "doc_state.h"
int setup_doc(void** state) {
DocState* doc_state = test_calloc(1, sizeof(DocState));
setup_base((void**)&doc_state->base_state);
AMitemToDoc(AMstackItem(&doc_state->base_state->stack, AMcreate(NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_DOC)),
&doc_state->doc);
*state = doc_state;
return 0;
}
int teardown_doc(void** state) {
DocState* doc_state = *state;
teardown_base((void**)&doc_state->base_state);
test_free(doc_state);
return 0;
}

View file

@ -0,0 +1,17 @@
#ifndef TESTS_DOC_STATE_H
#define TESTS_DOC_STATE_H
/* local */
#include <automerge-c/automerge.h>
#include "base_state.h"
typedef struct {
BaseState* base_state;
AMdoc* doc;
} DocState;
int setup_doc(void** state);
int teardown_doc(void** state);
#endif /* TESTS_DOC_STATE_H */

View file

@ -9,12 +9,14 @@
/* local */
#include <automerge-c/automerge.h>
#include "group_state.h"
#include "stack_utils.h"
#include <automerge-c/utils/stack_callback_data.h>
#include "base_state.h"
#include "cmocka_utils.h"
#include "doc_state.h"
#include "str_utils.h"
typedef struct {
GroupState* group_state;
DocState* doc_state;
AMbyteSpan actor_id_str;
uint8_t* actor_id_bytes;
size_t actor_id_size;
@ -22,7 +24,7 @@ typedef struct {
static int setup(void** state) {
TestState* test_state = test_calloc(1, sizeof(TestState));
group_setup((void**)&test_state->group_state);
setup_doc((void**)&test_state->doc_state);
test_state->actor_id_str.src = "000102030405060708090a0b0c0d0e0f";
test_state->actor_id_str.count = strlen(test_state->actor_id_str.src);
test_state->actor_id_size = test_state->actor_id_str.count / 2;
@ -34,155 +36,148 @@ static int setup(void** state) {
static int teardown(void** state) {
TestState* test_state = *state;
group_teardown((void**)&test_state->group_state);
teardown_doc((void**)&test_state->doc_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).src);
assert_null(AMstrsPrev(&forward, 1).src);
assert_null(AMstrsNext(&reverse, 1).src);
assert_null(AMstrsPrev(&reverse, 1).src);
AMfreeStack(&stack);
static void test_AMkeys_empty(void** state) {
TestState* test_state = *state;
AMstack** stack_ptr = &test_state->doc_state->base_state->stack;
AMdoc* doc;
assert_true(AMitemToDoc(AMstackItem(stack_ptr, AMcreate(NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc));
AMitems forward = AMstackItems(stack_ptr, AMkeys(doc, AM_ROOT, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
assert_int_equal(AMitemsSize(&forward), 0);
AMitems reverse = AMitemsReversed(&forward);
assert_int_equal(AMitemsSize(&reverse), 0);
assert_null(AMitemsNext(&forward, 1));
assert_null(AMitemsPrev(&forward, 1));
assert_null(AMitemsNext(&reverse, 1));
assert_null(AMitemsPrev(&reverse, 1));
}
static void test_AMkeys_list() {
AMresultStack* stack = NULL;
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMobjId const* const list = AMpush(
&stack,
AMmapPutObject(doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),
AM_VALUE_OBJ_ID,
cmocka_cb).obj_id;
AMfree(AMlistPutInt(doc, list, 0, true, 0));
AMfree(AMlistPutInt(doc, list, 1, true, 0));
AMfree(AMlistPutInt(doc, list, 2, true, 0));
AMstrs forward = AMpush(&stack,
AMkeys(doc, list, NULL),
AM_VALUE_STRS,
cmocka_cb).strs;
assert_int_equal(AMstrsSize(&forward), 3);
AMstrs reverse = AMstrsReversed(&forward);
assert_int_equal(AMstrsSize(&reverse), 3);
static void test_AMkeys_list(void** state) {
TestState* test_state = *state;
AMstack** stack_ptr = &test_state->doc_state->base_state->stack;
AMdoc* doc;
assert_true(AMitemToDoc(AMstackItem(stack_ptr, AMcreate(NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc));
AMobjId const* const list =
AMitemObjId(AMstackItem(stack_ptr, AMmapPutObject(doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST), cmocka_cb,
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
AMstackItem(NULL, AMlistPutInt(doc, list, 0, true, 0), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutInt(doc, list, 1, true, 0), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutInt(doc, list, 2, true, 0), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMitems forward = AMstackItems(stack_ptr, AMkeys(doc, list, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
assert_int_equal(AMitemsSize(&forward), 3);
AMitems reverse = AMitemsReversed(&forward);
assert_int_equal(AMitemsSize(&reverse), 3);
/* Forward iterator forward. */
AMbyteSpan str = AMstrsNext(&forward, 1);
AMbyteSpan str;
assert_true(AMitemToStr(AMitemsNext(&forward, 1), &str));
assert_ptr_equal(strstr(str.src, "2@"), str.src);
str = AMstrsNext(&forward, 1);
assert_true(AMitemToStr(AMitemsNext(&forward, 1), &str));
assert_ptr_equal(strstr(str.src, "3@"), str.src);
str = AMstrsNext(&forward, 1);
assert_true(AMitemToStr(AMitemsNext(&forward, 1), &str));
assert_ptr_equal(strstr(str.src, "4@"), str.src);
assert_null(AMstrsNext(&forward, 1).src);
assert_null(AMitemsNext(&forward, 1));
// /* Forward iterator reverse. */
str = AMstrsPrev(&forward, 1);
assert_true(AMitemToStr(AMitemsPrev(&forward, 1), &str));
assert_ptr_equal(strstr(str.src, "4@"), str.src);
str = AMstrsPrev(&forward, 1);
assert_true(AMitemToStr(AMitemsPrev(&forward, 1), &str));
assert_ptr_equal(strstr(str.src, "3@"), str.src);
str = AMstrsPrev(&forward, 1);
assert_true(AMitemToStr(AMitemsPrev(&forward, 1), &str));
assert_ptr_equal(strstr(str.src, "2@"), str.src);
assert_null(AMstrsPrev(&forward, 1).src);
assert_null(AMitemsPrev(&forward, 1));
/* Reverse iterator forward. */
str = AMstrsNext(&reverse, 1);
assert_true(AMitemToStr(AMitemsNext(&reverse, 1), &str));
assert_ptr_equal(strstr(str.src, "4@"), str.src);
str = AMstrsNext(&reverse, 1);
assert_true(AMitemToStr(AMitemsNext(&reverse, 1), &str));
assert_ptr_equal(strstr(str.src, "3@"), str.src);
str = AMstrsNext(&reverse, 1);
assert_true(AMitemToStr(AMitemsNext(&reverse, 1), &str));
assert_ptr_equal(strstr(str.src, "2@"), str.src);
assert_null(AMstrsNext(&reverse, 1).src);
assert_null(AMitemsNext(&reverse, 1));
/* Reverse iterator reverse. */
str = AMstrsPrev(&reverse, 1);
assert_true(AMitemToStr(AMitemsPrev(&reverse, 1), &str));
assert_ptr_equal(strstr(str.src, "2@"), str.src);
str = AMstrsPrev(&reverse, 1);
assert_true(AMitemToStr(AMitemsPrev(&reverse, 1), &str));
assert_ptr_equal(strstr(str.src, "3@"), str.src);
str = AMstrsPrev(&reverse, 1);
assert_true(AMitemToStr(AMitemsPrev(&reverse, 1), &str));
assert_ptr_equal(strstr(str.src, "4@"), str.src);
assert_null(AMstrsPrev(&reverse, 1).src);
AMfreeStack(&stack);
assert_null(AMitemsPrev(&reverse, 1));
}
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, AMstr("one"), 1));
AMfree(AMmapPutInt(doc, AM_ROOT, AMstr("two"), 2));
AMfree(AMmapPutInt(doc, AM_ROOT, AMstr("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);
static void test_AMkeys_map(void** state) {
TestState* test_state = *state;
AMstack** stack_ptr = &test_state->doc_state->base_state->stack;
AMdoc* doc;
assert_true(AMitemToDoc(AMstackItem(stack_ptr, AMcreate(NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc));
AMstackItem(NULL, AMmapPutInt(doc, AM_ROOT, AMstr("one"), 1), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMmapPutInt(doc, AM_ROOT, AMstr("two"), 2), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMmapPutInt(doc, AM_ROOT, AMstr("three"), 3), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMitems forward = AMstackItems(stack_ptr, AMkeys(doc, AM_ROOT, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
assert_int_equal(AMitemsSize(&forward), 3);
AMitems reverse = AMitemsReversed(&forward);
assert_int_equal(AMitemsSize(&reverse), 3);
/* Forward iterator forward. */
AMbyteSpan str = AMstrsNext(&forward, 1);
AMbyteSpan str;
assert_true(AMitemToStr(AMitemsNext(&forward, 1), &str));
assert_int_equal(str.count, 3);
assert_memory_equal(str.src, "one", str.count);
str = AMstrsNext(&forward, 1);
assert_true(AMitemToStr(AMitemsNext(&forward, 1), &str));
assert_int_equal(str.count, 5);
assert_memory_equal(str.src, "three", str.count);
str = AMstrsNext(&forward, 1);
assert_true(AMitemToStr(AMitemsNext(&forward, 1), &str));
assert_int_equal(str.count, 3);
assert_memory_equal(str.src, "two", str.count);
assert_null(AMstrsNext(&forward, 1).src);
assert_null(AMitemsNext(&forward, 1));
/* Forward iterator reverse. */
str = AMstrsPrev(&forward, 1);
assert_true(AMitemToStr(AMitemsPrev(&forward, 1), &str));
assert_int_equal(str.count, 3);
assert_memory_equal(str.src, "two", str.count);
str = AMstrsPrev(&forward, 1);
assert_true(AMitemToStr(AMitemsPrev(&forward, 1), &str));
assert_int_equal(str.count, 5);
assert_memory_equal(str.src, "three", str.count);
str = AMstrsPrev(&forward, 1);
assert_true(AMitemToStr(AMitemsPrev(&forward, 1), &str));
assert_int_equal(str.count, 3);
assert_memory_equal(str.src, "one", str.count);
assert_null(AMstrsPrev(&forward, 1).src);
assert_null(AMitemsPrev(&forward, 1));
/* Reverse iterator forward. */
str = AMstrsNext(&reverse, 1);
assert_true(AMitemToStr(AMitemsNext(&reverse, 1), &str));
assert_int_equal(str.count, 3);
assert_memory_equal(str.src, "two", str.count);
str = AMstrsNext(&reverse, 1);
assert_true(AMitemToStr(AMitemsNext(&reverse, 1), &str));
assert_int_equal(str.count, 5);
assert_memory_equal(str.src, "three", str.count);
str = AMstrsNext(&reverse, 1);
assert_true(AMitemToStr(AMitemsNext(&reverse, 1), &str));
assert_int_equal(str.count, 3);
assert_memory_equal(str.src, "one", str.count);
assert_null(AMstrsNext(&reverse, 1).src);
assert_null(AMitemsNext(&reverse, 1));
/* Reverse iterator reverse. */
str = AMstrsPrev(&reverse, 1);
assert_true(AMitemToStr(AMitemsPrev(&reverse, 1), &str));
assert_int_equal(str.count, 3);
assert_memory_equal(str.src, "one", str.count);
str = AMstrsPrev(&reverse, 1);
assert_true(AMitemToStr(AMitemsPrev(&reverse, 1), &str));
assert_int_equal(str.count, 5);
assert_memory_equal(str.src, "three", str.count);
str = AMstrsPrev(&reverse, 1);
assert_true(AMitemToStr(AMitemsPrev(&reverse, 1), &str));
assert_int_equal(str.count, 3);
assert_memory_equal(str.src, "two", str.count);
assert_null(AMstrsPrev(&reverse, 1).src);
AMfreeStack(&stack);
assert_null(AMitemsPrev(&reverse, 1));
}
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;
AMstack** stack_ptr = &test_state->doc_state->base_state->stack;
AMactorId const* actor_id;
assert_true(AMitemToActorId(
AMstackItem(stack_ptr, AMactorIdFromBytes(test_state->actor_id_bytes, test_state->actor_id_size), cmocka_cb,
AMexpect(AM_VAL_TYPE_ACTOR_ID)),
&actor_id));
AMstackItem(NULL, AMsetActorId(test_state->doc_state->doc, actor_id), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
assert_true(AMitemToActorId(
AMstackItem(stack_ptr, AMgetActorId(test_state->doc_state->doc), cmocka_cb, AMexpect(AM_VAL_TYPE_ACTOR_ID)),
&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);
@ -190,48 +185,46 @@ static void test_AMputActor_bytes(void **state) {
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;
AMstack** stack_ptr = &test_state->doc_state->base_state->stack;
AMactorId const* actor_id;
assert_true(AMitemToActorId(
AMstackItem(stack_ptr, AMactorIdFromStr(test_state->actor_id_str), cmocka_cb, AMexpect(AM_VAL_TYPE_ACTOR_ID)),
&actor_id));
AMstackItem(NULL, AMsetActorId(test_state->doc_state->doc, actor_id), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
assert_true(AMitemToActorId(
AMstackItem(stack_ptr, AMgetActorId(test_state->doc_state->doc), cmocka_cb, AMexpect(AM_VAL_TYPE_ACTOR_ID)),
&actor_id));
AMbyteSpan const str = AMactorIdStr(actor_id);
assert_int_equal(str.count, test_state->actor_id_str.count);
assert_memory_equal(str.src, test_state->actor_id_str.src, str.count);
}
static void test_AMspliceText() {
AMresultStack* stack = NULL;
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMobjId const* const text = AMpush(&stack,
AMmapPutObject(doc, AM_ROOT, AMstr("text"), AM_OBJ_TYPE_TEXT),
AM_VALUE_OBJ_ID,
cmocka_cb).obj_id;
AMfree(AMspliceText(doc, text, 0, 0, AMstr("one + ")));
AMfree(AMspliceText(doc, text, 4, 2, AMstr("two = ")));
AMfree(AMspliceText(doc, text, 8, 2, AMstr("three")));
AMbyteSpan const str = AMpush(&stack,
AMtext(doc, text, NULL),
AM_VALUE_STR,
cmocka_cb).str;
static char const* const STR_VALUE = "one two three";
assert_int_equal(str.count, strlen(STR_VALUE));
assert_memory_equal(str.src, STR_VALUE, str.count);
AMfreeStack(&stack);
static void test_AMspliceText(void** state) {
TestState* test_state = *state;
AMstack** stack_ptr = &test_state->doc_state->base_state->stack;
AMdoc* doc;
assert_true(AMitemToDoc(AMstackItem(stack_ptr, AMcreate(NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc));
AMobjId const* const text =
AMitemObjId(AMstackItem(stack_ptr, AMmapPutObject(doc, AM_ROOT, AMstr("text"), AM_OBJ_TYPE_TEXT), cmocka_cb,
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
AMstackItem(NULL, AMspliceText(doc, text, 0, 0, AMstr("one + ")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMspliceText(doc, text, 4, 2, AMstr("two = ")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMspliceText(doc, text, 8, 2, AMstr("three")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMbyteSpan str;
assert_true(
AMitemToStr(AMstackItem(stack_ptr, AMtext(doc, text, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR)), &str));
assert_int_equal(str.count, strlen("one two three"));
assert_memory_equal(str.src, "one two three", str.count);
}
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_AMkeys_empty, setup, teardown),
cmocka_unit_test_setup_teardown(test_AMkeys_list, setup, teardown),
cmocka_unit_test_setup_teardown(test_AMkeys_map, setup, teardown),
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),
cmocka_unit_test_setup_teardown(test_AMspliceText, setup, teardown),
};
return cmocka_run_group_tests(tests, NULL, NULL);

View file

@ -0,0 +1,148 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
/* third-party */
#include <cmocka.h>
/* local */
#include <automerge-c/automerge.h>
#include <automerge-c/utils/enum_string.h>
#define assert_to_string(function, tag) assert_string_equal(function(tag), #tag)
#define assert_from_string(function, type, tag) \
do { \
type out; \
assert_true(function(&out, #tag)); \
assert_int_equal(out, tag); \
} while (0)
static void test_AMidxTypeToString(void** state) {
assert_to_string(AMidxTypeToString, AM_IDX_TYPE_DEFAULT);
assert_to_string(AMidxTypeToString, AM_IDX_TYPE_KEY);
assert_to_string(AMidxTypeToString, AM_IDX_TYPE_POS);
/* Zero tag */
assert_string_equal(AMidxTypeToString(0), "AM_IDX_TYPE_DEFAULT");
/* Invalid tag */
assert_string_equal(AMidxTypeToString(-1), "???");
}
static void test_AMidxTypeFromString(void** state) {
assert_from_string(AMidxTypeFromString, AMidxType, AM_IDX_TYPE_DEFAULT);
assert_from_string(AMidxTypeFromString, AMidxType, AM_IDX_TYPE_KEY);
assert_from_string(AMidxTypeFromString, AMidxType, AM_IDX_TYPE_POS);
/* Invalid tag */
AMidxType out = -1;
assert_false(AMidxTypeFromString(&out, "???"));
assert_int_equal(out, (AMidxType)-1);
}
static void test_AMobjTypeToString(void** state) {
assert_to_string(AMobjTypeToString, AM_OBJ_TYPE_DEFAULT);
assert_to_string(AMobjTypeToString, AM_OBJ_TYPE_LIST);
assert_to_string(AMobjTypeToString, AM_OBJ_TYPE_MAP);
assert_to_string(AMobjTypeToString, AM_OBJ_TYPE_TEXT);
/* Zero tag */
assert_string_equal(AMobjTypeToString(0), "AM_OBJ_TYPE_DEFAULT");
/* Invalid tag */
assert_string_equal(AMobjTypeToString(-1), "???");
}
static void test_AMobjTypeFromString(void** state) {
assert_from_string(AMobjTypeFromString, AMobjType, AM_OBJ_TYPE_DEFAULT);
assert_from_string(AMobjTypeFromString, AMobjType, AM_OBJ_TYPE_LIST);
assert_from_string(AMobjTypeFromString, AMobjType, AM_OBJ_TYPE_MAP);
assert_from_string(AMobjTypeFromString, AMobjType, AM_OBJ_TYPE_TEXT);
/* Invalid tag */
AMobjType out = -1;
assert_false(AMobjTypeFromString(&out, "???"));
assert_int_equal(out, (AMobjType)-1);
}
static void test_AMstatusToString(void** state) {
assert_to_string(AMstatusToString, AM_STATUS_ERROR);
assert_to_string(AMstatusToString, AM_STATUS_INVALID_RESULT);
assert_to_string(AMstatusToString, AM_STATUS_OK);
/* Zero tag */
assert_string_equal(AMstatusToString(0), "AM_STATUS_OK");
/* Invalid tag */
assert_string_equal(AMstatusToString(-1), "???");
}
static void test_AMstatusFromString(void** state) {
assert_from_string(AMstatusFromString, AMstatus, AM_STATUS_ERROR);
assert_from_string(AMstatusFromString, AMstatus, AM_STATUS_INVALID_RESULT);
assert_from_string(AMstatusFromString, AMstatus, AM_STATUS_OK);
/* Invalid tag */
AMstatus out = -1;
assert_false(AMstatusFromString(&out, "???"));
assert_int_equal(out, (AMstatus)-1);
}
static void test_AMvalTypeToString(void** state) {
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_ACTOR_ID);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_BOOL);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_BYTES);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_CHANGE);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_CHANGE_HASH);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_COUNTER);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_DEFAULT);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_DOC);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_F64);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_INT);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_NULL);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_OBJ_TYPE);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_STR);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_SYNC_HAVE);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_SYNC_MESSAGE);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_SYNC_STATE);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_TIMESTAMP);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_UINT);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_UNKNOWN);
assert_to_string(AMvalTypeToString, AM_VAL_TYPE_VOID);
/* Zero tag */
assert_string_equal(AMvalTypeToString(0), "AM_VAL_TYPE_DEFAULT");
/* Invalid tag */
assert_string_equal(AMvalTypeToString(-1), "???");
}
static void test_AMvalTypeFromString(void** state) {
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_ACTOR_ID);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_BOOL);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_BYTES);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_CHANGE);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_CHANGE_HASH);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_COUNTER);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_DEFAULT);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_DOC);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_F64);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_INT);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_NULL);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_OBJ_TYPE);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_STR);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_SYNC_HAVE);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_SYNC_MESSAGE);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_SYNC_STATE);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_TIMESTAMP);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_UINT);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_UNKNOWN);
assert_from_string(AMvalTypeFromString, AMvalType, AM_VAL_TYPE_VOID);
/* Invalid tag */
AMvalType out = -1;
assert_false(AMvalTypeFromString(&out, "???"));
assert_int_equal(out, (AMvalType)-1);
}
int run_enum_string_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_AMidxTypeToString), cmocka_unit_test(test_AMidxTypeFromString),
cmocka_unit_test(test_AMobjTypeToString), cmocka_unit_test(test_AMobjTypeFromString),
cmocka_unit_test(test_AMstatusToString), cmocka_unit_test(test_AMstatusFromString),
cmocka_unit_test(test_AMvalTypeToString), cmocka_unit_test(test_AMvalTypeFromString),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}

View file

@ -1,27 +0,0 @@
#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

@ -1,16 +0,0 @@
#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,94 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
/* third-party */
#include <cmocka.h>
/* local */
#include <automerge-c/automerge.h>
#include <automerge-c/utils/stack_callback_data.h>
#include "cmocka_utils.h"
#include "doc_state.h"
static void test_AMitemResult(void** state) {
enum { ITEM_COUNT = 1000 };
DocState* doc_state = *state;
AMstack** stack_ptr = &doc_state->base_state->stack;
/* Append the strings to a list so that they'll be in numerical order. */
AMobjId const* const list =
AMitemObjId(AMstackItem(stack_ptr, AMmapPutObject(doc_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),
cmocka_cb, AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
for (size_t pos = 0; pos != ITEM_COUNT; ++pos) {
size_t const count = snprintf(NULL, 0, "%zu", pos);
char* const src = test_calloc(count + 1, sizeof(char));
assert_int_equal(sprintf(src, "%zu", pos), count);
AMstackItem(NULL, AMlistPutStr(doc_state->doc, list, pos, true, AMbytes(src, count)), cmocka_cb,
AMexpect(AM_VAL_TYPE_VOID));
test_free(src);
}
/* Get an item iterator. */
AMitems items = AMstackItems(stack_ptr, AMlistRange(doc_state->doc, list, 0, SIZE_MAX, NULL), cmocka_cb,
AMexpect(AM_VAL_TYPE_STR));
/* Get the item iterator's result so that it can be freed later. */
AMresult const* const items_result = (*stack_ptr)->result;
/* Iterate over all of the items and copy their pointers into an array. */
AMitem* item_ptrs[ITEM_COUNT] = {NULL};
AMitem* item = NULL;
for (size_t pos = 0; (item = AMitemsNext(&items, 1)) != NULL; ++pos) {
/* The item's reference count should be 1. */
assert_int_equal(AMitemRefCount(item), 1);
if (pos & 1) {
/* Create a redundant result for an odd item. */
AMitem* const new_item = AMstackItem(stack_ptr, AMitemResult(item), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
/* The item's old and new pointers will never match. */
assert_ptr_not_equal(new_item, item);
/* The item's reference count will have been incremented. */
assert_int_equal(AMitemRefCount(item), 2);
assert_int_equal(AMitemRefCount(new_item), 2);
/* The item's old and new indices should match. */
assert_int_equal(AMitemIdxType(item), AMitemIdxType(new_item));
assert_int_equal(AMitemIdxType(item), AM_IDX_TYPE_POS);
size_t pos, new_pos;
assert_true(AMitemPos(item, &pos));
assert_true(AMitemPos(new_item, &new_pos));
assert_int_equal(pos, new_pos);
/* The item's old and new object IDs should match. */
AMobjId const* const obj_id = AMitemObjId(item);
AMobjId const* const new_obj_id = AMitemObjId(new_item);
assert_true(AMobjIdEqual(obj_id, new_obj_id));
/* The item's old and new value types should match. */
assert_int_equal(AMitemValType(item), AMitemValType(new_item));
/* The item's old and new string values should match. */
AMbyteSpan str;
assert_true(AMitemToStr(item, &str));
AMbyteSpan new_str;
assert_true(AMitemToStr(new_item, &new_str));
assert_int_equal(str.count, new_str.count);
assert_memory_equal(str.src, new_str.src, new_str.count);
/* The item's old and new object IDs are one and the same. */
assert_ptr_equal(obj_id, new_obj_id);
/* The item's old and new string values are one and the same. */
assert_ptr_equal(str.src, new_str.src);
/* Save the item's new pointer. */
item_ptrs[pos] = new_item;
}
}
/* Free the item iterator's result. */
AMresultFree(AMstackPop(stack_ptr, items_result));
/* An odd item's reference count should be 1 again. */
for (size_t pos = 1; pos < ITEM_COUNT; pos += 2) {
assert_int_equal(AMitemRefCount(item_ptrs[pos]), 1);
}
}
int run_item_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_AMitemResult),
};
return cmocka_run_group_tests(tests, setup_doc, teardown_doc);
}

View file

@ -11,53 +11,50 @@
/* local */
#include <automerge-c/automerge.h>
#include <automerge-c/utils/stack_callback_data.h>
#include "base_state.h"
#include "cmocka_utils.h"
#include "group_state.h"
#include "doc_state.h"
#include "macro_utils.h"
#include "stack_utils.h"
static void test_AMlistIncrement(void** state) {
GroupState* group_state = *state;
AMobjId const* const list = AMpush(
&group_state->stack,
AMmapPutObject(group_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),
AM_VALUE_OBJ_ID,
cmocka_cb).obj_id;
AMfree(AMlistPutCounter(group_state->doc, list, 0, true, 0));
assert_int_equal(AMpush(&group_state->stack,
AMlistGet(group_state->doc, list, 0, NULL),
AM_VALUE_COUNTER,
cmocka_cb).counter, 0);
AMfree(AMpop(&group_state->stack));
AMfree(AMlistIncrement(group_state->doc, list, 0, 3));
assert_int_equal(AMpush(&group_state->stack,
AMlistGet(group_state->doc, list, 0, NULL),
AM_VALUE_COUNTER,
cmocka_cb).counter, 3);
AMfree(AMpop(&group_state->stack));
DocState* doc_state = *state;
AMstack** stack_ptr = &doc_state->base_state->stack;
AMobjId const* const list =
AMitemObjId(AMstackItem(stack_ptr, AMmapPutObject(doc_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),
cmocka_cb, AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
AMstackItem(NULL, AMlistPutCounter(doc_state->doc, list, 0, true, 0), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
int64_t counter;
assert_true(AMitemToCounter(
AMstackItem(stack_ptr, AMlistGet(doc_state->doc, list, 0, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_COUNTER)),
&counter));
assert_int_equal(counter, 0);
AMresultFree(AMstackPop(stack_ptr, NULL));
AMstackItem(NULL, AMlistIncrement(doc_state->doc, list, 0, 3), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
assert_true(AMitemToCounter(
AMstackItem(stack_ptr, AMlistGet(doc_state->doc, list, 0, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_COUNTER)),
&counter));
assert_int_equal(counter, 3);
AMresultFree(AMstackPop(stack_ptr, NULL));
}
#define test_AMlistPut(suffix, mode) test_AMlistPut##suffix##_##mode
#define static_void_test_AMlistPut(suffix, mode, member, scalar_value) \
#define static_void_test_AMlistPut(suffix, mode, type, scalar_value) \
static void test_AMlistPut##suffix##_##mode(void** state) { \
GroupState* group_state = *state; \
AMobjId const* const list = AMpush( \
&group_state->stack, \
AMmapPutObject(group_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),\
AM_VALUE_OBJ_ID, \
cmocka_cb).obj_id; \
AMfree(AMlistPut ## suffix(group_state->doc, \
list, \
0, \
!strcmp(#mode, "insert"), \
scalar_value)); \
assert_true(AMpush( \
&group_state->stack, \
AMlistGet(group_state->doc, list, 0, NULL), \
AMvalue_discriminant(#suffix), \
cmocka_cb).member == scalar_value); \
AMfree(AMpop(&group_state->stack)); \
DocState* doc_state = *state; \
AMstack** stack_ptr = &doc_state->base_state->stack; \
AMobjId const* const list = AMitemObjId( \
AMstackItem(stack_ptr, AMmapPutObject(doc_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST), \
cmocka_cb, AMexpect(AM_VAL_TYPE_OBJ_TYPE))); \
AMstackItem(NULL, AMlistPut##suffix(doc_state->doc, list, 0, !strcmp(#mode, "insert"), scalar_value), \
cmocka_cb, AMexpect(AM_VAL_TYPE_VOID)); \
type value; \
assert_true(AMitemTo##suffix(AMstackItem(stack_ptr, AMlistGet(doc_state->doc, list, 0, NULL), cmocka_cb, \
AMexpect(suffix_to_val_type(#suffix))), \
&value)); \
assert_true(value == scalar_value); \
AMresultFree(AMstackPop(stack_ptr, NULL)); \
}
#define test_AMlistPutBytes(mode) test_AMlistPutBytes##_##mode
@ -66,311 +63,364 @@ static void test_AMlistPut ## suffix ## _ ## mode(void **state) {
static void test_AMlistPutBytes_##mode(void** state) { \
static size_t const BYTES_SIZE = sizeof(bytes_value) / sizeof(uint8_t); \
\
GroupState* group_state = *state; \
AMobjId const* const list = AMpush( \
&group_state->stack, \
AMmapPutObject(group_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),\
AM_VALUE_OBJ_ID, \
cmocka_cb).obj_id; \
AMfree(AMlistPutBytes(group_state->doc, \
list, \
0, \
!strcmp(#mode, "insert"), \
AMbytes(bytes_value, BYTES_SIZE))); \
AMbyteSpan const bytes = AMpush( \
&group_state->stack, \
AMlistGet(group_state->doc, list, 0, NULL), \
AM_VALUE_BYTES, \
cmocka_cb).bytes; \
DocState* doc_state = *state; \
AMstack** stack_ptr = &doc_state->base_state->stack; \
AMobjId const* const list = AMitemObjId( \
AMstackItem(stack_ptr, AMmapPutObject(doc_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST), \
cmocka_cb, AMexpect(AM_VAL_TYPE_OBJ_TYPE))); \
AMstackItem( \
NULL, AMlistPutBytes(doc_state->doc, list, 0, !strcmp(#mode, "insert"), AMbytes(bytes_value, BYTES_SIZE)), \
cmocka_cb, AMexpect(AM_VAL_TYPE_VOID)); \
AMbyteSpan bytes; \
assert_true(AMitemToBytes( \
AMstackItem(stack_ptr, AMlistGet(doc_state->doc, list, 0, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_BYTES)), \
&bytes)); \
assert_int_equal(bytes.count, BYTES_SIZE); \
assert_memory_equal(bytes.src, bytes_value, BYTES_SIZE); \
AMfree(AMpop(&group_state->stack)); \
AMresultFree(AMstackPop(stack_ptr, NULL)); \
}
#define test_AMlistPutNull(mode) test_AMlistPutNull_##mode
#define static_void_test_AMlistPutNull(mode) \
static void test_AMlistPutNull_##mode(void** state) { \
GroupState* group_state = *state; \
AMobjId const* const list = AMpush( \
&group_state->stack, \
AMmapPutObject(group_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),\
AM_VALUE_OBJ_ID, \
cmocka_cb).obj_id; \
AMfree(AMlistPutNull(group_state->doc, \
list, \
0, \
!strcmp(#mode, "insert"))); \
AMresult* const result = AMlistGet(group_state->doc, list, 0, NULL); \
DocState* doc_state = *state; \
AMstack** stack_ptr = &doc_state->base_state->stack; \
AMobjId const* const list = AMitemObjId( \
AMstackItem(stack_ptr, AMmapPutObject(doc_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST), \
cmocka_cb, AMexpect(AM_VAL_TYPE_OBJ_TYPE))); \
AMstackItem(NULL, AMlistPutNull(doc_state->doc, list, 0, !strcmp(#mode, "insert")), cmocka_cb, \
AMexpect(AM_VAL_TYPE_VOID)); \
AMresult* result = AMstackResult(stack_ptr, AMlistGet(doc_state->doc, list, 0, NULL), NULL, NULL); \
if (AMresultStatus(result) != AM_STATUS_OK) { \
fail_msg_view("%s", AMerrorMessage(result)); \
fail_msg_view("%s", AMresultError(result)); \
} \
assert_int_equal(AMresultSize(result), 1); \
assert_int_equal(AMresultValue(result).tag, AM_VALUE_NULL); \
AMfree(result); \
assert_int_equal(AMitemValType(AMresultItem(result)), AM_VAL_TYPE_NULL); \
AMresultFree(AMstackPop(stack_ptr, NULL)); \
}
#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 list = AMpush( \
&group_state->stack, \
AMmapPutObject(group_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),\
AM_VALUE_OBJ_ID, \
cmocka_cb).obj_id; \
AMobjType const obj_type = AMobjType_tag(#label); \
if (obj_type != AM_OBJ_TYPE_VOID) { \
AMobjId const* const obj_id = AMpush( \
&group_state->stack, \
AMlistPutObject(group_state->doc, \
list, \
0, \
!strcmp(#mode, "insert"), \
obj_type), \
AM_VALUE_OBJ_ID, \
cmocka_cb).obj_id; \
DocState* doc_state = *state; \
AMstack** stack_ptr = &doc_state->base_state->stack; \
AMobjId const* const list = AMitemObjId( \
AMstackItem(stack_ptr, AMmapPutObject(doc_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST), \
cmocka_cb, AMexpect(AM_VAL_TYPE_OBJ_TYPE))); \
AMobjType const obj_type = suffix_to_obj_type(#label); \
AMobjId const* const obj_id = AMitemObjId( \
AMstackItem(stack_ptr, AMlistPutObject(doc_state->doc, list, 0, !strcmp(#mode, "insert"), obj_type), \
cmocka_cb, AMexpect(AM_VAL_TYPE_OBJ_TYPE))); \
assert_non_null(obj_id); \
assert_int_equal(AMobjObjType(group_state->doc, obj_id), obj_type); \
assert_int_equal(AMobjSize(group_state->doc, obj_id, NULL), 0); \
} \
else { \
AMpush(&group_state->stack, \
AMlistPutObject(group_state->doc, \
list, \
0, \
!strcmp(#mode, "insert"), \
obj_type), \
AM_VALUE_VOID, \
NULL); \
assert_int_not_equal(AMresultStatus(group_state->stack->result), \
AM_STATUS_OK); \
} \
AMfree(AMpop(&group_state->stack)); \
assert_int_equal(AMobjObjType(doc_state->doc, obj_id), obj_type); \
assert_int_equal(AMobjSize(doc_state->doc, obj_id, NULL), 0); \
AMresultFree(AMstackPop(stack_ptr, NULL)); \
}
#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; \
AMobjId const* const list = AMpush( \
&group_state->stack, \
AMmapPutObject(group_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),\
AM_VALUE_OBJ_ID, \
cmocka_cb).obj_id; \
AMfree(AMlistPutStr(group_state->doc, \
list, \
0, \
!strcmp(#mode, "insert"), \
AMstr(str_value))); \
AMbyteSpan const str = AMpush( \
&group_state->stack, \
AMlistGet(group_state->doc, list, 0, NULL), \
AM_VALUE_STR, \
cmocka_cb).str; \
DocState* doc_state = *state; \
AMstack** stack_ptr = &doc_state->base_state->stack; \
AMobjId const* const list = AMitemObjId( \
AMstackItem(stack_ptr, AMmapPutObject(doc_state->doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST), \
cmocka_cb, AMexpect(AM_VAL_TYPE_OBJ_TYPE))); \
AMstackItem(NULL, AMlistPutStr(doc_state->doc, list, 0, !strcmp(#mode, "insert"), AMstr(str_value)), \
cmocka_cb, AMexpect(AM_VAL_TYPE_VOID)); \
AMbyteSpan str; \
assert_true(AMitemToStr( \
AMstackItem(stack_ptr, AMlistGet(doc_state->doc, list, 0, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR)), \
&str)); \
assert_int_equal(str.count, strlen(str_value)); \
assert_memory_equal(str.src, str_value, str.count); \
AMfree(AMpop(&group_state->stack)); \
AMresultFree(AMstackPop(stack_ptr, NULL)); \
}
static_void_test_AMlistPut(Bool, insert, boolean, true)
static_void_test_AMlistPut(Bool, insert, bool, true);
static_void_test_AMlistPut(Bool, update, boolean, true)
static_void_test_AMlistPut(Bool, update, bool, 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(insert, BYTES_VALUE);
static_void_test_AMlistPutBytes(update, BYTES_VALUE)
static_void_test_AMlistPutBytes(update, BYTES_VALUE);
static_void_test_AMlistPut(Counter, insert, counter, INT64_MAX)
static_void_test_AMlistPut(Counter, insert, int64_t, INT64_MAX);
static_void_test_AMlistPut(Counter, update, counter, INT64_MAX)
static_void_test_AMlistPut(Counter, update, int64_t, INT64_MAX);
static_void_test_AMlistPut(F64, insert, f64, DBL_MAX)
static_void_test_AMlistPut(F64, insert, double, DBL_MAX);
static_void_test_AMlistPut(F64, update, f64, DBL_MAX)
static_void_test_AMlistPut(F64, update, double, DBL_MAX);
static_void_test_AMlistPut(Int, insert, int_, INT64_MAX)
static_void_test_AMlistPut(Int, insert, int64_t, INT64_MAX);
static_void_test_AMlistPut(Int, update, int_, INT64_MAX)
static_void_test_AMlistPut(Int, update, int64_t, INT64_MAX);
static_void_test_AMlistPutNull(insert)
static_void_test_AMlistPutNull(insert);
static_void_test_AMlistPutNull(update)
static_void_test_AMlistPutNull(update);
static_void_test_AMlistPutObject(List, insert)
static_void_test_AMlistPutObject(List, insert);
static_void_test_AMlistPutObject(List, update)
static_void_test_AMlistPutObject(List, update);
static_void_test_AMlistPutObject(Map, insert)
static_void_test_AMlistPutObject(Map, insert);
static_void_test_AMlistPutObject(Map, update)
static_void_test_AMlistPutObject(Map, update);
static_void_test_AMlistPutObject(Text, insert)
static_void_test_AMlistPutObject(Text, insert);
static_void_test_AMlistPutObject(Text, update)
static_void_test_AMlistPutObject(Text, update);
static_void_test_AMlistPutObject(Void, insert)
static_void_test_AMlistPutStr(insert,
"Hello, "
"world!");
static_void_test_AMlistPutObject(Void, update)
static_void_test_AMlistPutStr(update,
"Hello,"
" world"
"!");
static_void_test_AMlistPutStr(insert, "Hello, world!")
static_void_test_AMlistPut(Timestamp, insert, int64_t, INT64_MAX);
static_void_test_AMlistPutStr(update, "Hello, world!")
static_void_test_AMlistPut(Timestamp, update, int64_t, INT64_MAX);
static_void_test_AMlistPut(Timestamp, insert, timestamp, INT64_MAX)
static_void_test_AMlistPut(Uint, insert, uint64_t, UINT64_MAX);
static_void_test_AMlistPut(Timestamp, update, timestamp, INT64_MAX)
static_void_test_AMlistPut(Uint, update, uint64_t, UINT64_MAX);
static_void_test_AMlistPut(Uint, insert, uint, UINT64_MAX)
static_void_test_AMlistPut(Uint, update, uint, UINT64_MAX)
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, AMstr("list"), AM_OBJ_TYPE_LIST),
AM_VALUE_OBJ_ID,
cmocka_cb).obj_id;
static void test_get_range_values(void** state) {
BaseState* base_state = *state;
AMstack** stack_ptr = &base_state->stack;
AMdoc* doc1;
assert_true(AMitemToDoc(AMstackItem(stack_ptr, AMcreate(NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc1));
AMobjId const* const list =
AMitemObjId(AMstackItem(stack_ptr, AMmapPutObject(doc1, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST), cmocka_cb,
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
/* Insert elements. */
AMfree(AMlistPutStr(doc1, list, 0, true, AMstr("First")));
AMfree(AMlistPutStr(doc1, list, 0, true, AMstr("Second")));
AMfree(AMlistPutStr(doc1, list, 0, true, AMstr("Third")));
AMfree(AMlistPutStr(doc1, list, 0, true, AMstr("Fourth")));
AMfree(AMlistPutStr(doc1, list, 0, true, AMstr("Fifth")));
AMfree(AMlistPutStr(doc1, list, 0, true, AMstr("Sixth")));
AMfree(AMlistPutStr(doc1, list, 0, true, AMstr("Seventh")));
AMfree(AMlistPutStr(doc1, list, 0, true, AMstr("Eighth")));
AMfree(AMcommit(doc1, AMstr(NULL), NULL));
AMstackItem(NULL, AMlistPutStr(doc1, list, 0, true, AMstr("First")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutStr(doc1, list, 0, true, AMstr("Second")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutStr(doc1, list, 0, true, AMstr("Third")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutStr(doc1, list, 0, true, AMstr("Fourth")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutStr(doc1, list, 0, true, AMstr("Fifth")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutStr(doc1, list, 0, true, AMstr("Sixth")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutStr(doc1, list, 0, true, AMstr("Seventh")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutStr(doc1, list, 0, true, AMstr("Eighth")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMcommit(doc1, AMstr(NULL), NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
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;
AMitems const v1 = AMstackItems(stack_ptr, AMgetHeads(doc1), cmocka_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMdoc* doc2;
assert_true(AMitemToDoc(AMstackItem(stack_ptr, AMfork(doc1, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc2));
AMfree(AMlistPutStr(doc1, list, 2, false, AMstr("Third V2")));
AMfree(AMcommit(doc1, AMstr(NULL), NULL));
AMstackItem(NULL, AMlistPutStr(doc1, list, 2, false, AMstr("Third V2")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMcommit(doc1, AMstr(NULL), NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMfree(AMlistPutStr(doc2, list, 2, false, AMstr("Third V3")));
AMfree(AMcommit(doc2, AMstr(NULL), NULL));
AMstackItem(NULL, AMlistPutStr(doc2, list, 2, false, AMstr("Third V3")), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMcommit(doc2, AMstr(NULL), NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMfree(AMmerge(doc1, doc2));
AMstackItem(NULL, AMmerge(doc1, doc2), cmocka_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
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);
/* Forward vs. reverse: complete current list range. */
AMitems range =
AMstackItems(stack_ptr, AMlistRange(doc1, list, 0, SIZE_MAX, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
size_t size = AMitemsSize(&range);
assert_int_equal(size, 8);
AMitems range_back = AMitemsReversed(&range);
assert_int_equal(AMitemsSize(&range_back), size);
size_t pos;
assert_true(AMitemPos(AMitemsNext(&range, 1), &pos));
assert_int_equal(pos, 0);
assert_true(AMitemPos(AMitemsNext(&range_back, 1), &pos));
assert_int_equal(pos, 7);
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);
AMitem *item1, *item_back1;
size_t count, middle = size / 2;
range = AMitemsRewound(&range);
range_back = AMitemsRewound(&range_back);
for (item1 = NULL, item_back1 = NULL, count = 0; item1 && item_back1;
item1 = AMitemsNext(&range, 1), item_back1 = AMitemsNext(&range_back, 1), ++count) {
size_t pos1, pos_back1;
assert_true(AMitemPos(item1, &pos1));
assert_true(AMitemPos(item_back1, &pos_back1));
if ((count == middle) && (middle & 1)) {
/* The iterators are crossing in the middle. */
assert_int_equal(pos1, pos_back1);
assert_true(AMitemEqual(item1, item_back1));
assert_true(AMobjIdEqual(AMitemObjId(item1), AMitemObjId(item_back1)));
} else {
assert_int_not_equal(pos1, pos_back1);
}
AMitem* item2 = AMstackItem(stack_ptr, AMlistGet(doc1, list, pos1, NULL), NULL, NULL);
AMitem* item_back2 = AMstackItem(stack_ptr, AMlistGet(doc1, list, pos_back1, NULL), NULL, NULL);
/** \note An item returned from an `AM...Get()` call doesn't include the
index used to retrieve it. */
assert_false(AMitemIdxType(item2));
assert_false(AMitemIdxType(item_back2));
assert_true(AMitemEqual(item1, item2));
assert_true(AMobjIdEqual(AMitemObjId(item1), AMitemObjId(item2)));
assert_true(AMitemEqual(item_back1, item_back2));
assert_true(AMobjIdEqual(AMitemObjId(item_back1), AMitemObjId(item_back2)));
AMresultFree(AMstackPop(stack_ptr, NULL));
}
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);
/* Forward vs. reverse: partial current list range. */
range = AMstackItems(stack_ptr, AMlistRange(doc1, list, 1, 6, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
size = AMitemsSize(&range);
assert_int_equal(size, 5);
range_back = AMitemsReversed(&range);
assert_int_equal(AMitemsSize(&range_back), size);
assert_true(AMitemPos(AMitemsNext(&range, 1), &pos));
assert_int_equal(pos, 1);
assert_true(AMitemPos(AMitemsNext(&range_back, 1), &pos));
assert_int_equal(pos, 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);
middle = size / 2;
range = AMitemsRewound(&range);
range_back = AMitemsRewound(&range_back);
for (item1 = NULL, item_back1 = NULL, count = 0; item1 && item_back1;
item1 = AMitemsNext(&range, 1), item_back1 = AMitemsNext(&range_back, 1), ++count) {
size_t pos1, pos_back1;
assert_true(AMitemPos(item1, &pos1));
assert_true(AMitemPos(item_back1, &pos_back1));
if ((count == middle) && (middle & 1)) {
/* The iterators are crossing in the middle. */
assert_int_equal(pos1, pos_back1);
assert_true(AMitemEqual(item1, item_back1));
assert_true(AMobjIdEqual(AMitemObjId(item1), AMitemObjId(item_back1)));
} else {
assert_int_not_equal(pos1, pos_back1);
}
AMitem* item2 = AMstackItem(stack_ptr, AMlistGet(doc1, list, pos1, NULL), NULL, NULL);
AMitem* item_back2 = AMstackItem(stack_ptr, AMlistGet(doc1, list, pos_back1, NULL), NULL, NULL);
/** \note An item returned from an `AMlistGet()` call doesn't include
the index used to retrieve it. */
assert_int_equal(AMitemIdxType(item2), 0);
assert_int_equal(AMitemIdxType(item_back2), 0);
assert_true(AMitemEqual(item1, item2));
assert_true(AMobjIdEqual(AMitemObjId(item1), AMitemObjId(item2)));
assert_true(AMitemEqual(item_back1, item_back2));
assert_true(AMobjIdEqual(AMitemObjId(item_back1), AMitemObjId(item_back2)));
AMresultFree(AMstackPop(stack_ptr, NULL));
}
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);
/* Forward vs. reverse: complete historical map range. */
range = AMstackItems(stack_ptr, AMlistRange(doc1, list, 0, SIZE_MAX, &v1), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
size = AMitemsSize(&range);
assert_int_equal(size, 8);
range_back = AMitemsReversed(&range);
assert_int_equal(AMitemsSize(&range_back), size);
assert_true(AMitemPos(AMitemsNext(&range, 1), &pos));
assert_int_equal(pos, 0);
assert_true(AMitemPos(AMitemsNext(&range_back, 1), &pos));
assert_int_equal(pos, 7);
middle = size / 2;
range = AMitemsRewound(&range);
range_back = AMitemsRewound(&range_back);
for (item1 = NULL, item_back1 = NULL, count = 0; item1 && item_back1;
item1 = AMitemsNext(&range, 1), item_back1 = AMitemsNext(&range_back, 1), ++count) {
size_t pos1, pos_back1;
assert_true(AMitemPos(item1, &pos1));
assert_true(AMitemPos(item_back1, &pos_back1));
if ((count == middle) && (middle & 1)) {
/* The iterators are crossing in the middle. */
assert_int_equal(pos1, pos_back1);
assert_true(AMitemEqual(item1, item_back1));
assert_true(AMobjIdEqual(AMitemObjId(item1), AMitemObjId(item_back1)));
} else {
assert_int_not_equal(pos1, pos_back1);
}
AMitem* item2 = AMstackItem(stack_ptr, AMlistGet(doc1, list, pos1, &v1), NULL, NULL);
AMitem* item_back2 = AMstackItem(stack_ptr, AMlistGet(doc1, list, pos_back1, &v1), NULL, NULL);
/** \note An item returned from an `AM...Get()` call doesn't include the
index used to retrieve it. */
assert_false(AMitemIdxType(item2));
assert_false(AMitemIdxType(item_back2));
assert_true(AMitemEqual(item1, item2));
assert_true(AMobjIdEqual(AMitemObjId(item1), AMitemObjId(item2)));
assert_true(AMitemEqual(item_back1, item_back2));
assert_true(AMobjIdEqual(AMitemObjId(item_back1), AMitemObjId(item_back2)));
AMresultFree(AMstackPop(stack_ptr, NULL));
}
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);
/* Forward vs. reverse: partial historical map range. */
range = AMstackItems(stack_ptr, AMlistRange(doc1, list, 2, 7, &v1), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
size = AMitemsSize(&range);
assert_int_equal(size, 5);
range_back = AMitemsReversed(&range);
assert_int_equal(AMitemsSize(&range_back), size);
assert_true(AMitemPos(AMitemsNext(&range, 1), &pos));
assert_int_equal(pos, 2);
assert_true(AMitemPos(AMitemsNext(&range_back, 1), &pos));
assert_int_equal(pos, 6);
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);
middle = size / 2;
range = AMitemsRewound(&range);
range_back = AMitemsRewound(&range_back);
for (item1 = NULL, item_back1 = NULL, count = 0; item1 && item_back1;
item1 = AMitemsNext(&range, 1), item_back1 = AMitemsNext(&range_back, 1), ++count) {
size_t pos1, pos_back1;
assert_true(AMitemPos(item1, &pos1));
assert_true(AMitemPos(item_back1, &pos_back1));
if ((count == middle) && (middle & 1)) {
/* The iterators are crossing in the middle. */
assert_int_equal(pos1, pos_back1);
assert_true(AMitemEqual(item1, item_back1));
assert_true(AMobjIdEqual(AMitemObjId(item1), AMitemObjId(item_back1)));
} else {
assert_int_not_equal(pos1, pos_back1);
}
AMitem* item2 = AMstackItem(stack_ptr, AMlistGet(doc1, list, pos1, &v1), NULL, NULL);
AMitem* item_back2 = AMstackItem(stack_ptr, AMlistGet(doc1, list, pos_back1, &v1), NULL, NULL);
/** \note An item returned from an `AM...Get()` call doesn't include the
index used to retrieve it. */
assert_false(AMitemIdxType(item2));
assert_false(AMitemIdxType(item_back2));
assert_true(AMitemEqual(item1, item2));
assert_true(AMobjIdEqual(AMitemObjId(item1), AMitemObjId(item2)));
assert_true(AMitemEqual(item_back1, item_back2));
assert_true(AMobjIdEqual(AMitemObjId(item_back1), AMitemObjId(item_back2)));
AMresultFree(AMstackPop(stack_ptr, NULL));
}
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)));
/* List range vs. object range: complete current. */
range = AMstackItems(stack_ptr, AMlistRange(doc1, list, 0, SIZE_MAX, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
AMitems obj_items = AMstackItems(stack_ptr, AMobjItems(doc1, list, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
assert_int_equal(AMitemsSize(&range), AMitemsSize(&obj_items));
AMitem *item, *obj_item;
for (item = NULL, obj_item = NULL; item && obj_item;
item = AMitemsNext(&range, 1), obj_item = AMitemsNext(&obj_items, 1)) {
/** \note Object iteration doesn't yield any item indices. */
assert_true(AMitemIdxType(item));
assert_false(AMitemIdxType(obj_item));
assert_true(AMitemEqual(item, obj_item));
assert_true(AMobjIdEqual(AMitemObjId(item), AMitemObjId(obj_item)));
}
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)));
/* List range vs. object range: complete historical. */
range = AMstackItems(stack_ptr, AMlistRange(doc1, list, 0, SIZE_MAX, &v1), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
obj_items = AMstackItems(stack_ptr, AMobjItems(doc1, list, &v1), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
assert_int_equal(AMitemsSize(&range), AMitemsSize(&obj_items));
for (item = NULL, obj_item = NULL; item && obj_item;
item = AMitemsNext(&range, 1), obj_item = AMitemsNext(&obj_items, 1)) {
/** \note Object iteration doesn't yield any item indices. */
assert_true(AMitemIdxType(item));
assert_false(AMitemIdxType(obj_item));
assert_true(AMitemEqual(item, obj_item));
assert_true(AMobjIdEqual(AMitemObjId(item), AMitemObjId(obj_item)));
}
}
/** \brief A JavaScript application can introduce NUL (`\0`) characters into a
/**
* \brief A JavaScript application can introduce NUL (`\0`) characters into a
* list object's string value which will truncate it in a C application.
*/
static void test_get_NUL_string_value(void** state) {
@ -381,60 +431,52 @@ static void test_get_NUL_string_value(void** state) {
doc[0] = 'o\0ps';
});
const bytes = Automerge.save(doc);
console.log("static uint8_t const SAVED_DOC[] = {" + Array.apply([], bytes).join(", ") + "};");
console.log("static uint8_t const SAVED_DOC[] = {" + Array.apply([],
bytes).join(", ") + "};");
*/
static uint8_t const OOPS_VALUE[] = {'o', '\0', 'p', 's'};
static size_t const OOPS_SIZE = sizeof(OOPS_VALUE) / sizeof(uint8_t);
static uint8_t const SAVED_DOC[] = {
133, 111, 74, 131, 224, 28, 197, 17, 0, 113, 1, 16, 246, 137, 63, 193,
255, 181, 76, 79, 129, 213, 133, 29, 214, 158, 164, 15, 1, 207, 184,
14, 57, 1, 194, 79, 247, 82, 160, 134, 227, 144, 5, 241, 136, 205,
238, 250, 251, 54, 34, 250, 210, 96, 204, 132, 153, 203, 110, 109, 6,
6, 1, 2, 3, 2, 19, 2, 35, 2, 64, 2, 86, 2, 8, 21, 3, 33, 2, 35, 2, 52,
1, 66, 2, 86, 2, 87, 4, 128, 1, 2, 127, 0, 127, 1, 127, 1, 127, 0,
127, 0, 127, 7, 127, 1, 48, 127, 0, 127, 1, 1, 127, 1, 127, 70, 111,
0, 112, 115, 127, 0, 0};
133, 111, 74, 131, 224, 28, 197, 17, 0, 113, 1, 16, 246, 137, 63, 193, 255, 181, 76, 79, 129,
213, 133, 29, 214, 158, 164, 15, 1, 207, 184, 14, 57, 1, 194, 79, 247, 82, 160, 134, 227, 144,
5, 241, 136, 205, 238, 250, 251, 54, 34, 250, 210, 96, 204, 132, 153, 203, 110, 109, 6, 6, 1,
2, 3, 2, 19, 2, 35, 2, 64, 2, 86, 2, 8, 21, 3, 33, 2, 35, 2, 52, 1, 66,
2, 86, 2, 87, 4, 128, 1, 2, 127, 0, 127, 1, 127, 1, 127, 0, 127, 0, 127, 7, 127,
1, 48, 127, 0, 127, 1, 1, 127, 1, 127, 70, 111, 0, 112, 115, 127, 0, 0};
static size_t const SAVED_DOC_SIZE = sizeof(SAVED_DOC) / sizeof(uint8_t);
AMresultStack* stack = *state;
AMdoc* const doc = AMpush(&stack,
AMload(SAVED_DOC, SAVED_DOC_SIZE),
AM_VALUE_DOC,
cmocka_cb).doc;
AMbyteSpan const str = AMpush(&stack,
AMlistGet(doc, AM_ROOT, 0, NULL),
AM_VALUE_STR,
cmocka_cb).str;
BaseState* base_state = *state;
AMstack** stack_ptr = &base_state->stack;
AMdoc* doc;
assert_true(AMitemToDoc(
AMstackItem(stack_ptr, AMload(SAVED_DOC, SAVED_DOC_SIZE), cmocka_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc));
AMbyteSpan str;
assert_true(AMitemToStr(
AMstackItem(stack_ptr, AMlistGet(doc, AM_ROOT, 0, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR)), &str));
assert_int_not_equal(str.count, strlen(OOPS_VALUE));
assert_int_equal(str.count, OOPS_SIZE);
assert_memory_equal(str.src, OOPS_VALUE, str.count);
}
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,
AMmapPutObject(doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST),
AM_VALUE_OBJ_ID,
cmocka_cb).obj_id;
BaseState* base_state = *state;
AMstack** stack_ptr = &base_state->stack;
AMdoc* doc;
assert_true(AMitemToDoc(AMstackItem(stack_ptr, AMcreate(NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc));
AMobjId const* const list =
AMitemObjId(AMstackItem(stack_ptr, AMmapPutObject(doc, AM_ROOT, AMstr("list"), AM_OBJ_TYPE_LIST), cmocka_cb,
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
/* Insert both at the same index. */
AMfree(AMlistPutUint(doc, list, 0, true, 0));
AMfree(AMlistPutUint(doc, list, 0, true, 1));
AMstackItem(NULL, AMlistPutUint(doc, list, 0, true, 0), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMlistPutUint(doc, list, 0, true, 1), cmocka_cb, AMexpect(AM_VAL_TYPE_VOID));
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);
AMitems const keys = AMstackItems(stack_ptr, AMkeys(doc, list, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_STR));
assert_int_equal(AMitemsSize(&keys), 2);
AMitems const range =
AMstackItems(stack_ptr, AMlistRange(doc, list, 0, SIZE_MAX, NULL), cmocka_cb, AMexpect(AM_VAL_TYPE_UINT));
assert_int_equal(AMitemsSize(&range), 2);
}
int run_list_tests(void) {
@ -458,18 +500,16 @@ int run_list_tests(void) {
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_AMlistPutObject(Void, insert)),
cmocka_unit_test(test_AMlistPutObject(Void, 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_get_list_values, setup_stack, teardown_stack),
cmocka_unit_test_setup_teardown(test_get_NUL_string_value, setup_stack, teardown_stack),
cmocka_unit_test_setup_teardown(test_insert_at_index, setup_stack, teardown_stack),
cmocka_unit_test_setup_teardown(test_get_range_values, setup_base, teardown_base),
cmocka_unit_test_setup_teardown(test_get_NUL_string_value, setup_base, teardown_base),
cmocka_unit_test_setup_teardown(test_insert_at_index, setup_base, teardown_base),
};
return cmocka_run_group_tests(tests, group_setup, group_teardown);
return cmocka_run_group_tests(tests, setup_doc, teardown_doc);
}

View file

@ -3,23 +3,36 @@
/* 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 suffix_to_obj_type(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 AM_OBJ_TYPE_DEFAULT;
}
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 if (!strcmp(obj_type_label, "Void")) return AM_OBJ_TYPE_VOID;
else return 0;
AMvalType suffix_to_val_type(char const* suffix) {
if (!strcmp(suffix, "Bool"))
return AM_VAL_TYPE_BOOL;
else if (!strcmp(suffix, "Bytes"))
return AM_VAL_TYPE_BYTES;
else if (!strcmp(suffix, "Counter"))
return AM_VAL_TYPE_COUNTER;
else if (!strcmp(suffix, "F64"))
return AM_VAL_TYPE_F64;
else if (!strcmp(suffix, "Int"))
return AM_VAL_TYPE_INT;
else if (!strcmp(suffix, "Null"))
return AM_VAL_TYPE_NULL;
else if (!strcmp(suffix, "Str"))
return AM_VAL_TYPE_STR;
else if (!strcmp(suffix, "Timestamp"))
return AM_VAL_TYPE_TIMESTAMP;
else if (!strcmp(suffix, "Uint"))
return AM_VAL_TYPE_UINT;
else
return AM_VAL_TYPE_DEFAULT;
}

View file

@ -1,24 +1,23 @@
#ifndef MACRO_UTILS_H
#define MACRO_UTILS_H
#ifndef TESTS_MACRO_UTILS_H
#define TESTS_MACRO_UTILS_H
/* local */
#include <automerge-c/automerge.h>
/**
* \brief Gets the result value discriminant corresponding to a function name
* suffix.
* \brief Gets the object type tag corresponding to an object type 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.
* \param[in] suffix An object type suffix string.
* \return An `AMobjType` enum tag.
*/
AMobjType AMobjType_tag(char const* obj_type_label);
AMobjType suffix_to_obj_type(char const* suffix);
#endif /* MACRO_UTILS_H */
/**
* \brief Gets the value type tag corresponding to a value type suffix.
*
* \param[in] suffix A value type suffix string.
* \return An `AMvalType` enum tag.
*/
AMvalType suffix_to_val_type(char const* suffix);
#endif /* TESTS_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,8 +8,14 @@
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);
@ -17,11 +23,6 @@ extern int run_map_tests(void);
extern int run_ported_wasm_suite(void);
int main(void) {
return (
run_actor_id_tests() +
run_doc_tests() +
run_list_tests() +
run_map_tests() +
run_ported_wasm_suite()
);
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());
}

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,8 +11,5 @@ 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());
}

File diff suppressed because it is too large Load diff

View file

@ -1,31 +0,0 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
/* third-party */
#include <cmocka.h>
/* local */
#include "cmocka_utils.h"
#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_view("%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

@ -1,38 +0,0 @@
#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 <stdio.h>
#include <stdint.h>
#include <stdio.h>
/* local */
#include "str_utils.h"

View file

@ -1,14 +1,17 @@
#ifndef STR_UTILS_H
#define STR_UTILS_H
#ifndef TESTS_STR_UTILS_H
#define TESTS_STR_UTILS_H
/**
* \brief Converts a hexadecimal string into a sequence of bytes.
* \brief Converts a hexadecimal string into an array 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.
* \param[in] hex_str A hexadecimal string.
* \param[in] src A pointer to an array of bytes.
* \param[in] count The count of bytes to copy into the array pointed to by
* \p src.
* \pre \p src `!= NULL`
* \pre `sizeof(`\p src `) > 0`
* \pre \p count `<= sizeof(`\p src `)`
*/
void hex_to_bytes(char const* hex_str, uint8_t* src, size_t const count);
#endif /* STR_UTILS_H */
#endif /* TESTS_STR_UTILS_H */

View file

@ -464,6 +464,7 @@ impl Automerge {
return Err(load::Error::BadChecksum.into());
}
let mut change: Option<Change> = None;
let mut am = match first_chunk {
storage::Chunk::Document(d) => {
tracing::trace!("first chunk is document chunk, inflating");
@ -501,30 +502,31 @@ impl Automerge {
}
}
storage::Chunk::Change(stored_change) => {
tracing::trace!("first chunk is change chunk, applying");
let change = Change::new_from_unverified(stored_change.into_owned(), None)
.map_err(|e| load::Error::InvalidChangeColumns(Box::new(e)))?;
let mut am = Self::new();
am.apply_change(change, &mut observer);
am
tracing::trace!("first chunk is change chunk");
change = Some(
Change::new_from_unverified(stored_change.into_owned(), None)
.map_err(|e| load::Error::InvalidChangeColumns(Box::new(e)))?,
);
Self::new()
}
storage::Chunk::CompressedChange(stored_change, compressed) => {
tracing::trace!("first chunk is compressed change, decompressing and applying");
let change = Change::new_from_unverified(
tracing::trace!("first chunk is compressed change");
change = Some(
Change::new_from_unverified(
stored_change.into_owned(),
Some(compressed.into_owned()),
)
.map_err(|e| load::Error::InvalidChangeColumns(Box::new(e)))?;
let mut am = Self::new();
am.apply_change(change, &mut observer);
am
.map_err(|e| load::Error::InvalidChangeColumns(Box::new(e)))?,
);
Self::new()
}
};
tracing::trace!("first chunk loaded, loading remaining chunks");
tracing::trace!("loading change chunks");
match load::load_changes(remaining.reset()) {
load::LoadedChanges::Complete(c) => {
for change in c {
am.apply_change(change, &mut observer);
am.apply_changes(change.into_iter().chain(c))?;
if !am.queue.is_empty() {
return Err(AutomergeError::MissingDeps);
}
}
load::LoadedChanges::Partial { error, .. } => {
@ -721,7 +723,7 @@ impl Automerge {
obj,
Op {
id,
action: OpType::from_index_and_value(c.action, c.val).unwrap(),
action: OpType::from_action_and_value(c.action, c.val),
key,
succ: Default::default(),
pred,
@ -897,7 +899,6 @@ impl Automerge {
.add_change(&change, actor_index)
.expect("Change's deps should already be in the document");
self.history_index.insert(change.hash(), history_index);
self.history.push(change);
history_index

View file

@ -338,9 +338,9 @@ impl<'a, I: Iterator<Item = SimpleAction<'a>>> Iterator for TextActions<'a, I> {
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use std::{borrow::Cow, fs};
use crate::{transaction::Transactable, ObjType, OpObserver, Prop, ReadDoc, Value};
use crate::{transaction::Transactable, Automerge, ObjType, OpObserver, Prop, ReadDoc, Value};
// Observer ops often carry a "tagged value", which is a value and the OpID of the op which
// created that value. For a lot of values (i.e. any scalar value) we don't care about the
@ -887,4 +887,29 @@ mod tests {
])
);
}
#[test]
fn test_load_changes() {
fn fixture(name: &str) -> Vec<u8> {
fs::read("./tests/fixtures/".to_owned() + name).unwrap()
}
let mut obs = ObserverStub::new();
let _doc = Automerge::load_with(
&fixture("counter_value_is_ok.automerge"),
crate::OnPartialLoad::Error,
crate::storage::VerificationMode::Check,
Some(&mut obs),
);
assert_eq!(
Calls(obs.ops),
Calls(vec![ObserverCall::Put {
obj: crate::ROOT,
prop: "a".into(),
value: ObservedValue::Untagged(crate::ScalarValue::Counter(2000.into()).into()),
conflict: false,
},])
);
}
}

View file

@ -278,7 +278,7 @@ impl From<&Change> for crate::ExpandedChange {
let operations = c
.iter_ops()
.map(|o| crate::legacy::Op {
action: crate::types::OpType::from_index_and_value(o.action, o.val).unwrap(),
action: crate::types::OpType::from_action_and_value(o.action, o.val),
insert: o.insert,
key: match o.key {
StoredKey::Elem(e) if e.is_head() => {

View file

@ -104,11 +104,11 @@ impl<'a> OpIdIter<'a> {
.transpose()
.map_err(|e| DecodeColumnError::decode_raw("counter", e))?;
match (actor, counter) {
(Some(Some(a)), Some(Some(c))) => match c.try_into() {
Ok(c) => Ok(Some(OpId::new(c, a as usize))),
(Some(Some(a)), Some(Some(c))) => match u32::try_from(c) {
Ok(c) => Ok(Some(OpId::new(c as u64, a as usize))),
Err(_) => Err(DecodeColumnError::invalid_value(
"counter",
"negative value encountered",
"negative or large value encountered",
)),
},
(Some(None), _) => Err(DecodeColumnError::unexpected_null("actor")),

View file

@ -1,5 +1,5 @@
#[derive(Clone, Debug)]
pub(crate) struct DecodeColumnError {
pub struct DecodeColumnError {
path: Path,
error: DecodeColErrorKind,
}

View file

@ -139,7 +139,7 @@ pub(crate) fn option_splice_scenario<
}
pub(crate) fn opid() -> impl Strategy<Value = OpId> + Clone {
(0..(i64::MAX as usize), 0..(i64::MAX as u64)).prop_map(|(actor, ctr)| OpId::new(ctr, actor))
(0..(u32::MAX as usize), 0..(u32::MAX as u64)).prop_map(|(actor, ctr)| OpId::new(ctr, actor))
}
pub(crate) fn elemid() -> impl Strategy<Value = ElemId> + Clone {

View file

@ -1,3 +1,4 @@
use crate::change::LoadError as LoadChangeError;
use crate::storage::load::Error as LoadError;
use crate::types::{ActorId, ScalarValue};
use crate::value::DataType;
@ -18,6 +19,8 @@ pub enum AutomergeError {
Fail,
#[error("invalid actor ID `{0}`")]
InvalidActorId(String),
#[error(transparent)]
InvalidChangeHashBytes(#[from] InvalidChangeHashSlice),
#[error("invalid UTF-8 character at {0}")]
InvalidCharacter(usize),
#[error("invalid hash {0}")]
@ -39,10 +42,14 @@ pub enum AutomergeError {
},
#[error(transparent)]
Load(#[from] LoadError),
#[error(transparent)]
LoadChangeError(#[from] LoadChangeError),
#[error("increment operations must be against a counter value")]
MissingCounter,
#[error("hash {0} does not correspond to a change in this document")]
MissingHash(ChangeHash),
#[error("change's deps should already be in the document")]
MissingDeps,
#[error("compressed chunk was not a change")]
NonChangeCompressed,
#[error("id was not an object id")]
@ -92,7 +99,7 @@ pub struct InvalidElementId(pub String);
pub struct InvalidOpId(pub String);
#[derive(Error, Debug)]
pub(crate) enum InvalidOpType {
pub enum InvalidOpType {
#[error("unrecognized action index {0}")]
UnknownAction(u64),
#[error("non numeric argument for inc op")]

View file

@ -177,6 +177,9 @@ impl<'a> Change<'a, Unverified> {
for op in self.iter_ops() {
f(op?);
}
if u32::try_from(u64::from(self.start_op)).is_err() {
return Err(ReadChangeOpError::CounterTooLarge);
}
Ok(Change {
bytes: self.bytes,
header: self.header,

View file

@ -14,6 +14,7 @@ use crate::{
},
},
convert,
error::InvalidOpType,
storage::{
change::AsChangeOp,
columns::{
@ -22,6 +23,7 @@ use crate::{
RawColumns,
},
types::{ElemId, ObjId, OpId, ScalarValue},
OpType,
};
const OBJ_COL_ID: ColumnId = ColumnId::new(0);
@ -276,7 +278,14 @@ impl ChangeOpsColumns {
#[derive(thiserror::Error, Debug)]
#[error(transparent)]
pub struct ReadChangeOpError(#[from] DecodeColumnError);
pub enum ReadChangeOpError {
#[error(transparent)]
DecodeError(#[from] DecodeColumnError),
#[error(transparent)]
InvalidOpType(#[from] InvalidOpType),
#[error("counter too large")]
CounterTooLarge,
}
#[derive(Clone)]
pub(crate) struct ChangeOpsIter<'a> {
@ -308,6 +317,11 @@ impl<'a> ChangeOpsIter<'a> {
let action = self.action.next_in_col("action")?;
let val = self.val.next_in_col("value")?;
let pred = self.pred.next_in_col("pred")?;
// This check is necessary to ensure that OpType::from_action_and_value
// cannot panic later in the process.
OpType::validate_action_and_value(action, &val)?;
Ok(Some(ChangeOp {
obj,
key,
@ -458,10 +472,14 @@ mod tests {
action in 0_u64..6,
obj in opid(),
insert in any::<bool>()) -> ChangeOp {
let val = if action == 5 && !(value.is_int() || value.is_uint()) {
ScalarValue::Uint(0)
} else { value };
ChangeOp {
obj: obj.into(),
key,
val: value,
val,
pred,
action,
insert,

View file

@ -308,6 +308,7 @@ impl<'a> Input<'a> {
}
/// The bytes behind this input - including bytes which have been consumed
#[allow(clippy::misnamed_getters)]
pub(crate) fn bytes(&self) -> &'a [u8] {
self.original
}

View file

@ -216,23 +216,35 @@ impl OpType {
}
}
pub(crate) fn from_index_and_value(
index: u64,
value: ScalarValue,
) -> Result<OpType, error::InvalidOpType> {
match index {
0 => Ok(Self::Make(ObjType::Map)),
1 => Ok(Self::Put(value)),
2 => Ok(Self::Make(ObjType::List)),
3 => Ok(Self::Delete),
4 => Ok(Self::Make(ObjType::Text)),
pub(crate) fn validate_action_and_value(
action: u64,
value: &ScalarValue,
) -> Result<(), error::InvalidOpType> {
match action {
0..=4 => Ok(()),
5 => match value {
ScalarValue::Int(i) => Ok(Self::Increment(i)),
ScalarValue::Uint(i) => Ok(Self::Increment(i as i64)),
ScalarValue::Int(_) | ScalarValue::Uint(_) => Ok(()),
_ => Err(error::InvalidOpType::NonNumericInc),
},
6 => Ok(Self::Make(ObjType::Table)),
other => Err(error::InvalidOpType::UnknownAction(other)),
6 => Ok(()),
_ => Err(error::InvalidOpType::UnknownAction(action)),
}
}
pub(crate) fn from_action_and_value(action: u64, value: ScalarValue) -> OpType {
match action {
0 => Self::Make(ObjType::Map),
1 => Self::Put(value),
2 => Self::Make(ObjType::List),
3 => Self::Delete,
4 => Self::Make(ObjType::Text),
5 => match value {
ScalarValue::Int(i) => Self::Increment(i),
ScalarValue::Uint(i) => Self::Increment(i as i64),
_ => unreachable!("validate_action_and_value returned NonNumericInc"),
},
6 => Self::Make(ObjType::Table),
_ => unreachable!("validate_action_and_value returned UnknownAction"),
}
}
}
@ -427,17 +439,17 @@ pub(crate) struct OpId(u32, u32);
impl OpId {
pub(crate) fn new(counter: u64, actor: usize) -> Self {
Self(counter as u32, actor as u32)
Self(counter.try_into().unwrap(), actor.try_into().unwrap())
}
#[inline]
pub(crate) fn counter(&self) -> u64 {
self.0 as u64
self.0.into()
}
#[inline]
pub(crate) fn actor(&self) -> usize {
self.1 as usize
self.1.try_into().unwrap()
}
#[inline]

View file

@ -129,7 +129,7 @@ mod tests {
fn gen_opid(actors: Vec<ActorId>) -> impl Strategy<Value = OpId> {
(0..actors.len()).prop_flat_map(|actor_idx| {
(Just(actor_idx), 0..u64::MAX)
(Just(actor_idx), 0..(u32::MAX as u64))
.prop_map(|(actor_idx, counter)| OpId::new(counter, actor_idx))
})
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

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