aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects')
-rw-r--r--subprojects/exess/.clang-format30
-rw-r--r--subprojects/exess/.clang-tidy7
-rw-r--r--subprojects/exess/.clant.json4
-rw-r--r--subprojects/exess/.gitignore1
-rw-r--r--subprojects/exess/.gitlab-ci.yml168
-rw-r--r--subprojects/exess/.includes.imp10
-rw-r--r--subprojects/exess/NEWS5
-rw-r--r--subprojects/exess/README.md83
-rw-r--r--subprojects/exess/bindings/cpp/include/exess/exess.hpp707
-rw-r--r--subprojects/exess/bindings/cpp/meson.build54
-rw-r--r--subprojects/exess/bindings/cpp/test/.clang-tidy17
-rw-r--r--subprojects/exess/bindings/cpp/test/meson.build8
-rw-r--r--subprojects/exess/bindings/cpp/test/test_exess_hpp.cpp221
-rw-r--r--subprojects/exess/doc/c/Doxyfile.in39
-rw-r--r--subprojects/exess/doc/c/api/meson.build5
-rw-r--r--subprojects/exess/doc/c/index.rst10
-rw-r--r--subprojects/exess/doc/c/man/manlink.in1
-rw-r--r--subprojects/exess/doc/c/man/meson.build122
-rw-r--r--subprojects/exess/doc/c/meson.build53
-rw-r--r--subprojects/exess/doc/c/overview.rst151
-rw-r--r--subprojects/exess/doc/c/xml/meson.build12
-rw-r--r--subprojects/exess/doc/conf.py.in112
-rw-r--r--subprojects/exess/doc/cpp/Doxyfile.in44
-rw-r--r--subprojects/exess/doc/cpp/api/meson.build5
-rw-r--r--subprojects/exess/doc/cpp/index.rst11
-rw-r--r--subprojects/exess/doc/cpp/meson.build42
-rw-r--r--subprojects/exess/doc/cpp/overview.rst151
-rw-r--r--subprojects/exess/doc/cpp/xml/meson.build14
-rw-r--r--subprojects/exess/doc/meson.build17
-rw-r--r--subprojects/exess/doc/summary.rst3
-rw-r--r--subprojects/exess/include/exess/exess.h1633
-rw-r--r--subprojects/exess/meson.build236
-rw-r--r--subprojects/exess/meson/meson.build204
-rw-r--r--subprojects/exess/meson_options.txt17
-rwxr-xr-xsubprojects/exess/scripts/dox_to_sphinx.py675
-rw-r--r--subprojects/exess/src/.clang-tidy12
-rw-r--r--subprojects/exess/src/attributes.h30
-rw-r--r--subprojects/exess/src/base64.c163
-rw-r--r--subprojects/exess/src/bigint.c605
-rw-r--r--subprojects/exess/src/bigint.h118
-rw-r--r--subprojects/exess/src/boolean.c69
-rw-r--r--subprojects/exess/src/byte.c45
-rw-r--r--subprojects/exess/src/canonical.c309
-rw-r--r--subprojects/exess/src/coerce.c422
-rw-r--r--subprojects/exess/src/datatype.c79
-rw-r--r--subprojects/exess/src/date.c112
-rw-r--r--subprojects/exess/src/date_utils.h65
-rw-r--r--subprojects/exess/src/datetime.c265
-rw-r--r--subprojects/exess/src/decimal.c267
-rw-r--r--subprojects/exess/src/decimal.h63
-rw-r--r--subprojects/exess/src/digits.c243
-rw-r--r--subprojects/exess/src/digits.h38
-rw-r--r--subprojects/exess/src/double.c53
-rw-r--r--subprojects/exess/src/duration.c322
-rw-r--r--subprojects/exess/src/exess_config.h79
-rw-r--r--subprojects/exess/src/float.c44
-rw-r--r--subprojects/exess/src/hex.c131
-rw-r--r--subprojects/exess/src/ieee_float.h63
-rw-r--r--subprojects/exess/src/int.c45
-rw-r--r--subprojects/exess/src/int_math.c78
-rw-r--r--subprojects/exess/src/int_math.h72
-rw-r--r--subprojects/exess/src/long.c110
-rw-r--r--subprojects/exess/src/macros.h31
-rw-r--r--subprojects/exess/src/read_utils.c51
-rw-r--r--subprojects/exess/src/read_utils.h77
-rw-r--r--subprojects/exess/src/scientific.c125
-rw-r--r--subprojects/exess/src/scientific.h35
-rw-r--r--subprojects/exess/src/short.c45
-rw-r--r--subprojects/exess/src/soft_float.c161
-rw-r--r--subprojects/exess/src/soft_float.h71
-rw-r--r--subprojects/exess/src/strerror.c70
-rw-r--r--subprojects/exess/src/string_utils.h74
-rw-r--r--subprojects/exess/src/strtod.c405
-rw-r--r--subprojects/exess/src/strtod.h33
-rw-r--r--subprojects/exess/src/time.c172
-rw-r--r--subprojects/exess/src/time_utils.h35
-rw-r--r--subprojects/exess/src/timezone.c150
-rw-r--r--subprojects/exess/src/timezone.h59
-rw-r--r--subprojects/exess/src/ubyte.c45
-rw-r--r--subprojects/exess/src/uint.c45
-rw-r--r--subprojects/exess/src/ulong.c94
-rw-r--r--subprojects/exess/src/ushort.c45
-rw-r--r--subprojects/exess/src/variant.c431
-rw-r--r--subprojects/exess/src/warnings.h46
-rw-r--r--subprojects/exess/src/write_utils.c50
-rw-r--r--subprojects/exess/src/write_utils.h85
-rw-r--r--subprojects/exess/src/year.c98
-rw-r--r--subprojects/exess/test/.clang-tidy11
-rw-r--r--subprojects/exess/test/float_test_data.h130
-rw-r--r--subprojects/exess/test/int_test_data.h42
-rw-r--r--subprojects/exess/test/meson.build76
-rw-r--r--subprojects/exess/test/num_test_utils.h85
-rw-r--r--subprojects/exess/test/test_base64.c196
-rw-r--r--subprojects/exess/test/test_bigint.c841
-rw-r--r--subprojects/exess/test/test_boolean.c114
-rw-r--r--subprojects/exess/test/test_byte.c98
-rw-r--r--subprojects/exess/test/test_canonical.c412
-rw-r--r--subprojects/exess/test/test_coerce.c519
-rw-r--r--subprojects/exess/test/test_datatype.c81
-rw-r--r--subprojects/exess/test/test_date.c257
-rw-r--r--subprojects/exess/test/test_datetime.c459
-rw-r--r--subprojects/exess/test/test_decimal.c260
-rw-r--r--subprojects/exess/test/test_double.c246
-rw-r--r--subprojects/exess/test/test_duration.c310
-rw-r--r--subprojects/exess/test/test_float.c240
-rw-r--r--subprojects/exess/test/test_hex.c180
-rw-r--r--subprojects/exess/test/test_int.c129
-rw-r--r--subprojects/exess/test/test_int_math.c97
-rw-r--r--subprojects/exess/test/test_long.c144
-rw-r--r--subprojects/exess/test/test_short.c100
-rw-r--r--subprojects/exess/test/test_strerror.c35
-rw-r--r--subprojects/exess/test/test_time.c222
-rw-r--r--subprojects/exess/test/test_timezone.c178
-rw-r--r--subprojects/exess/test/test_ubyte.c100
-rw-r--r--subprojects/exess/test/test_uint.c128
-rw-r--r--subprojects/exess/test/test_ulong.c126
-rw-r--r--subprojects/exess/test/test_ushort.c100
-rw-r--r--subprojects/exess/test/test_variant.c301
-rw-r--r--subprojects/exess/test/time_test_utils.h52
119 files changed, 17576 insertions, 0 deletions
diff --git a/subprojects/exess/.clang-format b/subprojects/exess/.clang-format
new file mode 100644
index 00000000..466ffccf
--- /dev/null
+++ b/subprojects/exess/.clang-format
@@ -0,0 +1,30 @@
+---
+AlignConsecutiveAssignments: true
+AlignConsecutiveDeclarations: true
+AlignEscapedNewlinesLeft: true
+BasedOnStyle: Mozilla
+BraceWrapping:
+ AfterNamespace: false
+ AfterClass: true
+ AfterEnum: false
+ AfterExternBlock: false
+ AfterFunction: true
+ AfterStruct: false
+ SplitEmptyFunction: false
+ SplitEmptyRecord: false
+BinPackArguments: false
+BinPackParameters: false
+BreakBeforeBraces: Custom
+Cpp11BracedListStyle: true
+FixNamespaceComments: true
+IndentCaseLabels: false
+IndentPPDirectives: AfterHash
+KeepEmptyLinesAtTheStartOfBlocks: false
+SpacesInContainerLiterals: false
+StatementMacros:
+ - EXESS_API
+ - EXESS_CONST_FUNC
+ - EXESS_PURE_FUNC
+ - _Generic
+ - _Pragma
+...
diff --git a/subprojects/exess/.clang-tidy b/subprojects/exess/.clang-tidy
new file mode 100644
index 00000000..d3e82197
--- /dev/null
+++ b/subprojects/exess/.clang-tidy
@@ -0,0 +1,7 @@
+Checks: >
+ *,
+ -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
+ -llvmlibc-*,
+WarningsAsErrors: '*'
+HeaderFilterRegex: '.*'
+FormatStyle: file
diff --git a/subprojects/exess/.clant.json b/subprojects/exess/.clant.json
new file mode 100644
index 00000000..76cae6d3
--- /dev/null
+++ b/subprojects/exess/.clant.json
@@ -0,0 +1,4 @@
+{
+ "version": "1.0.0",
+ "mapping_files": [".includes.imp"]
+}
diff --git a/subprojects/exess/.gitignore b/subprojects/exess/.gitignore
new file mode 100644
index 00000000..84c048a7
--- /dev/null
+++ b/subprojects/exess/.gitignore
@@ -0,0 +1 @@
+/build/
diff --git a/subprojects/exess/.gitlab-ci.yml b/subprojects/exess/.gitlab-ci.yml
new file mode 100644
index 00000000..52426131
--- /dev/null
+++ b/subprojects/exess/.gitlab-ci.yml
@@ -0,0 +1,168 @@
+stages:
+ - build
+ - deploy
+
+.build_template: &build_definition
+ stage: build
+
+
+arm32_dbg:
+ <<: *build_definition
+ image: lv2plugin/debian-arm32
+ script:
+ - meson . build --cross-file=/usr/share/meson/cross/arm-linux-gnueabihf.ini -Dbuildtype=debug -Dstrict=true -Dwerror=true
+ - ninja -C build test
+
+arm32_rel:
+ <<: *build_definition
+ image: lv2plugin/debian-arm32
+ script:
+ - meson . build --cross-file=/usr/share/meson/cross/arm-linux-gnueabihf.ini -Dbuildtype=release -Dstrict=true -Dwerror=true
+ - ninja -C build test
+
+
+arm64_dbg:
+ <<: *build_definition
+ image: lv2plugin/debian-arm64
+ script:
+ - meson . build --cross-file=/usr/share/meson/cross/aarch64-linux-gnu.ini -Dbuildtype=debug -Dstrict=true -Dwerror=true
+ - ninja -C build test
+
+arm64_rel:
+ <<: *build_definition
+ image: lv2plugin/debian-arm64
+ script:
+ - meson . build --cross-file=/usr/share/meson/cross/aarch64-linux-gnu.ini -Dbuildtype=release -Dstrict=true -Dwerror=true
+ - ninja -C build test
+
+
+x32_dbg:
+ <<: *build_definition
+ image: lv2plugin/debian-x32
+ script:
+ - meson . build --cross-file=/usr/share/meson/cross/i686-linux-gnu.ini -Dbuildtype=debug -Dstrict=true -Dwerror=true
+ - ninja -C build test
+
+x32_rel:
+ <<: *build_definition
+ image: lv2plugin/debian-x32
+ script:
+ - meson . build --cross-file=/usr/share/meson/cross/i686-linux-gnu.ini -Dbuildtype=release -Dstrict=true -Dwerror=true
+ - ninja -C build test
+
+
+x64_dbg:
+ <<: *build_definition
+ image: lv2plugin/debian-x64
+ script:
+ - meson . build -Dbuildtype=debug -Dstrict=true -Dwerror=true -Db_coverage=true
+ - ninja -C build test
+ - ninja -C build coverage-html
+ artifacts:
+ paths:
+ - build/meson-logs/coveragereport
+
+x64_rel:
+ <<: *build_definition
+ image: lv2plugin/debian-x64
+ script:
+ - meson . build -Dbuildtype=release -Dstrict=true -Dwerror=true
+ - ninja -C build test
+
+
+x64_static:
+ <<: *build_definition
+ image: lv2plugin/debian-x64
+ script:
+ - meson . build -Ddefault_library=static -Dstrict=true -Dwerror=true
+ - ninja -C build test
+
+
+x64_sanitize:
+ <<: *build_definition
+ image: lv2plugin/debian-x64-clang
+ script:
+ - meson . build -Db_lundef=false -Dbuildtype=plain -Dstrict=true -Dwerror=true
+ - ninja -C build test
+ variables:
+ CC: "clang"
+ CFLAGS: "-fno-sanitize-recover=all -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero -fsanitize=implicit-conversion -fsanitize=local-bounds -fsanitize=nullability"
+ LDFLAGS: "-fno-sanitize-recover=all -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero -fsanitize=implicit-conversion -fsanitize=local-bounds -fsanitize=nullability"
+
+
+mingw32_dbg:
+ <<: *build_definition
+ image: lv2plugin/debian-mingw32
+ script:
+ - meson . build --cross-file=/usr/share/meson/cross/i686-w64-mingw32.ini -Dbuildtype=debug -Dstrict=true -Dwerror=true
+ - ninja -C build test
+
+mingw32_rel:
+ <<: *build_definition
+ image: lv2plugin/debian-mingw32
+ script:
+ - meson . build --cross-file=/usr/share/meson/cross/i686-w64-mingw32.ini -Dbuildtype=release -Dstrict=true -Dwerror=true
+ - ninja -C build test
+
+
+mingw64_dbg:
+ <<: *build_definition
+ image: lv2plugin/debian-mingw64
+ script:
+ - meson . build --cross-file=/usr/share/meson/cross/x86_64-w64-mingw32.ini -Dbuildtype=debug -Dstrict=true -Dwerror=true
+ - ninja -C build test
+
+mingw64_rel:
+ <<: *build_definition
+ image: lv2plugin/debian-mingw64
+ script:
+ - meson . build --cross-file=/usr/share/meson/cross/x86_64-w64-mingw32.ini -Dbuildtype=release -Dstrict=true -Dwerror=true
+ - ninja -C build test
+
+
+mac_dbg:
+ <<: *build_definition
+ tags: [macos]
+ script:
+ - meson . build -Dbuildtype=debug -Dstrict=true -Dwerror=true
+ - ninja -C build test
+
+mac_rel:
+ <<: *build_definition
+ tags: [macos]
+ script:
+ - meson . build -Dbuildtype=release -Dstrict=true -Dwerror=true
+ - ninja -C build test
+
+
+win_dbg:
+ <<: *build_definition
+ tags: [windows,meson]
+ script:
+ - meson . build -Dbuildtype=debug -Dstrict=true -Dwerror=true
+ - ninja -C build test
+
+win_rel:
+ <<: *build_definition
+ tags: [windows,meson]
+ script:
+ - meson . build -Dbuildtype=release -Dstrict=true -Dwerror=true
+ - ninja -C build test
+
+
+pages:
+ stage: deploy
+ script:
+ - mkdir -p .public/doc
+ - mkdir -p .public/c
+ - mv build/meson-logs/coveragereport/ .public/coverage
+ - mv build/doc/c/singlehtml .public/c/singlehtml
+ - mv build/doc/c/html .public/c/html
+ - mv .public public
+ dependencies:
+ - x64_dbg
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/subprojects/exess/.includes.imp b/subprojects/exess/.includes.imp
new file mode 100644
index 00000000..bf54f24d
--- /dev/null
+++ b/subprojects/exess/.includes.imp
@@ -0,0 +1,10 @@
+[
+ { "symbol": [ "int16_t", "private", "<stdint.h>", "public" ] },
+ { "symbol": [ "int32_t", "private", "<stdint.h>", "public" ] },
+ { "symbol": [ "int64_t", "private", "<stdint.h>", "public" ] },
+ { "symbol": [ "int8_t", "private", "<stdint.h>", "public" ] },
+ { "symbol": [ "uint16_t", "private", "<stdint.h>", "public" ] },
+ { "symbol": [ "uint32_t", "private", "<stdint.h>", "public" ] },
+ { "symbol": [ "uint64_t", "private", "<stdint.h>", "public" ] },
+ { "symbol": [ "uint8_t", "private", "<stdint.h>", "public" ] }
+]
diff --git a/subprojects/exess/NEWS b/subprojects/exess/NEWS
new file mode 100644
index 00000000..26c425ff
--- /dev/null
+++ b/subprojects/exess/NEWS
@@ -0,0 +1,5 @@
+exess (0.0.1) unstable;
+
+ * Initial release
+
+ -- David Robillard <d@drobilla.net> Thu, 14 Jan 2021 09:14:34 +0000
diff --git a/subprojects/exess/README.md b/subprojects/exess/README.md
new file mode 100644
index 00000000..c655ace7
--- /dev/null
+++ b/subprojects/exess/README.md
@@ -0,0 +1,83 @@
+Exess
+=====
+
+Exess is a simple library for reading and writing [XSD][] datatypes.
+
+Exess is useful for applications that need to read/write common datatypes
+from/to strings, in a standard and locale-independent format. It supports
+reading any valid syntax, and writing in canonical form. The implementation is
+not complete, but includes support for all of the common datatypes that are
+generally useful (the XML-specific and partial Gregorian calendar datatypes are
+omitted).
+
+Conversion to a string and back is lossless for all supported values. For
+example, writing a `float` number to a string then reading it back will yield
+the exact same `float` as the original value.
+
+The API consists mainly of simple read and write functions for each datatype.
+A variant type is also included which allows generic code to work with values
+of any type. For flexibility, allocation is handled by the caller, making it
+possible to work on the stack, or read and write values to fields in some
+structure. Syntax errors are reported with a descriptive error code and
+character offset, allowing friendly error messages to be produced.
+
+Supported Datatypes
+-------------------
+
+Exess supports reading and writing:
+
+ * `boolean`, like "false", "true", "0", or "1".
+
+ * `decimal`, like "1.234" (stored as `double`).
+
+ * `float` and `double`, like "4.2E1" or "4.2e1".
+
+ * The unbounded integer types `integer`, `nonPositiveInteger`,
+ `negativeInteger`, `nonNegativeInteger`, and `nonPositiveInteger` (stored
+ as `int64_t` or `uint64_t`).
+
+ * The fixed size integer types `long`, `int`, `short`, `byte`,
+ `unsignedLong`, `unsignedInt`, `unsignedShort`, and `unsignedByte`.
+
+ * `duration`, like "P1Y6M".
+
+ * `time`, like "12:30:00.00".
+
+ * `date`, like "2001-12-31".
+
+ * `hex`, like "EC5355".
+
+ * `base64`,like "Zm9vYmFy".
+
+Dependencies
+------------
+
+None, except the C standard library.
+
+Building
+--------
+
+A [Meson][] build definition is included which can be used to do a proper
+system installation with a `pkg-config` file, generate IDE projects, run the
+tests, and so on. For example, the library and tests can be built and run like
+so:
+
+ meson setup build
+ cd build
+ ninja test
+
+Documentation
+-------------
+
+ * [API reference (single page)](https://drobilla.gitlab.io/exess/c/singlehtml)
+ * [API reference (paginated)](https://drobilla.gitlab.io/exess/c/html)
+
+See the [Meson documentation][] for more details on using Meson.
+
+ -- David Robillard <d@drobilla.net>
+
+[XSD]: https://www.w3.org/TR/xmlschema-2/
+
+[Meson]: https://mesonbuild.com/
+
+[Meson documentation]: https://mesonbuild.com/Quick-guide.html
diff --git a/subprojects/exess/bindings/cpp/include/exess/exess.hpp b/subprojects/exess/bindings/cpp/include/exess/exess.hpp
new file mode 100644
index 00000000..7aa6fc5b
--- /dev/null
+++ b/subprojects/exess/bindings/cpp/include/exess/exess.hpp
@@ -0,0 +1,707 @@
+/*
+ Copyright 2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_EXESS_HPP
+#define EXESS_EXESS_HPP
+
+#include "exess/exess.h"
+
+#include <cstdint>
+#include <exception>
+#include <iostream>
+#include <string>
+#include <type_traits>
+#include <typeinfo>
+#include <vector>
+
+namespace exess {
+
+/**
+ @defgroup exesspp Exess C++ API
+ This is the C++ wrapper for the exess API.
+ @{
+*/
+
+constexpr const char* EXESS_NONNULL const xsd_uri =
+ "http://www.w3.org/2001/XMLSchema#";
+
+// FIXME: Reorganize
+using Datatype = ExessDatatype;
+using Result = ExessResult;
+
+using Duration = ExessDuration;
+using DateTime = ExessDateTime;
+using Time = ExessTime;
+using Date = ExessDate;
+
+/**
+ @defgroup exesspp_status Status
+ @copydoc exess_status
+ @{
+*/
+
+/// @copydoc ExessStatus
+enum class Status {
+ success, ///< @copydoc EXESS_SUCCESS
+ expected_end, ///< @copydoc EXESS_EXPECTED_END
+ expected_boolean, ///< @copydoc EXESS_EXPECTED_BOOLEAN
+ expected_integer, ///< @copydoc EXESS_EXPECTED_INTEGER
+ expected_duration, ///< @copydoc EXESS_EXPECTED_DURATION
+ expected_sign, ///< @copydoc EXESS_EXPECTED_SIGN
+ expected_digit, ///< @copydoc EXESS_EXPECTED_DIGIT
+ expected_colon, ///< @copydoc EXESS_EXPECTED_COLON
+ expected_dash, ///< @copydoc EXESS_EXPECTED_DASH
+ expected_time_sep, ///< @copydoc EXESS_EXPECTED_TIME_SEP
+ expected_time_tag, ///< @copydoc EXESS_EXPECTED_TIME_TAG
+ expected_date_tag, ///< @copydoc EXESS_EXPECTED_DATE_TAG
+ expected_hex, ///< @copydoc EXESS_EXPECTED_HEX
+ expected_base64, ///< @copydoc EXESS_EXPECTED_BASE64
+ bad_order, ///< @copydoc EXESS_BAD_ORDER
+ bad_value, ///< @copydoc EXESS_BAD_VALUE
+ out_of_range, ///< @copydoc EXESS_OUT_OF_RANGE
+ no_space, ///< @copydoc EXESS_NO_SPACE
+ would_reduce_precision, ///< @copydoc EXESS_WOULD_REDUCE_PRECISION
+ would_round, ///< @copydoc EXESS_WOULD_ROUND
+ would_truncate, ///< @copydoc EXESS_WOULD_TRUNCATE
+ unsupported, ///< @copydoc EXESS_UNSUPPORTED
+};
+
+/// @copydoc exess_strerror
+inline const char* EXESS_NONNULL
+strerror(const Status status)
+{
+ return exess_strerror(static_cast<ExessStatus>(status));
+}
+
+inline std::ostream&
+operator<<(std::ostream& stream, const Status status)
+{
+ return stream << strerror(status);
+}
+
+/**
+ @}
+
+ Datatype Traits
+*/
+
+namespace detail {
+
+template<class T>
+struct DatatypeTraits {};
+
+template<>
+struct DatatypeTraits<bool> {
+ static constexpr auto datatype = EXESS_BOOLEAN;
+ static constexpr auto read_function = exess_read_boolean;
+ static constexpr auto write_function = exess_write_boolean;
+};
+
+template<>
+struct DatatypeTraits<double> {
+ static constexpr auto datatype = EXESS_DOUBLE;
+ static constexpr auto read_function = exess_read_double;
+ static constexpr auto write_function = exess_write_double;
+};
+
+template<>
+struct DatatypeTraits<float> {
+ static constexpr auto datatype = EXESS_FLOAT;
+ static constexpr auto read_function = exess_read_float;
+ static constexpr auto write_function = exess_write_float;
+};
+
+template<>
+struct DatatypeTraits<int64_t> {
+ static constexpr auto datatype = EXESS_LONG;
+ static constexpr auto read_function = exess_read_long;
+ static constexpr auto write_function = exess_write_long;
+};
+
+template<>
+struct DatatypeTraits<int32_t> {
+ static constexpr auto datatype = EXESS_INT;
+ static constexpr auto read_function = exess_read_int;
+ static constexpr auto write_function = exess_write_int;
+};
+
+template<>
+struct DatatypeTraits<int16_t> {
+ static constexpr auto datatype = EXESS_SHORT;
+ static constexpr auto read_function = exess_read_short;
+ static constexpr auto write_function = exess_write_short;
+};
+
+template<>
+struct DatatypeTraits<int8_t> {
+ static constexpr auto datatype = EXESS_BYTE;
+ static constexpr auto read_function = exess_read_byte;
+ static constexpr auto write_function = exess_write_byte;
+};
+
+template<>
+struct DatatypeTraits<uint64_t> {
+ static constexpr auto datatype = EXESS_ULONG;
+ static constexpr auto read_function = exess_read_ulong;
+ static constexpr auto write_function = exess_write_ulong;
+};
+
+template<>
+struct DatatypeTraits<uint32_t> {
+ static constexpr auto datatype = EXESS_UINT;
+ static constexpr auto read_function = exess_read_uint;
+ static constexpr auto write_function = exess_write_uint;
+};
+
+template<>
+struct DatatypeTraits<uint16_t> {
+ static constexpr auto datatype = EXESS_USHORT;
+ static constexpr auto read_function = exess_read_ushort;
+ static constexpr auto write_function = exess_write_ushort;
+};
+
+template<>
+struct DatatypeTraits<uint8_t> {
+ static constexpr auto datatype = EXESS_UBYTE;
+ static constexpr auto read_function = exess_read_ubyte;
+ static constexpr auto write_function = exess_write_ubyte;
+};
+
+template<>
+struct DatatypeTraits<Duration> {
+ static constexpr auto datatype = EXESS_DURATION;
+ static constexpr auto read_function = exess_read_duration;
+ static constexpr auto write_function = exess_write_duration;
+};
+
+template<>
+struct DatatypeTraits<DateTime> {
+ static constexpr auto datatype = EXESS_DATETIME;
+ static constexpr auto read_function = exess_read_datetime;
+ static constexpr auto write_function = exess_write_datetime;
+};
+
+template<>
+struct DatatypeTraits<Time> {
+ static constexpr auto datatype = EXESS_TIME;
+ static constexpr auto read_function = exess_read_time;
+ static constexpr auto write_function = exess_write_time;
+};
+
+template<>
+struct DatatypeTraits<Date> {
+ static constexpr auto datatype = EXESS_DATE;
+ static constexpr auto read_function = exess_read_date;
+ static constexpr auto write_function = exess_write_date;
+};
+
+} // namespace detail
+
+/**
+ @defgroup exesspp_read_write Reading and Writing
+ @{
+*/
+
+/**
+ Read a value from a string.
+
+ @param out Set to the parsed value.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+template<class T>
+inline Result
+read(T* EXESS_NONNULL out, const char* EXESS_NONNULL str)
+{
+ return detail::DatatypeTraits<T>::read_function(out, str);
+}
+
+/**
+ Write a value to a canonical string.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return The `count` of characters in the output, and `status`
+ #Status::success, or #Status::no_space if the buffer is too small.
+*/
+template<class T>
+inline Result
+write(const T& value, const size_t buf_size, char* EXESS_NONNULL buf)
+{
+ return detail::DatatypeTraits<T>::write_function(value, buf_size, buf);
+}
+
+/**
+ Return a value as a string.
+
+ This is a wrapper for write() that allocates a new string of the appropriate
+ length, writes the value to it, and returns it.
+
+ @param value The value to convert to a string.
+ @return The value as a string, or the empty string on error.
+*/
+template<class T>
+inline std::string
+to_string(const T& value)
+{
+ auto r = detail::DatatypeTraits<T>::write_function(value, 0, nullptr);
+ if (r.status) {
+ return {};
+ }
+
+ // In C++17, std::string::data() allows mutable access
+#if __cplusplus >= 201703L
+ std::string string(r.count, ' ');
+ r = detail::DatatypeTraits<T>::write_function(
+ value, r.count + 1, string.data());
+
+ // Before, we had to allocate somewhere else
+#else
+ std::vector<char> buf(r.count + 1, '\0');
+ r = detail::DatatypeTraits<T>::write_function(value, r.count + 1, buf.data());
+
+ std::string string(buf.data());
+#endif
+
+ return r.status ? "" : string;
+}
+
+/**
+ @}
+*/
+
+// TODO: hex, base64
+
+struct Variant : ExessVariant {
+ explicit Variant(const Status status) noexcept
+ : ExessVariant{}
+ {
+ datatype = EXESS_NOTHING;
+ value.as_status = static_cast<ExessStatus>(status);
+ }
+
+ explicit Variant(const bool v) noexcept
+ : ExessVariant{}
+ {
+ datatype = EXESS_BOOLEAN;
+ value.as_bool = v;
+ }
+
+ explicit Variant(const double v) noexcept
+ : ExessVariant{}
+ {
+ datatype = EXESS_DOUBLE;
+ value.as_double = v;
+ }
+
+ explicit Variant(const float v) noexcept
+ : ExessVariant{}
+ {
+ datatype = EXESS_FLOAT;
+ value.as_float = v;
+ }
+
+ explicit Variant(const int64_t v) noexcept
+ : ExessVariant{}
+ {
+ datatype = EXESS_LONG;
+ value.as_long = v;
+ }
+
+ explicit Variant(const int32_t v) noexcept
+ : ExessVariant{}
+ {
+ datatype = EXESS_INT;
+ value.as_int = v;
+ }
+
+ explicit Variant(const int16_t v) noexcept
+ : ExessVariant{}
+ {
+ datatype = EXESS_SHORT;
+ value.as_short = v;
+ }
+
+ explicit Variant(const int8_t v) noexcept
+ : ExessVariant{}
+ {
+ datatype = EXESS_BYTE;
+ value.as_byte = v;
+ }
+
+ explicit Variant(const uint64_t v) noexcept
+ : ExessVariant{}
+ {
+ datatype = EXESS_ULONG;
+ value.as_ulong = v;
+ }
+
+ explicit Variant(const uint32_t v) noexcept
+ : ExessVariant{}
+ {
+ datatype = EXESS_UINT;
+ value.as_uint = v;
+ }
+
+ explicit Variant(const uint16_t v) noexcept
+ : ExessVariant{}
+ {
+ datatype = EXESS_USHORT;
+ value.as_ushort = v;
+ }
+
+ explicit Variant(const uint8_t v) noexcept
+ : ExessVariant{}
+ {
+ datatype = EXESS_UBYTE;
+ value.as_ubyte = v;
+ }
+
+ explicit Variant(const Duration v) noexcept
+ : ExessVariant{}
+ {
+ datatype = EXESS_DURATION;
+ value.as_duration = v;
+ }
+
+ explicit Variant(const DateTime v) noexcept
+ : ExessVariant{}
+ {
+ datatype = EXESS_DATETIME;
+ value.as_datetime = v;
+ }
+
+ explicit Variant(const Time v) noexcept
+ : ExessVariant{}
+ {
+ datatype = EXESS_TIME;
+ value.as_time = v;
+ }
+
+ explicit Variant(const Date v) noexcept
+ : ExessVariant{}
+ {
+ datatype = EXESS_DATE;
+ value.as_date = v;
+ }
+};
+
+inline Variant
+make_decimal(const double value) noexcept
+{
+ Variant result{value};
+ result.datatype = EXESS_DECIMAL;
+ return result;
+}
+
+inline Variant
+make_integer(const int64_t value) noexcept
+{
+ Variant result{value};
+ result.datatype = EXESS_INTEGER;
+ return result;
+}
+
+inline Variant
+make_non_positive_integer(const int64_t value) noexcept
+{
+ if (value > 0) {
+ return Variant{Status::out_of_range};
+ }
+
+ Variant result{value};
+ result.datatype = EXESS_NON_POSITIVE_INTEGER;
+ return result;
+}
+
+inline Variant
+make_negative_integer(const int64_t value) noexcept
+{
+ if (value >= 0) {
+ return Variant{Status::out_of_range};
+ }
+
+ Variant result{value};
+ result.datatype = EXESS_NEGATIVE_INTEGER;
+ return result;
+}
+
+inline Variant
+make_non_negative_integer(const uint64_t value) noexcept
+{
+ Variant result{value};
+ result.datatype = EXESS_NON_NEGATIVE_INTEGER;
+ return result;
+}
+
+inline Variant
+make_positive_integer(const uint64_t value) noexcept
+{
+ if (value == 0) {
+ return Variant{Status::out_of_range};
+ }
+
+ Variant result{value};
+ result.datatype = EXESS_POSITIVE_INTEGER;
+ return result;
+}
+
+// enum class Datatype {
+// NOTHING, ///< Sentinel for unknown datatypes or errors
+// BOOLEAN, ///< xsd:boolean (see @ref exess_boolean)
+// DECIMAL, ///< xsd:decimal (see @ref exess_decimal)
+// DOUBLE, ///< xsd:double (see @ref exess_double)
+// FLOAT, ///< xsd:float (see @ref exess_float)
+// INTEGER, ///< xsd:integer (see @ref exess_long)
+// NON_POSITIVE_INTEGER, ///< xsd:nonPositiveInteger (see @ref exess_long)
+// NEGATIVE_INTEGER, ///< xsd:negativeInteger (see @ref exess_long)
+// LONG, ///< xsd:long (see @ref exess_long)
+// INT, ///< xsd:integer (see @ref exess_int)
+// SHORT, ///< xsd:short (see @ref exess_short)
+// BYTE, ///< xsd:byte (see @ref exess_byte)
+// NON_NEGATIVE_INTEGER, ///< xsd:nonNegativeInteger (see @ref exess_ulong)
+// ULONG, ///< xsd:unsignedLong (see @ref exess_ulong)
+// UINT, ///< xsd:unsignedInt (see @ref exess_uint)
+// USHORT, ///< xsd:unsignedShort (see @ref exess_ushort)
+// UBYTE, ///< xsd:unsignedByte (see @ref exess_ubyte)
+// POSITIVE_INTEGER, ///< xsd:positiveInteger (see @ref exess_ulong)
+// DURATION, ///< xsd:duration (see @ref exess_duration)
+// DATETIME, ///< xsd:dateTime (see @ref exess_datetime)
+// TIME, ///< xsd:time (see @ref exess_time)
+// DATE, ///< xsd:date (see @ref exess_date)
+// HEX, ///< xsd:hexBinary (see @ref exess_hex)
+// BASE64, ///< xsd:base64Binary (see @ref exess_base64)
+// }
+
+//
+
+/**
+ Return a pointer to the value of `variant` if it has type `T`.
+
+ This is safe to call on any variant, and will only return a pointer to the
+ value if the variant has the matching type and the value is valid to read.
+
+ @return A pointer to the value in `variant`, or null.
+*/
+template<class T>
+constexpr const T* EXESS_NULLABLE
+get_if(const Variant& variant) noexcept
+{
+ return (variant.datatype == detail::DatatypeTraits<T>::datatype)
+ ? reinterpret_cast<const T*>(&variant.value)
+ : nullptr;
+}
+
+/**
+ Return a pointer to the Status value in `variant` if it exists.
+*/
+template<>
+constexpr const Status* EXESS_NULLABLE
+get_if<Status>(const Variant& variant) noexcept
+{
+ return variant.datatype == EXESS_NOTHING
+ ? reinterpret_cast<const Status*>(&variant.value.as_status)
+ : nullptr;
+}
+
+/**
+ Return a pointer to the value of `variant` if it is a `double`.
+
+ This specialization works for both #EXESS_DECIMAL and #EXESS_DOUBLE values.
+*/
+template<>
+constexpr const double* EXESS_NULLABLE
+get_if<double>(const Variant& variant) noexcept
+{
+ return (variant.datatype == EXESS_DECIMAL || variant.datatype == EXESS_DOUBLE)
+ ? &variant.value.as_double
+ : nullptr;
+}
+
+/**
+ Return a pointer to the value of `variant` if it is an `int64_t` (long).
+
+ This specialization works for #EXESS_INTEGER, #EXESS_NON_POSITIVE_INTEGER,
+ #EXESS_NEGATIVE_INTEGER, and #EXESS_LONG values.
+*/
+template<>
+constexpr const int64_t* EXESS_NULLABLE
+get_if<int64_t>(const Variant& variant) noexcept
+{
+ return (variant.datatype >= EXESS_INTEGER && variant.datatype <= EXESS_LONG)
+ ? &variant.value.as_long
+ : nullptr;
+}
+
+/**
+ Return a pointer to the value of `variant` if it is a `uint64_t` (ulong).
+
+ This specialization works for #EXESS_NON_NEGATIVE_INTEGER, #EXESS_ULONG, and
+ #EXESS_POSITIVE_INTEGER values.
+*/
+template<>
+constexpr const uint64_t* EXESS_NULLABLE
+get_if<uint64_t>(const Variant& variant) noexcept
+{
+ return (variant.datatype == EXESS_NON_NEGATIVE_INTEGER ||
+ variant.datatype == EXESS_ULONG ||
+ variant.datatype == EXESS_POSITIVE_INTEGER)
+ ? &variant.value.as_ulong
+ : nullptr;
+}
+
+/**
+ Return the value of a variant with the given type.
+*/
+template<class T>
+const T&
+get(const Variant& variant)
+{
+ if (const T* const pointer = get_if<T>(variant)) {
+ return *pointer;
+ }
+
+ if (variant.datatype == EXESS_NOTHING) {
+ throw std::runtime_error{"Empty exess::Variant access"};
+ }
+
+ throw std::runtime_error{
+ std::string("Bad exess::Variant access: ") +
+ std::string(exess_datatype_uri(variant.datatype) + strlen(xsd_uri)) +
+ " from " + typeid(T).name()};
+}
+
+template<class T>
+constexpr size_t
+max_length()
+{
+ return 0u;
+}
+
+template<>
+constexpr size_t
+max_length<bool>()
+{
+ return EXESS_MAX_BOOLEAN_LENGTH;
+}
+
+template<>
+constexpr size_t
+max_length<double>()
+{
+ return EXESS_MAX_DOUBLE_LENGTH;
+}
+
+template<>
+constexpr size_t
+max_length<float>()
+{
+ return EXESS_MAX_FLOAT_LENGTH;
+}
+
+template<>
+constexpr size_t
+max_length<int64_t>()
+{
+ return EXESS_MAX_LONG_LENGTH;
+}
+
+template<>
+constexpr size_t
+max_length<int32_t>()
+{
+ return EXESS_MAX_INT_LENGTH;
+}
+
+template<>
+constexpr size_t
+max_length<int16_t>()
+{
+ return EXESS_MAX_SHORT_LENGTH;
+}
+
+template<>
+constexpr size_t
+max_length<int8_t>()
+{
+ return EXESS_MAX_BYTE_LENGTH;
+}
+
+template<>
+constexpr size_t
+max_length<uint64_t>()
+{
+ return EXESS_MAX_ULONG_LENGTH;
+}
+
+template<>
+constexpr size_t
+max_length<uint32_t>()
+{
+ return EXESS_MAX_UINT_LENGTH;
+}
+
+template<>
+constexpr size_t
+max_length<uint16_t>()
+{
+ return EXESS_MAX_USHORT_LENGTH;
+}
+
+template<>
+constexpr size_t
+max_length<uint8_t>()
+{
+ return EXESS_MAX_UBYTE_LENGTH;
+}
+
+template<>
+constexpr size_t
+max_length<Duration>()
+{
+ return EXESS_MAX_DURATION_LENGTH;
+}
+
+template<>
+constexpr size_t
+max_length<DateTime>()
+{
+ return EXESS_MAX_DATETIME_LENGTH;
+}
+
+template<>
+constexpr size_t
+max_length<Time>()
+{
+ return EXESS_MAX_TIME_LENGTH;
+}
+
+template<>
+constexpr size_t
+max_length<Date>()
+{
+ return EXESS_MAX_DATE_LENGTH;
+}
+
+/**
+ @}
+*/
+
+} // namespace exess
+
+#endif // EXESS_EXESS_HPP
diff --git a/subprojects/exess/bindings/cpp/meson.build b/subprojects/exess/bindings/cpp/meson.build
new file mode 100644
index 00000000..61e3616e
--- /dev/null
+++ b/subprojects/exess/bindings/cpp/meson.build
@@ -0,0 +1,54 @@
+add_languages(['cpp'])
+cpp = meson.get_compiler('cpp')
+
+# Set ultra strict warnings for developers, if requested
+if get_option('strict')
+ if cpp.get_id() == 'clang'
+ cpp_suppressions = [
+ '-Wno-c++14-extensions',
+ '-Wno-c++98-compat',
+ '-Wno-c++98-compat-pedantic',
+ '-Wno-documentation-unknown-command',
+ '-Wno-nullability-extension',
+ '-Wno-padded',
+ ]
+
+ elif cpp.get_id() == 'gcc'
+ cpp_suppressions = [
+ '-Wno-inline',
+ '-Wno-padded',
+ '-Wno-switch-default',
+ '-Wno-unused-const-variable',
+ '-Wno-useless-cast',
+ ]
+
+ elif cpp.get_id() == 'msvc'
+ cpp_suppressions = [
+ '/wd4028', # formal parameter different from declaration
+ '/wd4204', # nonstandard extension: non-constant aggregate initializer
+ '/wd4706', # assignment within conditional expression
+ '/wd4710', # function not inlined
+ '/wd4711', # function selected for automatic inline expansion
+ '/wd4820', # padding added after data member
+ '/wd5045', # will insert Spectre mitigation
+ ]
+ endif
+
+else
+ if cpp.get_id() == 'clang'
+ cpp_suppressions = [
+ '-Wno-nullability-extension',
+ ]
+ endif
+endif
+
+exess_cpp_args = cc.get_supported_arguments(cpp_suppressions)
+
+cpp_headers = ['include/exess/exess.hpp']
+cpp_header_files = files(cpp_headers)
+
+exess_hpp_dep = declare_dependency(
+ dependencies: exess_dep,
+ include_directories: include_directories('include'))
+
+subdir('test')
diff --git a/subprojects/exess/bindings/cpp/test/.clang-tidy b/subprojects/exess/bindings/cpp/test/.clang-tidy
new file mode 100644
index 00000000..a0307839
--- /dev/null
+++ b/subprojects/exess/bindings/cpp/test/.clang-tidy
@@ -0,0 +1,17 @@
+Checks: >
+ *,
+ -*-magic-numbers,
+ -*-uppercase-literal-suffix,
+ -clang-analyzer-nullability.NullableDereferenced,
+ -cppcoreguidelines-pro-bounds-array-to-pointer-decay,
+ -cppcoreguidelines-pro-type-reinterpret-cast,
+ -fuchsia-default-arguments-calls,
+ -fuchsia-overloaded-operator,
+ -hicpp-no-array-decay,
+ -llvmlibc-*,
+ -misc-non-private-member-variables-in-classes,
+ -modernize-unary-static-assert,
+ -modernize-use-trailing-return-type,
+WarningsAsErrors: '*'
+HeaderFilterRegex: '.*'
+FormatStyle: file
diff --git a/subprojects/exess/bindings/cpp/test/meson.build b/subprojects/exess/bindings/cpp/test/meson.build
new file mode 100644
index 00000000..e04c21c9
--- /dev/null
+++ b/subprojects/exess/bindings/cpp/test/meson.build
@@ -0,0 +1,8 @@
+test('exess_hpp',
+ executable('test_exess_hpp',
+ 'test_exess_hpp.cpp',
+ cpp_args: exess_cpp_args + prog_args,
+ dependencies: [exess_hpp_dep],
+ include_directories: include_directories('../../../test',
+ '../../../src')),
+ suite: 'cpp')
diff --git a/subprojects/exess/bindings/cpp/test/test_exess_hpp.cpp b/subprojects/exess/bindings/cpp/test/test_exess_hpp.cpp
new file mode 100644
index 00000000..e940f4b1
--- /dev/null
+++ b/subprojects/exess/bindings/cpp/test/test_exess_hpp.cpp
@@ -0,0 +1,221 @@
+/*
+ Copyright 2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "float_test_data.h"
+
+#include "exess/exess.h"
+#include "exess/exess.hpp"
+
+#include <cassert>
+#include <cstdint>
+#include <cstdlib>
+#include <iostream>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+
+namespace exess {
+namespace {
+
+void
+test_status()
+{
+ std::ostringstream ss;
+ ss << Status::no_space;
+
+ std::cerr << ss.str();
+ assert(ss.str() == "Insufficient space");
+}
+
+void
+test_read()
+{
+ bool a_bool = false;
+ assert(!read(&a_bool, "true").status);
+ assert(a_bool == true);
+
+ double a_double = 0.0;
+ assert(!read(&a_double, "4.2E16").status);
+ assert(double_matches(a_double, 42000000000000000.0));
+
+ float a_float = 0.0f;
+ assert(!read(&a_float, "4.2E7").status);
+ assert(float_matches(a_float, 42000000.0f));
+
+ int64_t a_long = 0;
+ assert(!read(&a_long, "-4200000000").status);
+ assert(a_long == -4200000000);
+
+ int32_t a_int = 0;
+ assert(!read(&a_int, "-42000").status);
+ assert(a_int == -42000);
+
+ int16_t a_short = 0;
+ assert(!read(&a_short, "-420").status);
+ assert(a_short == -420);
+
+ int8_t a_byte = 0;
+ assert(!read(&a_byte, "-42").status);
+ assert(a_byte == -42);
+
+ uint64_t a_ulong = 0u;
+ assert(!read(&a_ulong, "4200000000").status);
+ assert(a_ulong == 4200000000);
+
+ uint32_t a_uint = 0u;
+ assert(!read(&a_uint, "42000").status);
+ assert(a_uint == 42000);
+
+ uint16_t a_ushort = 0u;
+ assert(!read(&a_ushort, "420").status);
+ assert(a_ushort == 420);
+
+ uint8_t a_ubyte = 0u;
+ assert(!read(&a_ubyte, "42").status);
+ assert(a_ubyte == 42);
+}
+
+void
+test_to_string()
+{
+ assert(to_string(true) == "true");
+ assert(to_string(42000000000000000.0) == "4.2E16");
+ assert(to_string(42000000.0f) == "4.2E7");
+ assert(to_string(int64_t(-4200000000)) == "-4200000000");
+ assert(to_string(int32_t(-42000)) == "-42000");
+ assert(to_string(int16_t(-420)) == "-420");
+ assert(to_string(int8_t(-42)) == "-42");
+ assert(to_string(uint64_t(4200000000u)) == "4200000000");
+ assert(to_string(uint32_t(42000u)) == "42000");
+ assert(to_string(uint16_t(420u)) == "420");
+ assert(to_string(uint8_t(42u)) == "42");
+}
+
+void
+test_max_length()
+{
+ static_assert(max_length<bool>() == EXESS_MAX_BOOLEAN_LENGTH, "");
+ static_assert(max_length<double>() == EXESS_MAX_DOUBLE_LENGTH, "");
+ static_assert(max_length<float>() == EXESS_MAX_FLOAT_LENGTH, "");
+ static_assert(max_length<int64_t>() == EXESS_MAX_LONG_LENGTH, "");
+ static_assert(max_length<int32_t>() == EXESS_MAX_INT_LENGTH, "");
+ static_assert(max_length<int16_t>() == EXESS_MAX_SHORT_LENGTH, "");
+ static_assert(max_length<int8_t>() == EXESS_MAX_BYTE_LENGTH, "");
+ static_assert(max_length<uint64_t>() == EXESS_MAX_ULONG_LENGTH, "");
+ static_assert(max_length<uint32_t>() == EXESS_MAX_UINT_LENGTH, "");
+ static_assert(max_length<uint16_t>() == EXESS_MAX_USHORT_LENGTH, "");
+ static_assert(max_length<uint8_t>() == EXESS_MAX_UBYTE_LENGTH, "");
+ static_assert(max_length<Duration>() == EXESS_MAX_DURATION_LENGTH, "");
+ static_assert(max_length<DateTime>() == EXESS_MAX_DATETIME_LENGTH, "");
+ static_assert(max_length<Date>() == EXESS_MAX_DATE_LENGTH, "");
+ static_assert(max_length<Time>() == EXESS_MAX_TIME_LENGTH, "");
+}
+
+template<class T>
+void
+check_get_throws(const Variant& variant)
+{
+ bool caught = false;
+
+ try {
+ get<T>(variant);
+ } catch (const std::runtime_error&) {
+ caught = true;
+ }
+
+ assert(caught);
+}
+
+void
+test_variant()
+{
+ const auto a_nothing = Variant{Status::success};
+ const auto a_bool = Variant{true};
+ const auto a_decimal = make_decimal(1.2);
+ const auto a_double = Variant{3.4};
+ const auto a_float = Variant{5.6f};
+ const auto a_integer = make_integer(7);
+ const auto a_non_positive_integer = make_non_positive_integer(-8);
+ const auto a_negative_integer = make_negative_integer(-9);
+ const auto a_long = Variant{int64_t(10)};
+ const auto a_int = Variant{int32_t(11)};
+ const auto a_short = Variant{int16_t(12)};
+ const auto a_byte = Variant{int8_t(13)};
+ const auto a_non_negative_integer = make_non_negative_integer(14u);
+ const auto a_ulong = Variant{uint64_t(15u)};
+ const auto a_uint = Variant{uint32_t(16u)};
+ const auto a_ushort = Variant{uint16_t(17u)};
+ const auto a_ubyte = Variant{uint8_t(18u)};
+ const auto a_positive_integer = make_positive_integer(19u);
+
+ try {
+ assert(get<Status>(a_nothing) == Status::success);
+ assert(get<bool>(a_bool) == true);
+ assert(double_matches(get<double>(a_decimal), 1.2));
+ assert(double_matches(get<double>(a_double), 3.4));
+ assert(float_matches(get<float>(a_float), 5.6f));
+ assert(get<int64_t>(a_integer) == 7);
+ assert(get<int64_t>(a_non_positive_integer) == -8);
+ assert(get<int64_t>(a_negative_integer) == -9);
+ assert(get<int64_t>(a_long) == 10);
+ assert(get<int32_t>(a_int) == 11);
+ assert(get<int16_t>(a_short) == 12);
+ assert(get<int8_t>(a_byte) == 13);
+ assert(get<uint64_t>(a_non_negative_integer) == 14u);
+ assert(get<uint64_t>(a_ulong) == 15u);
+ assert(get<uint32_t>(a_uint) == 16u);
+ assert(get<uint16_t>(a_ushort) == 17u);
+ assert(get<uint8_t>(a_ubyte) == 18u);
+ assert(get<uint64_t>(a_positive_integer) == 19u);
+ } catch (const std::runtime_error&) {
+ abort();
+ }
+
+ check_get_throws<int>(a_nothing);
+ check_get_throws<int>(a_bool);
+ check_get_throws<int>(a_double);
+ check_get_throws<int>(a_float);
+ check_get_throws<int>(a_integer);
+ check_get_throws<int>(a_non_positive_integer);
+ check_get_throws<int>(a_negative_integer);
+ check_get_throws<int>(a_long);
+ check_get_throws<bool>(a_int);
+ check_get_throws<int>(a_short);
+ check_get_throws<int>(a_byte);
+ check_get_throws<int>(a_non_negative_integer);
+ check_get_throws<int>(a_ulong);
+ check_get_throws<int>(a_uint);
+ check_get_throws<int>(a_ushort);
+ check_get_throws<int>(a_ubyte);
+ check_get_throws<int>(a_positive_integer);
+}
+
+} // namespace
+} // namespace exess
+
+int
+main()
+{
+ exess::test_status();
+ exess::test_read();
+ exess::test_to_string();
+ exess::test_max_length();
+ exess::test_variant();
+
+ return 0;
+}
diff --git a/subprojects/exess/doc/c/Doxyfile.in b/subprojects/exess/doc/c/Doxyfile.in
new file mode 100644
index 00000000..9a768f89
--- /dev/null
+++ b/subprojects/exess/doc/c/Doxyfile.in
@@ -0,0 +1,39 @@
+PROJECT_NAME = Exess
+PROJECT_BRIEF = "A library for reading and writing XSD datatypes"
+
+QUIET = YES
+WARN_AS_ERROR = YES
+WARN_IF_UNDOCUMENTED = NO
+WARN_NO_PARAMDOC = NO
+
+JAVADOC_AUTOBRIEF = YES
+
+FULL_PATH_NAMES = NO
+CASE_SENSE_NAMES = YES
+HIDE_IN_BODY_DOCS = YES
+REFERENCES_LINK_SOURCE = NO
+
+GENERATE_HTML = NO
+GENERATE_LATEX = NO
+GENERATE_XML = YES
+XML_PROGRAMLISTING = NO
+SHOW_FILES = NO
+
+ENABLE_PREPROCESSING = YES
+SKIP_FUNCTION_MACROS = NO
+
+MACRO_EXPANSION = YES
+EXPAND_ONLY_PREDEF = YES
+PREDEFINED = EXESS_API \
+ EXESS_CONST_API \
+ EXESS_CONST_FUNC= \
+ EXESS_NONNULL= \
+ EXESS_NULLABLE= \
+ EXESS_PURE_API \
+ EXESS_PURE_FUNC= \
+
+RECURSIVE = YES
+STRIP_FROM_PATH = @EXESS_SRCDIR@
+INPUT = @EXESS_SRCDIR@/include
+
+OUTPUT_DIRECTORY = @DOX_OUTPUT@
diff --git a/subprojects/exess/doc/c/api/meson.build b/subprojects/exess/doc/c/api/meson.build
new file mode 100644
index 00000000..1a3d6f94
--- /dev/null
+++ b/subprojects/exess/doc/c/api/meson.build
@@ -0,0 +1,5 @@
+c_exess_rst = custom_target(
+ 'Exess C API Sphinx Input',
+ command: [dox_to_sphinx, '-f', '@INPUT0@', meson.current_build_dir()],
+ input: [c_index_xml] + c_rst_files,
+ output: 'exess.rst')
diff --git a/subprojects/exess/doc/c/index.rst b/subprojects/exess/doc/c/index.rst
new file mode 100644
index 00000000..396a2333
--- /dev/null
+++ b/subprojects/exess/doc/c/index.rst
@@ -0,0 +1,10 @@
+#####
+Exess
+#####
+
+.. include:: summary.rst
+
+.. toctree::
+
+ overview
+ api/exess
diff --git a/subprojects/exess/doc/c/man/manlink.in b/subprojects/exess/doc/c/man/manlink.in
new file mode 100644
index 00000000..95accb8a
--- /dev/null
+++ b/subprojects/exess/doc/c/man/manlink.in
@@ -0,0 +1 @@
+.so @TARGET@
diff --git a/subprojects/exess/doc/c/man/meson.build b/subprojects/exess/doc/c/man/meson.build
new file mode 100644
index 00000000..d2a31557
--- /dev/null
+++ b/subprojects/exess/doc/c/man/meson.build
@@ -0,0 +1,122 @@
+# Run Sphinx to generate group man pages
+
+# Group man pages generated by Sphinx
+man_pages = [
+ 'exess_base64.3',
+ 'exess_binary.3',
+ 'exess_boolean.3',
+ 'exess_coercion.3',
+ 'exess_byte.3',
+ 'exess_datatypes.3',
+ 'exess_date.3',
+ 'exess_datetime.3',
+ 'exess_decimal.3',
+ 'exess_double.3',
+ 'exess_duration.3',
+ 'exess_float.3',
+ 'exess_hex.3',
+ 'exess_int.3',
+ 'exess_long.3',
+ # 'exess_numbers.3',
+ 'exess_short.3',
+ 'exess_status.3',
+ 'exess_time.3',
+ 'exess_timezone.3',
+ 'exess_ubyte.3',
+ 'exess_uint.3',
+ 'exess_ulong.3',
+ 'exess_ushort.3',
+ 'exess_variant.3',
+]
+
+# Run Sphinx to generate man pages
+man_docs = custom_target(
+ 'man pages for exess',
+ command: [sphinx_build, '-M', 'man',
+ meson.current_build_dir() / '..', meson.current_build_dir() / '..',
+ '-E', '-q', '-t', 'man'],
+ input: [c_rst_files, c_exess_rst, c_index_xml],
+ output: man_pages,
+ build_by_default: true,
+ install: true,
+ install_dir: get_option('mandir') / 'man3')
+
+
+
+# message(man_docs.extract_all_objects())
+
+# foreach man_page : man_pages
+# install_man(man_docs[0])#meson.current_build_dir() / man_page)
+# endforeach
+
+# "Links" from symbol names to corresponding page (one-line includes)
+man_links = [
+ ['ExessStatus.3', 'exess_status.3'],
+ ['ExessResult.3', 'exess_status.3'],
+ ['exess_strerror.3', 'exess_status.3'],
+ ['exess_read_decimal.3', 'exess_decimal.3'],
+ ['exess_write_decimal.3', 'exess_decimal.3'],
+ ['exess_read_double.3', 'exess_double.3'],
+ ['exess_write_double.3', 'exess_double.3'],
+ ['exess_read_float.3', 'exess_float.3'],
+ ['exess_write_float.3', 'exess_float.3'],
+ ['exess_read_boolean.3', 'exess_boolean.3'],
+ ['exess_write_boolean.3', 'exess_boolean.3'],
+ ['exess_read_long.3', 'exess_long.3'],
+ ['exess_write_long.3', 'exess_long.3'],
+ ['exess_read_int.3', 'exess_int.3'],
+ ['exess_write_int.3', 'exess_int.3'],
+ ['exess_read_short.3', 'exess_short.3'],
+ ['exess_write_short.3', 'exess_short.3'],
+ ['exess_read_byte.3', 'exess_byte.3'],
+ ['exess_write_byte.3', 'exess_byte.3'],
+ ['exess_read_ulong.3', 'exess_ulong.3'],
+ ['exess_write_ulong.3', 'exess_ulong.3'],
+ ['exess_read_uint.3', 'exess_uint.3'],
+ ['exess_write_uint.3', 'exess_uint.3'],
+ ['exess_read_ushort.3', 'exess_ushort.3'],
+ ['exess_write_ushort.3', 'exess_ushort.3'],
+ ['exess_read_ubyte.3', 'exess_ubyte.3'],
+ ['exess_write_ubyte.3', 'exess_ubyte.3'],
+ ['ExessDuration.3', 'exess_duration.3'],
+ ['exess_read_duration.3', 'exess_duration.3'],
+ ['exess_write_duration.3', 'exess_duration.3'],
+ ['ExessDateTime.3', 'exess_datetime.3'],
+ ['exess_read_datetime.3', 'exess_datetime.3'],
+ ['exess_write_datetime.3', 'exess_datetime.3'],
+ ['ExessTimezone.3', 'exess_timezone.3'],
+ ['ExessDate.3', 'exess_date.3'],
+ ['exess_read_date.3', 'exess_date.3'],
+ ['exess_write_date.3', 'exess_date.3'],
+ ['ExessTime.3', 'exess_time.3'],
+ ['exess_read_time.3', 'exess_time.3'],
+ ['exess_write_time.3', 'exess_time.3'],
+ ['ExessBlob.3', 'exess_binary.3'],
+ ['ExessDatatype.3', 'exess_datatypes.3'],
+ ['ExessValue.3', 'exess_variant.3'],
+ ['ExessVariant.3', 'exess_variant.3'],
+ ['exess_read_variant.3', 'exess_variant.3'],
+ ['exess_write_variant.3', 'exess_variant.3'],
+ ['exess_write_canonical.3', 'exess_variant.3'],
+ ['ExessCoercionFlag.3', 'exess_variant.3'],
+ ['ExessCoercionFlags.3', 'exess_variant.3'],
+ ['exess_coerce.3', 'exess_variant.3'],
+]
+
+foreach man_link : man_links
+ link = man_link[0]
+ target = man_link[1]
+
+ config = configuration_data()
+ config.set('TARGET', target)
+
+ manlink = configure_file(configuration: config,
+ input: 'manlink.in',
+ output: link,
+ install: true,
+ install_dir: get_option('mandir') / 'man3')
+
+ # message(target)
+
+ # install_man(manlink)
+endforeach
diff --git a/subprojects/exess/doc/c/meson.build b/subprojects/exess/doc/c/meson.build
new file mode 100644
index 00000000..64c02180
--- /dev/null
+++ b/subprojects/exess/doc/c/meson.build
@@ -0,0 +1,53 @@
+config = configuration_data()
+config.set('EXESS_VERSION', meson.project_version())
+
+conf_py = configure_file(configuration: config,
+ input: '../conf.py.in',
+ output: 'conf.py')
+
+configure_file(copy: true, input: '../summary.rst', output: 'summary.rst')
+
+c_rst_files = files(
+ 'index.rst',
+ 'overview.rst',
+)
+
+foreach f : c_rst_files
+ configure_file(copy: true, input: f, output: '@PLAINNAME@')
+endforeach
+
+subdir('xml')
+subdir('api')
+
+custom_target(
+ 'Exess C API Documentation (singlehtml)',
+ command: [sphinx_build, '-M', 'singlehtml',
+ meson.current_build_dir(), meson.current_build_dir(),
+ '-E', '-q', '-t', 'singlehtml'],
+ input: [c_rst_files, c_exess_rst, c_index_xml],
+ output: 'singlehtml',
+ build_by_default: true,
+ install: true,
+ install_dir: docdir / 'exess-0')
+
+custom_target(
+ 'Exess C API Documentation (html)',
+ command: [sphinx_build, '-M', 'html',
+ meson.current_build_dir(), meson.current_build_dir(),
+ '-E', '-q', '-t', 'html'],
+ input: [c_rst_files, c_exess_rst, c_index_xml],
+ output: 'html',
+ build_by_default: true,
+ install: true,
+ install_dir: docdir / 'exess-0')
+
+# custom_target(
+# 'man pages for exess',
+# command: [sphinx_build, '-M', 'man',
+# meson.current_build_dir(), meson.current_build_dir(),
+# '-E', '-q', '-t', 'man'],
+# input: [c_rst_files, c_exess_rst, c_index_xml],
+# output: ['man/exess_base64.3'],
+# build_by_default: true)
+
+subdir('man')
diff --git a/subprojects/exess/doc/c/overview.rst b/subprojects/exess/doc/c/overview.rst
new file mode 100644
index 00000000..1a2d3f6c
--- /dev/null
+++ b/subprojects/exess/doc/c/overview.rst
@@ -0,0 +1,151 @@
+########
+Overview
+########
+
+.. default-domain:: c
+.. highlight:: c
+
+The complete API is declared in ``exess.h``:
+
+.. code-block:: c
+
+ #include <exess/exess.h>
+
+**************
+Reading Values
+**************
+
+Each supported type has a read function that takes a pointer to an output value,
+and a string to read.
+It reads the value after skipping any leading whitespace,
+then returns an :struct:`ExessResult` with a ``status`` code and the ``count`` of characters read.
+For example:
+
+.. code-block:: c
+
+ int32_t value = 0;
+ ExessResult r = exess_read_int(&value, "1234");
+ if (!r.status) {
+ fprintf(stderr, "Read %zu bytes as int %d\n", r.count, value);
+ }
+
+If there was a syntax error,
+the status code indicates the specific problem.
+If a value was read but didn't end at whitespace or the end of the string,
+the status :enumerator:`EXESS_EXPECTED_END` is returned.
+This indicates that there is trailing garbage in the string,
+so the parse may not be complete or correct depending on the context.
+
+
+**************
+Writing Values
+**************
+
+The corresponding write function takes a value to write,
+a buffer size in bytes, and a buffer to write to.
+It returns an :struct:`ExessResult`,
+with a ``status`` code and the ``count`` of characters written,
+not including the trailing null byte.
+
+For datatypes with a bounded length,
+a constant like :var:`EXESS_MAX_INT_LENGTH` is the maximum length of the canonical representation of any value.
+This can be used to allocate buffers statically or on the stack,
+for example:
+
+.. code-block:: c
+
+ char buf[EXESS_MAX_INT_LENGTH + 1] = {0};
+
+ ExessResult r = exess_write_int(1234, sizeof(buf), buf);
+ if (!r.status) {
+ printf("Write error: %s\n", exess_strerror(r.status));
+ }
+
+******************
+Allocating Strings
+******************
+
+Exess doesn't do any allocation itself,
+so the calling code is responsible for providing a large enough buffer for output.
+The `count` returned by write functions can be used to determine the space required for a specific value.
+If the write function is called with a null output buffer,
+then this count is still returned as if a value were written.
+This can be used to precisely allocate memory for the string,
+taking care to allocate an extra byte for the null terminator.
+For example:
+
+.. code-block:: c
+
+ ExessResult r = exess_write_int(1234, 0, NULL);
+ char* str = (char*)calloc(r.count + 1, 1);
+
+ r = exess_write_int(1234, r.count + 1, buf);
+
+Note that for some types,
+this operation can be about as expensive as actually writing the value.
+For example, it requires binary to decimal conversion for floating point numbers.
+For ``float`` and ``double``,
+since the length is bounded and relatively small,
+it may be better to write immediately to a static buffer,
+then copy the result to the final destination.
+
+********
+Variants
+********
+
+The fundamental read and write functions all have similar semantics,
+but different type signatures since they use different value types.
+:struct:`ExessVariant` is a tagged union that can hold any supported value,
+allowing generic code to work with values of any type.
+
+Any value can be read with :func:`exess_read_variant` and written with :func:`exess_write_variant`,
+which work similarly to the fundamental read and write functions,
+except the read function takes an additional ``datatype`` parameter.
+The expected datatype must be provided,
+attempting to infer a datatype from the string content is not supported.
+
+Datatypes
+=========
+
+:enum:`ExessDatatype` enumerates all of the supported variant datatypes.
+The special value :enumerator:`EXESS_NOTHING` is used as a sentinel for unknown datatypes or other errors.
+
+If you have a datatype URI, then :func:`exess_datatype_from_uri()` can be used
+to map it to a datatype. If the URI is not for a supported datatype, then it will return :enumerator:`EXESS_NOTHING`.
+
+Unbounded Numeric Types
+=======================
+
+There are 6 unbounded numeric types:
+decimal, integer, nonPositiveInteger, negativeInteger, nonNegativeInteger, and positiveInteger.
+:struct:`ExessVariant` supports reading and writing these types,
+but stores them in the largest corresponding native type:
+``double``, ``int64_t``, or ``uint64_t``.
+If the value doesn't fit in this type,
+then :func:`exess_read_variant` will return an :enumerator:`EXESS_OUT_OF_RANGE` error.
+
+Writing Canonical Form
+======================
+
+Since values are always written in canonical form,
+:struct:`ExessVariant` can be used as a generic mechanism to convert any string to canonical form:
+simply read a value,
+then write it.
+If the value itself isn't required,
+then :func:`exess_write_canonical` can be used to do this in a single step.
+For example, this will print ``123``:
+
+.. code-block:: c
+
+ char buf[4] = {0};
+
+ ExessResult r = exess_write_canonical(" +123", EXESS_INT, sizeof(buf), buf);
+ if (!r) {
+ printf("%s\n", buf);
+ }
+
+Note that it is better to use :func:`exess_write_canonical` if the value isn't required,
+since it supports transforming some values outside the range of :struct:`ExessVariant`.
+Specifically,
+decimal and integer strings will be transformed directly,
+avoiding conversion into values and the limits of the machine's numeric types.
diff --git a/subprojects/exess/doc/c/xml/meson.build b/subprojects/exess/doc/c/xml/meson.build
new file mode 100644
index 00000000..cfa726ed
--- /dev/null
+++ b/subprojects/exess/doc/c/xml/meson.build
@@ -0,0 +1,12 @@
+config = configuration_data()
+config.set('EXESS_SRCDIR', exess_src_root)
+config.set('DOX_OUTPUT', meson.current_build_dir() / '..')
+
+c_doxyfile = configure_file(configuration: config,
+ input: '../Doxyfile.in',
+ output: 'Doxyfile')
+
+c_index_xml = custom_target('exess-c-index.xml',
+ command: [doxygen, '@INPUT0@'],
+ input: [c_doxyfile] + c_header_files,
+ output: 'index.xml')
diff --git a/subprojects/exess/doc/conf.py.in b/subprojects/exess/doc/conf.py.in
new file mode 100644
index 00000000..3eba5bc1
--- /dev/null
+++ b/subprojects/exess/doc/conf.py.in
@@ -0,0 +1,112 @@
+# Project information
+
+project = "Exess"
+copyright = "2021, David Robillard"
+author = "David Robillard"
+release = "@EXESS_VERSION@"
+
+# General configuration
+
+exclude_patterns = ["xml"]
+language = "en"
+nitpicky = True
+pygments_style = "friendly"
+
+# Ignore everything opaque or external for nitpicky mode
+_opaque = [
+ "int16_t",
+ "int32_t",
+ "int64_t",
+ "int8_t",
+ "size_t",
+ "uint16_t",
+ "uint32_t",
+ "uint64_t",
+ "uint8_t",
+]
+
+_c_nitpick_ignore = map(lambda x: ("c:identifier", x), _opaque)
+_cpp_nitpick_ignore = map(lambda x: ("cpp:identifier", x), _opaque)
+nitpick_ignore = list(_c_nitpick_ignore) + list(_cpp_nitpick_ignore)
+
+# HTML output
+
+html_copy_source = False
+html_short_title = "Exess"
+html_theme = "sphinx_lv2_theme"
+
+if tags.has("singlehtml"):
+ html_sidebars = {
+ "**": [
+ "globaltoc.html",
+ ]
+ }
+
+ html_theme_options = {
+ "body_max_width": "48em",
+ "body_min_width": "48em",
+ "description": "A library for reading and writing XSD datatypes",
+ "show_footer_version": True,
+ "show_logo_version": False,
+ "logo_name": True,
+ "logo_width": "8em",
+ "nosidebar": False,
+ "page_width": "80em",
+ "sidebar_width": "18em",
+ "globaltoc_maxdepth": 3,
+ "globaltoc_collapse": False,
+ }
+
+else:
+ html_theme_options = {
+ "body_max_width": "60em",
+ "body_min_width": "40em",
+ "description": "A library for reading and writing XSD datatypes",
+ "show_footer_version": True,
+ "show_logo_version": False,
+ "logo_name": True,
+ "logo_width": "8em",
+ "nosidebar": True,
+ "page_width": "60em",
+ "sidebar_width": "14em",
+ "globaltoc_maxdepth": 1,
+ "globaltoc_collapse": True,
+ }
+
+# Man page output
+
+groups = {
+ "status": "Status",
+ # "numbers": "",
+ "decimal": "Decimal Strings",
+ "double": "Double Strings",
+ "float": "Float Strings",
+ "boolean": "Boolean Strings",
+ "long": "Long Strings",
+ "int": "Int Strings",
+ "short": "Short Strings",
+ "byte": "Byte Strings",
+ "ulong": "Unsigned Long Strings",
+ "uint": "Unsigned Int Strings",
+ "ushort": "Unsigned Short Strings",
+ "ubyte": "Unsigned Byte Strings",
+ # 'time_and_date': "",
+ "duration": "Duration Strings",
+ "datetime": "Datetime Strings",
+ "timezone": "Time zone strings",
+ "date": "Date strings",
+ "time": "Time strings",
+ "binary": "",
+ "hex": "Hex Binary Strings",
+ "base64": "Base64 Binary Strings",
+ "datatypes": "Datatypes",
+ "variant": "Value Variant",
+ # 'generics': "",
+ "coercion": "Value Type Coercion",
+}
+
+author = "David Robillard <d@drobilla.net>"
+man_pages = []
+for group, title in groups.items():
+ name = "exess_" + group
+ man_pages += [("api/" + name, name, title, author, 3)]
diff --git a/subprojects/exess/doc/cpp/Doxyfile.in b/subprojects/exess/doc/cpp/Doxyfile.in
new file mode 100644
index 00000000..d254aa77
--- /dev/null
+++ b/subprojects/exess/doc/cpp/Doxyfile.in
@@ -0,0 +1,44 @@
+PROJECT_NAME = Exess
+PROJECT_BRIEF = "A library for reading and writing XSD datatypes"
+
+QUIET = YES
+WARN_AS_ERROR = YES
+WARN_IF_UNDOCUMENTED = NO
+WARN_NO_PARAMDOC = NO
+
+JAVADOC_AUTOBRIEF = YES
+
+CASE_SENSE_NAMES = YES
+EXCLUDE_SYMBOLS = exess::detail
+EXTRACT_LOCAL_CLASSES = NO
+EXTRACT_PRIVATE = NO
+HIDE_IN_BODY_DOCS = YES
+HIDE_UNDOC_CLASSES = YES
+HIDE_UNDOC_MEMBERS = YES
+REFERENCES_LINK_SOURCE = NO
+
+GENERATE_HTML = NO
+GENERATE_LATEX = NO
+GENERATE_XML = YES
+XML_PROGRAMLISTING = NO
+SHOW_FILES = NO
+
+ENABLE_PREPROCESSING = YES
+SKIP_FUNCTION_MACROS = NO
+
+EXPAND_ONLY_PREDEF = YES
+MACRO_EXPANSION = YES
+PREDEFINED = EXESS_API \
+ EXESS_CONST_API \
+ EXESS_CONST_FUNC= \
+ EXESS_NONNULL= \
+ EXESS_NULLABLE= \
+ EXESS_PURE_API \
+ EXESS_PURE_FUNC= \
+
+RECURSIVE = YES
+STRIP_FROM_PATH = @EXESS_SRCDIR@
+INPUT = @EXESS_SRCDIR@/include \
+ @EXESS_SRCDIR@/bindings/cpp/include
+
+OUTPUT_DIRECTORY = @DOX_OUTPUT@
diff --git a/subprojects/exess/doc/cpp/api/meson.build b/subprojects/exess/doc/cpp/api/meson.build
new file mode 100644
index 00000000..6cdf4e04
--- /dev/null
+++ b/subprojects/exess/doc/cpp/api/meson.build
@@ -0,0 +1,5 @@
+cpp_exess_rst = custom_target(
+ 'Exess C++ API Sphinx Input',
+ command: [dox_to_sphinx, '-l', 'cpp', '-f', '@INPUT@', 'doc/cpp/api'],
+ input: cpp_index_xml,
+ output: 'exess.rst')
diff --git a/subprojects/exess/doc/cpp/index.rst b/subprojects/exess/doc/cpp/index.rst
new file mode 100644
index 00000000..a88d3007
--- /dev/null
+++ b/subprojects/exess/doc/cpp/index.rst
@@ -0,0 +1,11 @@
+#####
+Exess
+#####
+
+.. include:: summary.rst
+
+.. toctree::
+
+ overview
+ api/exesspp
+ api/exess
diff --git a/subprojects/exess/doc/cpp/meson.build b/subprojects/exess/doc/cpp/meson.build
new file mode 100644
index 00000000..27dc34c5
--- /dev/null
+++ b/subprojects/exess/doc/cpp/meson.build
@@ -0,0 +1,42 @@
+config = configuration_data()
+config.set('EXESS_VERSION', meson.project_version())
+
+conf_py = configure_file(configuration: config,
+ input: '../conf.py.in',
+ output: 'conf.py')
+
+configure_file(copy: true, input: '../summary.rst', output: 'summary.rst')
+
+cpp_rst_files = files(
+ 'index.rst',
+ 'overview.rst',
+)
+
+foreach f : cpp_rst_files
+ configure_file(copy: true, input: f, output: '@PLAINNAME@')
+endforeach
+
+subdir('xml')
+subdir('api')
+
+docs = custom_target(
+ 'Exess C++ API Documentation (singlehtml)',
+ command: [sphinx_build, '-M', 'singlehtml',
+ meson.current_build_dir(), meson.current_build_dir(),
+ '-E', '-q', '-t', 'singlehtml'],
+ input: [cpp_rst_files, cpp_exess_rst, cpp_index_xml],
+ output: 'singlehtml',
+ build_by_default: true,
+ install: true,
+ install_dir: docdir / 'exessxx-0')
+
+docs = custom_target(
+ 'Exess C++ API Documentation (html)',
+ command: [sphinx_build, '-M', 'html',
+ meson.current_build_dir(), meson.current_build_dir(),
+ '-E', '-q', '-t', 'html'],
+ input: [cpp_rst_files, cpp_exess_rst, cpp_index_xml],
+ output: 'html',
+ build_by_default: true,
+ install: true,
+ install_dir: docdir / 'exessxx-0')
diff --git a/subprojects/exess/doc/cpp/overview.rst b/subprojects/exess/doc/cpp/overview.rst
new file mode 100644
index 00000000..3e59a953
--- /dev/null
+++ b/subprojects/exess/doc/cpp/overview.rst
@@ -0,0 +1,151 @@
+########
+Overview
+########
+
+.. default-domain:: cpp
+.. highlight:: cpp
+.. namespace:: exess
+
+The complete API is declared in ``exess.hpp``:
+
+.. code-block:: cpp
+
+ #include <exess/exess.hpp>
+
+**************
+Reading Values
+**************
+
+Each supported type has a read function that takes a pointer to an output value,
+and a string to read.
+It reads the value after skipping any leading whitespace,
+then returns an :struct:`ExessResult` with a ``status`` code and the ``count`` of characters read.
+For example:
+
+.. code-block:: cpp
+
+ int32_t value = 0;
+ ExessResult r = exess_read_int(&value, "1234");
+ if (!r.status) {
+ fprintf(stderr, "Read %zu bytes as int %d\n", r.count, value);
+ }
+
+If there was a syntax error,
+the status code indicates the specific problem.
+If a value was read but didn't end at whitespace or the end of the string,
+the status :enumerator:`EXESS_EXPECTED_END` is returned.
+This indicates that there is trailing garbage in the string,
+so the parse may not be complete or correct depending on the context.
+
+**************
+Writing Values
+**************
+
+The corresponding write function takes a value to write,
+a buffer size in bytes, and a buffer to write to.
+It returns an :struct:`ExessResult`,
+with a ``status`` code and the ``count`` of characters written,
+not including the trailing null byte.
+
+For datatypes with a bounded length,
+the `constexpr` function template :func:`max_length` returns the maximum length of the canonical representation of any value.
+This can be used to allocate buffers statically or on the stack,
+for example:
+
+.. code-block:: cpp
+
+ char buf[exess::max_length<int>() + 1] = {0};
+
+ exess::Result r = exess::write(1234, sizeof(buf), buf);
+ if (r.status != exess::Status::success) {
+ std::cerr << "Write error: " << exess::strerror(r.status) << "\n";
+ }
+
+******************
+Allocating Strings
+******************
+
+Exess doesn't do any allocation itself,
+so the calling code is responsible for providing a large enough buffer for output.
+The `count` returned by write functions can be used to determine the space required for a specific value.
+If the write function is called with a null output buffer,
+then this count is still returned as if a value were written.
+This can be used to precisely allocate memory for the string,
+taking care to allocate an extra byte for the null terminator.
+For example:
+
+.. code-block:: cpp
+
+ exess::Result r = exess::write(1234, 0, NULL);
+ char* str = (char*)calloc(r.count + 1, 1);
+
+ r = exess_write_int(1234, r.count + 1, buf);
+
+Note that for some types,
+this operation can be about as expensive as actually writing the value.
+For example, it requires binary to decimal conversion for floating point numbers.
+For ``float`` and ``double``,
+since the length is bounded and relatively small,
+it may be better to write immediately to a static buffer,
+then copy the result to the final destination.
+
+********
+Variants
+********
+
+The fundamental read and write functions all have similar semantics,
+but different type signatures since they use different value types.
+:struct:`ExessVariant` is a tagged union that can hold any supported value,
+allowing generic code to work with values of any type.
+
+Any value can be read with :func:`exess_read_variant` and written with :func:`exess_write_variant`,
+which work similarly to the fundamental read and write functions,
+except the read function takes an additional ``datatype`` parameter.
+The expected datatype must be provided,
+attempting to infer a datatype from the string content is not supported.
+
+Datatypes
+=========
+
+:enum:`ExessDatatype` enumerates all of the supported variant datatypes.
+The special value :enumerator:`EXESS_NOTHING` is used as a sentinel for unknown datatypes or other errors.
+
+If you have a datatype URI, then :func:`exess_datatype_from_uri()` can be used
+to map it to a datatype. If the URI is not for a supported datatype, then it will return :enumerator:`EXESS_NOTHING`.
+
+Unbounded Numeric Types
+=======================
+
+There are 6 unbounded numeric types:
+decimal, integer, nonPositiveInteger, negativeInteger, nonNegativeInteger, and positiveInteger.
+:struct:`ExessVariant` supports reading and writing these types,
+but stores them in the largest corresponding native type:
+``double``, ``int64_t``, or ``uint64_t``.
+If the value doesn't fit in this type,
+then :func:`exess_read_variant` will return an :enumerator:`EXESS_OUT_OF_RANGE` error.
+
+Writing Canonical Form
+======================
+
+Since values are always written in canonical form,
+:struct:`ExessVariant` can be used as a generic mechanism to convert any string to canonical form:
+simply read a value,
+then write it.
+If the value itself isn't required,
+then :func:`exess_write_canonical` can be used to do this in a single step.
+For example, this will print ``123``:
+
+.. code-block:: cpp
+
+ char buf[4] = {0};
+
+ ExessResult r = exess_write_canonical(" +123", EXESS_INT, sizeof(buf), buf);
+ if (!r) {
+ printf("%s\n", buf);
+ }
+
+Note that it is better to use :func:`exess_write_canonical` if the value isn't required,
+since it supports transforming some values outside the range of :struct:`ExessVariant`.
+Specifically,
+decimal and integer strings will be transformed directly,
+avoiding conversion into values and the limits of the machine's numeric types.
diff --git a/subprojects/exess/doc/cpp/xml/meson.build b/subprojects/exess/doc/cpp/xml/meson.build
new file mode 100644
index 00000000..ee529bca
--- /dev/null
+++ b/subprojects/exess/doc/cpp/xml/meson.build
@@ -0,0 +1,14 @@
+config = configuration_data()
+config.set('EXESS_SRCDIR', exess_src_root)
+config.set('DOX_OUTPUT', meson.current_build_dir() / '..')
+
+cpp_doxyfile = configure_file(configuration: config,
+ input: '../Doxyfile.in',
+ output: 'Doxyfile')
+
+cpp_index_xml = custom_target(
+ 'exess-cpp-index.xml',
+ command: [doxygen, '@INPUT0@'],
+ input: [cpp_doxyfile] + c_header_files + cpp_header_files,
+ output: 'index.xml')
+
diff --git a/subprojects/exess/doc/meson.build b/subprojects/exess/doc/meson.build
new file mode 100644
index 00000000..e69c4f05
--- /dev/null
+++ b/subprojects/exess/doc/meson.build
@@ -0,0 +1,17 @@
+docdir = get_option('datadir') / 'doc'
+
+doxygen = find_program('doxygen', required: get_option('docs'))
+dox_to_sphinx = find_program('../scripts/dox_to_sphinx.py')
+sphinx_build = find_program('sphinx-build', required: get_option('docs'))
+
+build_docs = doxygen.found() and sphinx_build.found()
+
+if build_docs
+ subdir('c')
+ subdir('cpp')
+endif
+
+if meson.version().version_compare('>=0.53.0')
+ summary('Documentation', build_docs, bool_yn: true)
+endif
+
diff --git a/subprojects/exess/doc/summary.rst b/subprojects/exess/doc/summary.rst
new file mode 100644
index 00000000..7e4dd84f
--- /dev/null
+++ b/subprojects/exess/doc/summary.rst
@@ -0,0 +1,3 @@
+Exess is a simple C library for reading and writing XSD_ datatypes.
+
+.. _XSD: https://www.w3.org/TR/xmlschema-2/
diff --git a/subprojects/exess/include/exess/exess.h b/subprojects/exess/include/exess/exess.h
new file mode 100644
index 00000000..f327eb2e
--- /dev/null
+++ b/subprojects/exess/include/exess/exess.h
@@ -0,0 +1,1633 @@
+/*
+ Copyright 2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_EXESS_H
+#define EXESS_EXESS_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#if defined(_WIN32) && !defined(EXESS_STATIC) && defined(EXESS_INTERNAL)
+# define EXESS_API __declspec(dllexport)
+#elif defined(_WIN32) && !defined(EXESS_STATIC)
+# define EXESS_API __declspec(dllimport)
+#elif defined(__GNUC__)
+# define EXESS_API __attribute__((visibility("default")))
+#else
+# define EXESS_API
+#endif
+
+#ifdef __GNUC__
+# define EXESS_PURE_FUNC __attribute__((pure))
+# define EXESS_CONST_FUNC __attribute__((const))
+#else
+# define EXESS_PURE_FUNC
+# define EXESS_CONST_FUNC
+#endif
+
+#if defined(__clang__) && __clang_major__ >= 7
+# define EXESS_NONNULL _Nonnull
+# define EXESS_NULLABLE _Nullable
+#else
+# define EXESS_NONNULL
+# define EXESS_NULLABLE
+#endif
+
+// Pure API functions have no observable side-effects
+#define EXESS_PURE_API \
+ EXESS_API \
+ EXESS_PURE_FUNC
+
+// Const API functions are pure, and read no memory other than their parameters
+#define EXESS_CONST_API \
+ EXESS_API \
+ EXESS_CONST_FUNC
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ @defgroup exess Exess C API
+ This is the complete public C API of exess.
+ @{
+*/
+
+/// The base URI of XML Schema, `http://www.w3.org/2001/XMLSchema#`
+#define EXESS_XSD_URI "http://www.w3.org/2001/XMLSchema#"
+
+/**
+ @defgroup exess_status Status
+
+ Status codes and return values used for error handling.
+
+ Success and various specific errors are reported by an integer status code,
+ which can be converted to a string to produce friendly error messages.
+ Reading and writing functions return a "result", which has a status code
+ along with a count of bytes read or written.
+
+ @{
+*/
+
+/// Status code to describe errors or other relevant situations
+typedef enum {
+ EXESS_SUCCESS, ///< Success
+ EXESS_EXPECTED_END, ///< Expected end of value
+ EXESS_EXPECTED_BOOLEAN, ///< Expected "false", "true", "0" or "1"
+ EXESS_EXPECTED_INTEGER, ///< Expected an integer value
+ EXESS_EXPECTED_DURATION, ///< Expected a duration starting with 'P'
+ EXESS_EXPECTED_SIGN, ///< Expected '-' or '+'
+ EXESS_EXPECTED_DIGIT, ///< Expected a digit
+ EXESS_EXPECTED_COLON, ///< Expected ':'
+ EXESS_EXPECTED_DASH, ///< Expected '-'
+ EXESS_EXPECTED_TIME_SEP, ///< Expected 'T'
+ EXESS_EXPECTED_TIME_TAG, ///< Expected 'H', 'M', or 'S'
+ EXESS_EXPECTED_DATE_TAG, ///< Expected 'Y', 'M', or 'D'
+ EXESS_EXPECTED_HEX, ///< Expected a hexadecimal character
+ EXESS_EXPECTED_BASE64, ///< Expected a base64 character
+ EXESS_BAD_ORDER, ///< Invalid field order
+ EXESS_BAD_VALUE, ///< Invalid value
+ EXESS_OUT_OF_RANGE, ///< Value out of range for datatype
+ EXESS_NO_SPACE, ///< Insufficient space
+ EXESS_WOULD_REDUCE_PRECISION, ///< Precision reducing coercion required
+ EXESS_WOULD_ROUND, ///< Rounding coercion required
+ EXESS_WOULD_TRUNCATE, ///< Truncating coercion required
+ EXESS_UNSUPPORTED, ///< Unsupported value
+} ExessStatus;
+
+/**
+ Result returned from a read or write function.
+
+ This combines a status code with a byte offset, so it can be used to
+ determine how many characters were read or written, or what error occurred
+ at what character offset.
+*/
+typedef struct {
+ ExessStatus status; ///< Status code
+ size_t count; ///< Number of bytes read or written, excluding null
+} ExessResult;
+
+/**
+ Return a string describing a status code in plain English.
+
+ The returned string is always one sentence, with an uppercase first
+ character, and no trailing period.
+*/
+EXESS_CONST_API
+const char* EXESS_NONNULL
+exess_strerror(ExessStatus status);
+
+/**
+ @}
+ @defgroup exess_numbers Numbers
+ Datatypes for numbers
+ @{
+*/
+
+/**
+ @defgroup exess_decimal Decimal
+
+ An xsd:decimal is a decimal number of arbitrary precision, but this
+ implementation only supports values that fit in a `double`.
+
+ Unlike xsd:double, xsd:decimal is written in numeric form, never in
+ scientific notation. Special infinity and NaN values are not supported.
+ Note that the xsd:decimal representation for some numbers is very long, so
+ xsd:double may be a better choice for values in a wide range.
+
+ Canonical form has no leading "+" sign, and at most 1 leading or trailing
+ zero such that there is at least 1 digit on either side of the decimal
+ point, like "12.34", "-1.0", and "0.0".
+
+ Non-canonical form allows a leading "+", any number of leading and trailing
+ zeros, any number of digits (including zero) on either side of the point,
+ and does not require a decimal point, like "+1", "01", "-.5", "4.", and
+ "42".
+
+ @{
+*/
+
+/// The maximum length of an xsd:decimal string from exess_write_decimal(), 327
+#define EXESS_MAX_DECIMAL_LENGTH 327
+
+/**
+ Read an xsd:decimal string after any leading whitespace.
+
+ Values beyond the range of `decimal` will produce `-INF` or `INF`, and
+ return an error because these are not valid decimal values.
+
+ @param out Set to the parsed value, or NaN on error.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_decimal(double* EXESS_NONNULL out, const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical xsd:decimal string.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return The `count` of characters in the output, and `status`
+ #EXESS_SUCCESS, or #EXESS_NO_SPACE if the buffer is too small.
+*/
+EXESS_API
+ExessResult
+exess_write_decimal(double value, size_t buf_size, char* EXESS_NULLABLE buf);
+
+/**
+ @}
+ @defgroup exess_double Double
+
+ An xsd:double is an IEEE-754 64-bit floating point number, written in
+ scientific notation.
+
+ Canonical form has no leading "+" sign, at most 1 leading or trailing zero
+ such that there is at least 1 digit on either side of the decimal point, and
+ always has an exponent, like "12.34E56", "-1.0E-2", and "-0.0E0". The
+ special values negative infinity, positive infinity, and not-a-number are
+ written "-INF", "INF", and "NaN", respectively.
+
+ Non-canonical form allows a leading "+", any number of leading and trailing
+ zeros, any number of digits (including zero) on either side of the point,
+ and does not require an exponent or decimal point, like "+1E3", "1E+3",
+ ".5E3", "4.2", and "42".
+
+ @{
+*/
+
+/// The maximum length of a canonical xsd:double string, 24
+#define EXESS_MAX_DOUBLE_LENGTH 24
+
+/**
+ Read an xsd:double string after any leading whitespace.
+
+ Values beyond the range of `double` will produce `-INF` or `INF`.
+
+ @param out Set to the parsed value, or `NAN` on error.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_double(double* EXESS_NONNULL out, const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical xsd:double string.
+
+ Any `double` value is supported. Reading the resulting string with
+ exess_read_double() will produce exactly `value`, except the extra bits in
+ NaNs are not preserved.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return The `count` of characters in the output and `status` #EXESS_SUCCESS,
+ or #EXESS_NO_SPACE if the buffer is too small.
+*/
+EXESS_API
+ExessResult
+exess_write_double(double value, size_t buf_size, char* EXESS_NULLABLE buf);
+
+/**
+ @}
+ @defgroup exess_float Float
+
+ An xsd:float is an IEEE-754 32-bit floating point number, written in
+ scientific notation.
+
+ The lexical form is the same as xsd:double, the only difference is that the
+ value space of xsd:float is smaller. See @ref exess_double for details.
+
+ @{
+*/
+
+/// The maximum length of a canonical xsd:float string, 15
+#define EXESS_MAX_FLOAT_LENGTH 15
+
+/**
+ Read an xsd:float string after any leading whitespace.
+
+ Values beyond the range of `float` will produce `-INF` or `INF`.
+
+ @param out Set to the parsed value, or `NAN` on error.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_float(float* EXESS_NONNULL out, const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical xsd:float string.
+
+ Any `float` value is supported. Reading the resulting string with
+ exess_read_float() will produce exactly `value`, except the extra bits in
+ NaNs are not preserved.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return The `count` of characters in the output, and `status`
+ #EXESS_SUCCESS, or #EXESS_NO_SPACE if the buffer is too small.
+*/
+EXESS_API
+ExessResult
+exess_write_float(float value, size_t buf_size, char* EXESS_NULLABLE buf);
+
+/**
+ @}
+ @defgroup exess_boolean Boolean
+
+ An xsd:boolean has only two possible values, canonically written as "false"
+ and "true". The non-canonical forms "0" and "1" are also supported.
+
+ @{
+*/
+
+/// The maximum length of a canonical xsd:boolean string, 5
+#define EXESS_MAX_BOOLEAN_LENGTH 5
+
+/**
+ Read an xsd:boolean string after any leading whitespace.
+
+ @param out Set to the parsed value, or false on error.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_boolean(bool* EXESS_NONNULL out, const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical xsd:boolean string.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return The `count` of characters in the output, and `status`
+ #EXESS_SUCCESS, or #EXESS_NO_SPACE if the buffer is too small.
+*/
+EXESS_API
+ExessResult
+exess_write_boolean(bool value, size_t buf_size, char* EXESS_NULLABLE buf);
+
+/**
+ @}
+ @defgroup exess_long Long
+
+ An xsd:long is a signed 64-bit integer, written in decimal.
+
+ Values range from -9223372036854775808 to 9223372036854775807 inclusive.
+
+ Canonical form has no leading "+" sign and no leading zeros (except for the
+ number "0"), like "-1", "0", and "1234".
+
+ Non-canonical form allows a leading "+" and any number of leading zeros,
+ like "01" and "+0001234".
+
+ @{
+*/
+
+/// The maximum length of a canonical xsd:long string, 20
+#define EXESS_MAX_LONG_LENGTH 20
+
+/**
+ Read an xsd:long string after any leading whitespace.
+
+ @param out Set to the parsed value, or zero on error.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_long(int64_t* EXESS_NONNULL out, const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical xsd:long string.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return The `count` of characters in the output, and `status`
+ #EXESS_SUCCESS, or #EXESS_NO_SPACE if the buffer is too small.
+*/
+EXESS_API
+ExessResult
+exess_write_long(int64_t value, size_t buf_size, char* EXESS_NULLABLE buf);
+
+/**
+ @}
+ @defgroup exess_int Int
+
+ An xsd:int is a signed 32-bit integer.
+
+ Values range from -2147483648 to 2147483647 inclusive.
+
+ The lexical form is the same as xsd:long, the only difference is that the
+ value space of xsd:int is smaller. See @ref exess_long for details.
+
+ @{
+*/
+
+/// The maximum length of a canonical xsd:int string, 11
+#define EXESS_MAX_INT_LENGTH 11
+
+/**
+ Read an xsd:int string after any leading whitespace.
+
+ @param out Set to the parsed value, or zero on error.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_int(int32_t* EXESS_NONNULL out, const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical xsd:int string.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return The `count` of characters in the output, and `status`
+ #EXESS_SUCCESS, or #EXESS_NO_SPACE if the buffer is too small.
+*/
+EXESS_API
+ExessResult
+exess_write_int(int32_t value, size_t buf_size, char* EXESS_NULLABLE buf);
+
+/**
+ @}
+ @defgroup exess_short Short
+
+ An xsd:short is a signed 16-bit integer.
+
+ Values range from -32768 to 32767 inclusive.
+
+ The lexical form is the same as xsd:long, the only difference is that the
+ value space of xsd:short is smaller. See @ref exess_long for details.
+
+ @{
+*/
+
+/// The maximum length of a canonical xsd:short string, 6
+#define EXESS_MAX_SHORT_LENGTH 6
+
+/**
+ Read an xsd:short string after any leading whitespace.
+
+ @param out Set to the parsed value, or zero on error.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_short(int16_t* EXESS_NONNULL out, const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical xsd:short string.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return The `count` of characters in the output, and `status`
+ #EXESS_SUCCESS, or #EXESS_NO_SPACE if the buffer is too small.
+*/
+EXESS_API
+ExessResult
+exess_write_short(int16_t value, size_t buf_size, char* EXESS_NULLABLE buf);
+
+/**
+ @}
+ @defgroup exess_byte Byte
+
+ An xsd:byte is a signed 8-bit integer.
+
+ Values range from -128 to 127 inclusive.
+
+ The lexical form is the same as xsd:long, the only difference is that the
+ value space of xsd:byte is smaller. See @ref exess_long for details.
+
+ @{
+*/
+
+/// The maximum length of a canonical xsd:byte string, 4
+#define EXESS_MAX_BYTE_LENGTH 4
+
+/**
+ Read an xsd:byte string after any leading whitespace.
+
+ @param out Set to the parsed value, or zero on error.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_byte(int8_t* EXESS_NONNULL out, const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical xsd:byte string.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return The `count` of characters in the output, and `status`
+ #EXESS_SUCCESS, or #EXESS_NO_SPACE if the buffer is too small.
+*/
+EXESS_API
+ExessResult
+exess_write_byte(int8_t value, size_t buf_size, char* EXESS_NULLABLE buf);
+
+/**
+ @}
+ @defgroup exess_ulong Unsigned Long
+
+ An xsd:unsignedLong is an unsigned 64-bit integer, written in decimal.
+
+ Values range from 0 to 18446744073709551615 inclusive.
+
+ Canonical form has no leading "+" sign and no leading zeros (except for the
+ number "0"), like "0", and "1234".
+
+ Non-canonical form allows any number of leading zeros, like "01" and
+ "0001234".
+
+ @{
+*/
+
+/// The maximum length of a canonical xsd:unsignedLong string, 20
+#define EXESS_MAX_ULONG_LENGTH 20
+
+/**
+ Read an xsd:unsignedLong string after any leading whitespace.
+
+ @param out Set to the parsed value, or zero on error.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_ulong(uint64_t* EXESS_NONNULL out, const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical xsd:unsignedLong string.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return The `count` of characters in the output, and `status`
+ #EXESS_SUCCESS, or #EXESS_NO_SPACE if the buffer is too small.
+*/
+EXESS_API
+ExessResult
+exess_write_ulong(uint64_t value, size_t buf_size, char* EXESS_NULLABLE buf);
+
+/**
+ @}
+ @defgroup exess_uint Unsigned Int
+
+ An xsd:unsignedInt is an unsigned 32-bit integer.
+
+ Values range from 0 to 4294967295 inclusive.
+
+ The lexical form is the same as xsd:unsignedLong, the only difference is
+ that the value space of xsd:unsignedInt is smaller. See @ref exess_ulong
+ for details.
+
+ @{
+*/
+
+/// The maximum length of a canonical xsd:unsignedInt string, 10
+#define EXESS_MAX_UINT_LENGTH 10
+
+/**
+ Read an xsd:unsignedInt string after any leading whitespace.
+
+ @param out Set to the parsed value, or zero on error.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_uint(uint32_t* EXESS_NONNULL out, const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical xsd:unsignedInt string.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return The `count` of characters in the output, and `status`
+ #EXESS_SUCCESS, or #EXESS_NO_SPACE if the buffer is too small.
+*/
+EXESS_API
+ExessResult
+exess_write_uint(uint32_t value, size_t buf_size, char* EXESS_NULLABLE buf);
+
+/**
+ @}
+ @defgroup exess_ushort Unsigned Short
+
+ An xsd:unsignedShort is an unsigned 16-bit integer.
+
+ Values range from 0 to 65535 inclusive.
+
+ The lexical form is the same as xsd:unsignedLong, the only difference is
+ that the value space of xsd:unsignedShort is smaller. See @ref exess_ulong
+ for details.
+
+ @{
+*/
+
+/// The maximum length of a canonical xsd:unsignedShort string, 5
+#define EXESS_MAX_USHORT_LENGTH 5
+
+/**
+ Read an xsd:unsignedShort string after any leading whitespace.
+
+ @param out Set to the parsed value, or zero on error.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_ushort(uint16_t* EXESS_NONNULL out, const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical xsd:unsignedShort string.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return The `count` of characters in the output, and `status`
+ #EXESS_SUCCESS, or #EXESS_NO_SPACE if the buffer is too small.
+*/
+EXESS_API
+ExessResult
+exess_write_ushort(uint16_t value, size_t buf_size, char* EXESS_NULLABLE buf);
+
+/**
+ @}
+ @defgroup exess_ubyte Unsigned Byte
+
+ An xsd:unsignedByte is an unsigned 8-bit integer. Values range from 0 to
+ 255 inclusive.
+
+ The lexical form is the same as xsd:unsignedLong, the only difference is
+ that the value space of xsd:unsignedByte is smaller. See @ref exess_ulong
+ for details.
+
+ @{
+*/
+
+/// The maximum length of a canonical xsd:unsignedByte string, 3
+#define EXESS_MAX_UBYTE_LENGTH 3
+
+/**
+ Read an xsd:unsignedByte string after any leading whitespace.
+
+ @param out Set to the parsed value, or zero on error.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_ubyte(uint8_t* EXESS_NONNULL out, const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical xsd:unsignedByte string.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return The `count` of characters in the output, and `status`
+ #EXESS_SUCCESS, or #EXESS_NO_SPACE if the buffer is too small.
+*/
+EXESS_API
+ExessResult
+exess_write_ubyte(uint8_t value, size_t buf_size, char* EXESS_NULLABLE buf);
+
+/**
+ @}
+ @}
+ @defgroup exess_time_and_date Time and Date
+ Datatypes for dates, times, and durations of time.
+ @{
+*/
+
+/**
+ @defgroup exess_duration Duration
+
+ An xsd:duration is a positive or negative duration of time, written in ISO
+ 8601 format like "PnYnMnDTnHnMnS" where each "n" is a number and fields may
+ be omitted if they are zero.
+
+ All numbers must be integers, except for seconds which may be a decimal. If
+ seconds is a decimal, then at least one digit must follow the decimal point.
+ A negative duration is written with "-" as the first character, for example
+ "-P60D".
+
+ Canonical form omits all zero fields and writes no leading or trailing
+ zeros, except for the zero duration which is written "P0Y", for example
+ "P1DT2H", "PT30M", or "PT4.5S".
+
+ Non-canonical form allows zero fields, leading zeros, and for seconds to be
+ written as a decimal even if it is integer, for example "P06D", "PT7.0S", or
+ "P0Y0M01DT06H00M00S".
+
+ @{
+*/
+
+/// The maximum length of an xsd:duration string from exess_write_duration(), 41
+#define EXESS_MAX_DURATION_LENGTH 41
+
+/**
+ A duration of time (xsd:duration value).
+
+ To save space and to simplify arithmetic, this representation only stores
+ two values: integer months, and decimal seconds (to nanosecond precision).
+ These values are converted to and from the other fields during writing and
+ reading. Years and months are stored as months, and days, hours, minutes,
+ and seconds are stored as seconds.
+
+ The sign of all members must match, so a negative duration has all
+ non-positive members, and a positive duration has all non-negative members.
+*/
+typedef struct {
+ int32_t months;
+ int32_t seconds;
+ int32_t nanoseconds;
+} ExessDuration;
+
+/**
+ Read an xsd:duration string after any leading whitespace.
+
+ @param out Set to the parsed value, or zero on error.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_duration(ExessDuration* EXESS_NONNULL out,
+ const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical xsd:duration string.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return #EXESS_SUCCESS on success, #EXESS_NO_SPACE if the buffer is too
+ small, or #EXESS_BAD_VALUE if the value is invalid.
+*/
+EXESS_API
+ExessResult
+exess_write_duration(ExessDuration value,
+ size_t buf_size,
+ char* EXESS_NULLABLE buf);
+/**
+ @}
+ @defgroup exess_datetime Datetime
+
+ An xsd:datetime is a date and time in either UTC or local time.
+
+ Strings have the form YYYY-MM-DDTHH:MM:SS with at least 4 year digits
+ (negative or positive), and all other fields positive two-digit integers
+ except seconds which may be a decimal, for example "2001-02-03T12:13:14.56".
+ Nanosecond precision is supported.
+
+ A local datetime has no suffix, a datetime with a time zone is always in
+ UTC, and is written with a "Z" suffix, for example 2001-02-03T12:13:14Z.
+
+ Canonical form only includes a decimal point if the number of seconds is not
+ an integer.
+
+ This implementation supports up to nanosecond resolution.
+
+ @{
+*/
+
+/// The maximum length of an xsd:dateTime string from exess_write_datetime(), 32
+#define EXESS_MAX_DATETIME_LENGTH 32
+
+/**
+ A date and time (xsd:dateTime value).
+
+ This representation follows the syntax, except the UTC flag is stored
+ between the date and time for more efficient packing.
+*/
+typedef struct {
+ int16_t year; ///< Year: any positive or negative value
+ uint8_t month; ///< Month: [1, 12]
+ uint8_t day; ///< Day: [1, 31]
+ uint8_t is_utc; ///< True if this is UTC (not local) time
+ uint8_t hour; ///< Hour: [0, 23]
+ uint8_t minute; ///< Minute: [0, 59]
+ uint8_t second; ///< Second: [0, 59]
+ uint32_t nanosecond; ///< Nanosecond: [0, 999999999]
+} ExessDateTime;
+
+/**
+ Add a duration to a datetime.
+
+ This advances or rewinds the datetime by the given duration, depending on
+ whether the duration is positive or negative.
+
+ If underflow or overflow occur, then this will return an infinite value. A
+ positive infinity has all fields at maximum, and a negative infinity has all
+ fields at minimum, except `is_utc` which is preserved from the input (so
+ infinities are comparable with the values they came from). Since 0 and 255
+ are never valid months, these can be tested for by checking if the year and
+ month are `INT16_MIN` and 0, or `INT16_MAX` and `INT8_MAX`.
+
+ @return `s + d`, or an infinite past or infinite future if underflow or
+ overflow occurs.
+*/
+EXESS_CONST_API
+ExessDateTime
+exess_add_datetime_duration(ExessDateTime s, ExessDuration d);
+
+/**
+ Read an xsd:dateTime string after any leading whitespace.
+
+ @param out Set to the parsed value, or zero on error.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_datetime(ExessDateTime* EXESS_NONNULL out,
+ const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical xsd:datetime string.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return #EXESS_SUCCESS on success, #EXESS_NO_SPACE if the buffer is too
+ small, or #EXESS_BAD_VALUE if the value is invalid.
+*/
+EXESS_API
+ExessResult
+exess_write_datetime(ExessDateTime value,
+ size_t buf_size,
+ char* EXESS_NULLABLE buf);
+
+/**
+ @}
+ @defgroup exess_timezone Timezones
+
+ Date and time values can have a timezone qualifier suffix. Note that
+ timezone is not a datatype, one only exists as a part of another value.
+
+ Canonical form starts with a sign, followed by two-digit hour and minute
+ offsets separated by a colon, like "-06:00" and "+02:30". The zero offset,
+ UTC, is written "Z".
+
+ Non-canonical form also allows writing UTC as "-00:00" or "+00:00".
+
+ This implementation only supports a resolution of 15 minutes, that is, only
+ offsets at 0, 15, 30, and 45 minutes within an hour.
+
+ @{
+*/
+
+/// A time zone offset for a date or time value
+typedef struct {
+ int8_t quarter_hours; ///< Offset in quarter hours: [-56, 56]
+} ExessTimezone;
+
+/// Sentinel value for local time, `INT8_MAX`
+#define EXESS_LOCAL INT8_MAX
+
+/**
+ @}
+ @defgroup exess_date Date
+ An xsd:date is a year, month, and day, with optional timezone.
+ @{
+*/
+
+/// The maximum length of an xsd:date string from exess_write_date(), 18
+#define EXESS_MAX_DATE_LENGTH 18
+
+/// Date (xsd:date)
+typedef struct {
+ int16_t year;
+ uint8_t month;
+ uint8_t day;
+ ExessTimezone zone;
+} ExessDate;
+
+/**
+ Read an xsd:date string after any leading whitespace.
+
+ @param out Set to the parsed value, or zero on error.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_date(ExessDate* EXESS_NONNULL out, const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical xsd:date string.
+
+ The output is always in canonical form, like `2001-04-12` or
+ `-2001-10-26+02:00`.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return #EXESS_SUCCESS on success, #EXESS_NO_SPACE if the buffer is too
+ small, or #EXESS_BAD_VALUE if the value is invalid.
+*/
+EXESS_API
+ExessResult
+exess_write_date(ExessDate value, size_t buf_size, char* EXESS_NULLABLE buf);
+
+/**
+ @}
+ @defgroup exess_time Time
+
+ An xsd:time is a time of day, with optional timezone.
+
+ @{
+*/
+
+/// The maximum length of an xsd:time string from exess_write_time(), 24
+#define EXESS_MAX_TIME_LENGTH 24
+
+/// Time (xsd:time)
+typedef struct {
+ ExessTimezone zone; ///< Time zone
+ uint8_t hour; ///< Hour: [0, 23]
+ uint8_t minute; ///< Minute: [0, 59]
+ uint8_t second; ///< Second: [0, 59]
+ uint32_t nanosecond; ///< Nanosecond: [0, 999999999]
+} ExessTime;
+
+/**
+ Read an xsd:time string after any leading whitespace.
+
+ @param out Set to the parsed value, or zero on error.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_time(ExessTime* EXESS_NONNULL out, const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical xsd:time string.
+
+ The output is always in canonical form, like "12:15" or "02:00Z".
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return #EXESS_SUCCESS on success, #EXESS_NO_SPACE if the buffer is too
+ small, or #EXESS_BAD_VALUE if the value is invalid.
+*/
+EXESS_API
+ExessResult
+exess_write_time(ExessTime value, size_t buf_size, char* EXESS_NULLABLE buf);
+
+/**
+ @}
+ @}
+ @defgroup exess_binary Binary
+ Datatypes for arbitrary binary data.
+ @{
+*/
+
+typedef struct {
+ size_t size;
+ void* EXESS_NULLABLE data;
+} ExessBlob;
+
+/**
+ @defgroup exess_base64 Base64
+ An xsd:base64Binary is arbitrary binary data in base64 encoding.
+ @{
+*/
+
+/**
+ Return the maximum number of bytes required to decode `length` bytes of
+ base64.
+
+ The returned value is an upper bound which is only exact for canonical
+ strings.
+
+ @param length The number of input (text) bytes to decode.
+ @return The size of a decoded value in bytes.
+*/
+EXESS_CONST_API
+size_t
+exess_base64_decoded_size(size_t length);
+
+/**
+ Read a binary value from a base64 string.
+
+ Canonical syntax is a multiple of 4 base64 characters, with either 1 or 2
+ trailing "=" characters as necessary, like "Zm9vYg==", with no whitespace.
+ All whitespace is skipped when reading.
+
+ The caller must allocate a large enough buffer to read the value, otherwise
+ an #EXESS_NO_SPACE error will be returned. The required space can be
+ calculated with exess_base64_decoded_size().
+
+ When this is called, the output blob must have the size of the available
+ buffer in bytes, and a pointer to the buffer. On return, the size will be
+ set to the exact size of the decoded data, which may be smaller than the
+ initial available size. Only these first bytes are written, the rest of the
+ buffer is not modified.
+
+ @param out The blob to set to the decoded binary data.
+ @param str String to parse.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_base64(ExessBlob* EXESS_NONNULL out, const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical xsd:base64Binary string.
+
+ The data is always written in canonical form, as a multiple of 4 characters
+ with no whitespace and 1 or 2 trailing "=" characters as padding if
+ necessary.
+
+ @param data_size The size of `data` in bytes.
+ @param data Data to write to a string.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return The `count` of characters in the output, and `status`
+ #EXESS_SUCCESS, or #EXESS_NO_SPACE if the buffer is too small.
+*/
+EXESS_API
+ExessResult
+exess_write_base64(size_t data_size,
+ const void* EXESS_NONNULL data,
+ size_t buf_size,
+ char* EXESS_NULLABLE buf);
+
+/**
+ @}
+ @defgroup exess_hex Hex
+ An xsd:hexBinary is arbitrary binary data in hexadecimal encoding.
+ @{
+*/
+
+/**
+ Return the maximum number of bytes required to decode `length` bytes of hex.
+
+ The returned value is an upper bound which is only exact for canonical
+ strings.
+
+ @param length The number of input (text) bytes to decode.
+ @return The size of a decoded value in bytes.
+*/
+EXESS_CONST_API
+size_t
+exess_hex_decoded_size(size_t length);
+
+/**
+ Read a binary value from a hex string.
+
+ Canonical syntax is an even number of uppercase hexadecimal digits with no
+ whitespace, like "666F6F". Lowercase hexadecimal is also supported, and all
+ whitespace is skipped when reading.
+
+ The caller must allocate a large enough buffer to read the value, otherwise
+ an #EXESS_NO_SPACE error will be returned. The required space can be
+ calculated with exess_hex_decoded_size().
+
+ When this is called, the output blob must have the size of the available
+ buffer in bytes, and a pointer to the buffer. On return, the size will be
+ set to the exact size of the decoded data, which may be smaller than the
+ initial available size. Only these first bytes are written, the rest of the
+ buffer is not modified.
+
+ @param out The blob to set to the decoded binary data.
+ @param str String to parse.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_hex(ExessBlob* EXESS_NONNULL out, const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical xsd:hexBinary string.
+
+ The data is always written in canonical form, as an even number of uppercase
+ hexadecimal digits with no whitespace.
+
+ @param data_size The size of `data` in bytes.
+ @param data Data to write to a string.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return The `count` of characters in the output, and `status`
+ #EXESS_SUCCESS, or #EXESS_NO_SPACE if the buffer is too small.
+*/
+EXESS_API
+ExessResult
+exess_write_hex(size_t data_size,
+ const void* EXESS_NONNULL data,
+ size_t buf_size,
+ char* EXESS_NULLABLE buf);
+
+/**
+ @}
+ @}
+ @defgroup exess_datatypes Datatypes
+ Runtime integer tags for supported datatypes with conversion to/from URIs.
+ @{
+*/
+
+typedef enum {
+ EXESS_NOTHING, ///< Sentinel for unknown datatypes or errors
+ EXESS_BOOLEAN, ///< xsd:boolean (see @ref exess_boolean)
+ EXESS_DECIMAL, ///< xsd:decimal (see @ref exess_decimal)
+ EXESS_DOUBLE, ///< xsd:double (see @ref exess_double)
+ EXESS_FLOAT, ///< xsd:float (see @ref exess_float)
+ EXESS_INTEGER, ///< xsd:integer (see @ref exess_long)
+ EXESS_NON_POSITIVE_INTEGER, ///< xsd:nonPositiveInteger (see @ref exess_long)
+ EXESS_NEGATIVE_INTEGER, ///< xsd:negativeInteger (see @ref exess_long)
+ EXESS_LONG, ///< xsd:long (see @ref exess_long)
+ EXESS_INT, ///< xsd:integer (see @ref exess_int)
+ EXESS_SHORT, ///< xsd:short (see @ref exess_short)
+ EXESS_BYTE, ///< xsd:byte (see @ref exess_byte)
+ EXESS_NON_NEGATIVE_INTEGER, ///< xsd:nonNegativeInteger (see @ref exess_ulong)
+ EXESS_ULONG, ///< xsd:unsignedLong (see @ref exess_ulong)
+ EXESS_UINT, ///< xsd:unsignedInt (see @ref exess_uint)
+ EXESS_USHORT, ///< xsd:unsignedShort (see @ref exess_ushort)
+ EXESS_UBYTE, ///< xsd:unsignedByte (see @ref exess_ubyte)
+ EXESS_POSITIVE_INTEGER, ///< xsd:positiveInteger (see @ref exess_ulong)
+ EXESS_DURATION, ///< xsd:duration (see @ref exess_duration)
+ EXESS_DATETIME, ///< xsd:dateTime (see @ref exess_datetime)
+ EXESS_TIME, ///< xsd:time (see @ref exess_time)
+ EXESS_DATE, ///< xsd:date (see @ref exess_date)
+ EXESS_HEX, ///< xsd:hexBinary (see @ref exess_hex)
+ EXESS_BASE64, ///< xsd:base64Binary (see @ref exess_base64)
+} ExessDatatype;
+
+/**
+ Return the URI for a supported datatype.
+
+ This only returns URIs that start with
+ `http://www.w3.org/2001/XMLSchema#`.
+
+ @param datatype Datatype tag.
+ @return The URI of the datatype, or null for #EXESS_NOTHING.
+*/
+EXESS_CONST_API
+const char* EXESS_NULLABLE
+exess_datatype_uri(ExessDatatype datatype);
+
+/**
+ Return the datatype tag for a datatype URI.
+
+ @return A datatype tag, or #EXESS_NOTHING if the URI is not a supported
+ datatype.
+*/
+EXESS_PURE_API
+ExessDatatype
+exess_datatype_from_uri(const char* EXESS_NONNULL uri);
+
+/**
+ Return whether a datatype has an upper bound on value sizes.
+
+ This returns true for all datatypes except #EXESS_DECIMAL, #EXESS_INTEGER
+ and its half-bounded subtypes #EXESS_NON_POSITIVE_INTEGER,
+ #EXESS_NEGATIVE_INTEGER, #EXESS_NON_NEGATIVE_INTEGER, and
+ #EXESS_POSITIVE_INTEGER, and the binary types #EXESS_HEX and #EXESS_BASE64.
+
+ For bounded datatypes, the maximum length of the string representation is
+ available in `exess_max_lengths` array, or as static constants like
+ #EXESS_MAX_INT_LENGTH.
+
+ @return True if values of the given datatype have a maximum size.
+*/
+EXESS_CONST_API
+bool
+exess_datatype_is_bounded(ExessDatatype datatype);
+
+/**
+ @}
+ @defgroup exess_variant Variant
+ An ExessVariant is a tagged union that can hold any supported datatype.
+ @{
+*/
+
+typedef union {
+ ExessStatus as_status;
+ ExessBlob as_blob;
+ bool as_bool;
+ double as_double;
+ float as_float;
+ int64_t as_long;
+ int32_t as_int;
+ int16_t as_short;
+ int8_t as_byte;
+ uint64_t as_ulong;
+ uint32_t as_uint;
+ uint16_t as_ushort;
+ uint8_t as_ubyte;
+ ExessDuration as_duration;
+ ExessDateTime as_datetime;
+ ExessTime as_time;
+ ExessDate as_date;
+} ExessValue;
+
+/**
+ Any supported value.
+
+ A variant is either nothing, or a value of a specific supported type. The
+ nothing variant has datatype #EXESS_NOTHING.
+
+ The value fields (everything other than datatype) are stored in an anonymous
+ union, only the field corresponding to the datatype is active. This should
+ not be used for type punning, use exess_coerce() for that instead.
+*/
+typedef struct {
+ ExessDatatype datatype;
+ ExessValue value;
+} ExessVariant;
+
+/**
+ @defgroup exess_variant_constructors Constructors
+ @{
+*/
+
+/// Return a nothing (null) variant, with a status code to signal errors
+EXESS_CONST_API
+ExessVariant
+exess_make_nothing(ExessStatus status);
+
+/// Return a boolean variant with the given value
+EXESS_CONST_API
+ExessVariant
+exess_make_boolean(bool value);
+
+/// Return a decimal variant with the given value
+EXESS_CONST_API
+ExessVariant
+exess_make_decimal(double value);
+
+/// Return a double variant with the given value
+EXESS_CONST_API
+ExessVariant
+exess_make_double(double value);
+
+/// Return a float variant with the given value
+EXESS_CONST_API
+ExessVariant
+exess_make_float(float value);
+
+/// Return a long variant with the given value
+EXESS_CONST_API
+ExessVariant
+exess_make_long(int64_t value);
+
+/// Return an int variant with the given value
+EXESS_CONST_API
+ExessVariant
+exess_make_int(int32_t value);
+
+/// Return a short variant with the given value
+EXESS_CONST_API
+ExessVariant
+exess_make_short(int16_t value);
+
+/// Return a byte variant with the given value
+EXESS_CONST_API
+ExessVariant
+exess_make_byte(int8_t value);
+
+/// Return a ulong variant with the given value
+EXESS_CONST_API
+ExessVariant
+exess_make_ulong(uint64_t value);
+
+/// Return a uint variant with the given value
+EXESS_CONST_API
+ExessVariant
+exess_make_uint(uint32_t value);
+
+/// Return a ushort variant with the given value
+EXESS_CONST_API
+ExessVariant
+exess_make_ushort(uint16_t value);
+
+/// Return a ubyte variant with the given value
+EXESS_CONST_API
+ExessVariant
+exess_make_ubyte(uint8_t value);
+
+/// Return a duration variant with the given value
+EXESS_CONST_API
+ExessVariant
+exess_make_duration(ExessDuration value);
+
+/// Return a datetime variant with the given value
+EXESS_CONST_API
+ExessVariant
+exess_make_datetime(ExessDateTime value);
+
+/// Return a time variant with the given value
+EXESS_CONST_API
+ExessVariant
+exess_make_time(ExessTime value);
+
+/// Return a date variant with the given value
+EXESS_CONST_API
+ExessVariant
+exess_make_date(ExessDate value);
+
+/// Return a hex binary variant with the given value
+EXESS_CONST_API
+ExessVariant
+exess_make_hex(ExessBlob value);
+
+/// Return a base64 binary variant with the given value
+EXESS_CONST_API
+ExessVariant
+exess_make_base64(ExessBlob value);
+
+/**
+ @}
+ @defgroup exess_variant_accessors Accessors
+ @{
+*/
+
+/**
+ Return the status of a variant.
+
+ This returns #EXESS_SUCCESS for any valid value, or the stored status for a
+ #EXESS_NOTHING variant.
+*/
+EXESS_PURE_API
+ExessStatus
+exess_get_status(const ExessVariant* EXESS_NONNULL variant);
+
+/// Return a pointer to the value if `variant` is a boolean, otherwise null
+EXESS_PURE_API
+const bool* EXESS_NULLABLE
+exess_get_boolean(const ExessVariant* EXESS_NONNULL variant);
+
+/**
+ Return a pointer to the value if `variant` is a double, otherwise null.
+
+ This will also access the value for #EXESS_DECIMAL.
+*/
+EXESS_PURE_API
+const double* EXESS_NULLABLE
+exess_get_double(const ExessVariant* EXESS_NONNULL variant);
+
+/// Return a pointer to the value if `variant` is a float, otherwise null
+EXESS_PURE_API
+const float* EXESS_NULLABLE
+exess_get_float(const ExessVariant* EXESS_NONNULL variant);
+
+/**
+ Return a pointer to the value if `variant` is a long, otherwise null.
+
+ This will also access the value for #EXESS_INTEGER,
+ #EXESS_NON_POSITIVE_INTEGER, and #EXESS_NEGATIVE_INTEGER.
+*/
+EXESS_PURE_API
+const int64_t* EXESS_NULLABLE
+exess_get_long(const ExessVariant* EXESS_NONNULL variant);
+
+/// Return a pointer to the value if `variant` is an int, otherwise null
+EXESS_PURE_API
+const int32_t* EXESS_NULLABLE
+exess_get_int(const ExessVariant* EXESS_NONNULL variant);
+
+/// Return a pointer to the value if `variant` is a short, otherwise null
+EXESS_PURE_API
+const int16_t* EXESS_NULLABLE
+exess_get_short(const ExessVariant* EXESS_NONNULL variant);
+
+/// Return a pointer to the value if `variant` is a byte, otherwise null
+EXESS_PURE_API
+const int8_t* EXESS_NULLABLE
+exess_get_byte(const ExessVariant* EXESS_NONNULL variant);
+
+/**
+ Return a pointer to the value if `variant` is a ulong, otherwise null.
+
+ This will also access the value for #EXESS_NON_NEGATIVE_INTEGER and
+ #EXESS_POSITIVE_INTEGER.
+*/
+EXESS_PURE_API
+const uint64_t* EXESS_NULLABLE
+exess_get_ulong(const ExessVariant* EXESS_NONNULL variant);
+
+/// Return a pointer to the value if `variant` is a uint, otherwise null
+EXESS_PURE_API
+const uint32_t* EXESS_NULLABLE
+exess_get_uint(const ExessVariant* EXESS_NONNULL variant);
+
+/// Return a pointer to the value if `variant` is a ushort, otherwise null
+EXESS_PURE_API
+const uint16_t* EXESS_NULLABLE
+exess_get_ushort(const ExessVariant* EXESS_NONNULL variant);
+
+/// Return a pointer to the value if `variant` is a ubyte, otherwise null
+EXESS_PURE_API
+const uint8_t* EXESS_NULLABLE
+exess_get_ubyte(const ExessVariant* EXESS_NONNULL variant);
+
+/// Return a pointer to the value if `variant` is a duration, otherwise null
+EXESS_PURE_API
+const ExessDuration* EXESS_NULLABLE
+exess_get_duration(const ExessVariant* EXESS_NONNULL variant);
+
+/// Return a pointer to the value if `variant` is a datetime, otherwise null
+EXESS_PURE_API
+const ExessDateTime* EXESS_NULLABLE
+exess_get_datetime(const ExessVariant* EXESS_NONNULL variant);
+
+/// Return a pointer to the value if `variant` is a time, otherwise null
+EXESS_PURE_API
+const ExessTime* EXESS_NULLABLE
+exess_get_time(const ExessVariant* EXESS_NONNULL variant);
+
+/// Return a pointer to the value if `variant` is a date, otherwise null
+EXESS_PURE_API
+const ExessDate* EXESS_NULLABLE
+exess_get_date(const ExessVariant* EXESS_NONNULL variant);
+
+/// Return a pointer to the value if `variant` is a date, otherwise null
+EXESS_PURE_API
+const ExessBlob* EXESS_NULLABLE
+exess_get_blob(const ExessVariant* EXESS_NONNULL variant);
+
+/**
+ @}
+*/
+
+//
+
+/**
+ Read any supported datatype from a string.
+
+ For reading binary blobs from base64 or hex, the `as_blob` field of `out`
+ must have the size of the available buffer in bytes, and a pointer to the
+ buffer. On return, the size will be set to the exact size of the decoded
+ data, which may be smaller than the initial available size. Only these
+ first bytes are written, the rest of the buffer is not modified.
+
+ @param out Set to the parsed value, or nothing on error.
+ @param datatype The datatype to read the string as.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_variant(ExessVariant* EXESS_NONNULL out,
+ ExessDatatype datatype,
+ const char* EXESS_NONNULL str);
+
+/**
+ Write any supported xsd datatype to a canonical string.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return The `count` of characters in the output, and `status`
+ #EXESS_SUCCESS, or #EXESS_NO_SPACE if the buffer is too small.
+*/
+EXESS_API
+ExessResult
+exess_write_variant(ExessVariant value,
+ size_t buf_size,
+ char* EXESS_NULLABLE buf);
+
+/**
+ Rewrite a supported xsd datatype in canonical form.
+
+ @param value Input value string.
+ @param datatype Datatype of value.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+
+ @return The `count` of characters in the output, and a `status` code. The
+ status may be an error from reading or writing, but the `count` always
+ refers to the number of characters written.
+*/
+EXESS_API
+ExessResult
+exess_write_canonical(const char* EXESS_NONNULL value,
+ ExessDatatype datatype,
+ size_t buf_size,
+ char* EXESS_NULLABLE buf);
+
+/**
+ @defgroup exess_coercion Datatype coercion
+
+ Values can be converted between some datatypes using exess_coerce(). This is
+ particularly useful for reducing the number of datatypes that the application
+ needs to explicitly handle.
+
+ @{
+*/
+
+/**
+ Coercion flags.
+
+ These values are ORed together to enable different kinds of lossy conversion
+ in exess_coerce().
+*/
+typedef enum {
+ /**
+ Only do lossless datatype coercions.
+
+ A lossless coercion is when the value has been perfectly preserved in the
+ target datatype, and coercing it back will result in the same value.
+
+ For some datatype combinations this will always be the case, for example
+ from short to long. For others it will depend on the value, for example
+ only the numbers 0 and 1 coerce to boolean without loss.
+ */
+ EXESS_LOSSLESS = 0u,
+
+ /**
+ Allow datatype coercions that reduce the precision of values.
+
+ This allows coercions that are lossy only in terms of precision, so the
+ resulting value is approximately equal to the original value.
+ Specifically, this allows coercing double to float.
+ */
+ EXESS_REDUCE_PRECISION = 1u << 0u,
+
+ /**
+ Allow datatype coercions that round to the nearest integer.
+
+ This allows coercing floating point numbers to integers by rounding to the
+ nearest integer, with halfway cases rounding towards even (the default
+ IEEE-754 rounding order).
+ */
+ EXESS_ROUND = 1u << 1u,
+
+ /**
+ Allow datatype coercions that truncate significant parts of values.
+
+ This allows coercions that lose data beyond simple precision loss.
+ Specifically, this allows coercing any number to boolean, datetime to
+ date, and datetime to time.
+ */
+ EXESS_TRUNCATE = 1u << 2u,
+} ExessCoercionFlag;
+
+/// Bitwise OR of #ExessCoercionFlag values
+typedef uint32_t ExessCoercionFlags;
+
+/**
+ Coerce a value to another datatype if possible.
+
+ @param value Value to coerce.
+
+ @param datatype Datatype to convert to.
+
+ @param coercions Enabled coercion flags. If this is #EXESS_LOSSLESS (zero),
+ then #EXESS_SUCCESS is only returned if the resulting value can be coerced
+ back to the original type without any loss of data. Otherwise, the lossy
+ coercions enabled by the set bits will be attempted.
+
+ @return #EXESS_SUCCESS on successful conversion, #EXESS_OUT_OF_RANGE if the
+ value is outside the range of the target type,
+ #EXESS_WOULD_REDUCE_PRECISION, #EXESS_WOULD_ROUND, or #EXESS_WOULD_TRUNCATE
+ if the required coercion is not enabled, or #EXESS_UNSUPPORTED if conversion
+ between the types is not supported at all.
+*/
+EXESS_API
+ExessVariant
+exess_coerce(ExessVariant value,
+ ExessDatatype datatype,
+ ExessCoercionFlags coercions);
+
+/**
+ @}
+ @}
+*/
+
+/**
+ The maximum length of the string representation of datatypes.
+
+ For datatypes that do not have such a bound, the value is zero.
+*/
+static const size_t exess_max_lengths[] = {
+ 0, // Unknown
+ EXESS_MAX_BOOLEAN_LENGTH,
+ 0, // decimal
+ EXESS_MAX_DOUBLE_LENGTH,
+ EXESS_MAX_FLOAT_LENGTH,
+ 0, // integer
+ 0, // nonPositiveInteger
+ 0, // negativeInteger
+ EXESS_MAX_LONG_LENGTH,
+ EXESS_MAX_INT_LENGTH,
+ EXESS_MAX_SHORT_LENGTH,
+ EXESS_MAX_BYTE_LENGTH,
+ 0, // nonNegativeInteger
+ EXESS_MAX_ULONG_LENGTH,
+ EXESS_MAX_UINT_LENGTH,
+ EXESS_MAX_USHORT_LENGTH,
+ EXESS_MAX_UBYTE_LENGTH,
+ 0, // positiveInteger
+ EXESS_MAX_DURATION_LENGTH,
+ EXESS_MAX_DATETIME_LENGTH,
+ EXESS_MAX_TIME_LENGTH,
+ EXESS_MAX_DATE_LENGTH,
+ 0, // hexBinary
+ 0, // base64Binary
+};
+
+/**
+ @}
+*/
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // EXESS_EXESS_H
diff --git a/subprojects/exess/meson.build b/subprojects/exess/meson.build
new file mode 100644
index 00000000..9e2e96b0
--- /dev/null
+++ b/subprojects/exess/meson.build
@@ -0,0 +1,236 @@
+project('exess', ['c'],
+ version: '0.0.1',
+ license: 'ISC',
+ meson_version: '>= 0.49.0',
+ default_options: [
+ 'b_ndebug=if-release',
+ 'buildtype=release',
+ 'c_std=c99',
+ 'cpp_std=c++11',
+ 'default_library=shared',
+ 'warning_level=3',
+ ])
+
+exess_src_root = meson.current_source_dir()
+major_version = meson.project_version().split('.')[0]
+version_suffix = '-@0@'.format(major_version)
+versioned_name = 'exess' + version_suffix
+
+# Load build tools
+pkg = import('pkgconfig')
+cc = meson.get_compiler('c')
+add_languages(['cpp'])
+cpp = meson.get_compiler('cpp')
+
+# Set ultra strict warnings for developers, if requested
+c_warnings = []
+c_suppressions = []
+if get_option('strict')
+ if meson.is_subproject()
+ c_warnings = []
+ else
+ subdir('meson')
+ add_project_arguments(all_c_warnings, language: ['c'])
+ endif
+
+ if cc.get_id() == 'clang'
+ c_suppressions = [
+ '-Wno-bad-function-cast',
+ '-Wno-nullability-extension',
+ '-Wno-padded',
+ ]
+
+ elif cc.get_id() == 'gcc'
+ c_suppressions = [
+ '-Wno-aggregate-return',
+ '-Wno-inline',
+ '-Wno-padded',
+ '-Wno-strict-overflow',
+ '-Wno-switch-default',
+ '-Wno-unsuffixed-float-constants',
+ '-Wno-unused-const-variable',
+ ]
+
+ elif cc.get_id() == 'msvc'
+ c_suppressions = [
+ '/wd4028', # formal parameter different from declaration
+ '/wd4204', # nonstandard extension: non-constant aggregate initializer
+ '/wd4706', # assignment within conditional expression
+ '/wd4710', # function not inlined
+ '/wd4711', # function selected for automatic inline expansion
+ '/wd4820', # padding added after data member
+ '/wd5045', # will insert Spectre mitigation
+ ]
+ endif
+else
+ if cc.get_id() == 'clang'
+ c_suppressions = [
+ '-Wno-nullability-extension',
+ ]
+ endif
+endif
+
+exess_c_args = cc.get_supported_arguments(c_suppressions)
+
+message(exess_c_args)
+
+if cc.get_id() == 'msvc'
+ # Suppress warnings in system headers
+ add_project_arguments(['/experimental:external',
+ '/external:W0',
+ '/external:anglebrackets'],
+ language: ['c'])
+endif
+
+# Detect compiler features
+
+feature_checks = {
+ 'builtin_clz': 'return __builtin_clz(1);',
+ 'builtin_clzll': 'return __builtin_clzll(1);',
+}
+
+checks = get_option('checks')
+foreach name, fragment : feature_checks
+ opt = get_option('use_@0@'.format(name))
+ code = 'int main(void) { @0@ }'.format(fragment)
+ define_name = 'HAVE_@0@'.format(name.to_upper())
+
+ if opt.enabled()
+ add_project_arguments(['-D@0@=1'.format(define_name)], language: ['c'])
+ elif opt.disabled()
+ add_project_arguments(['-D@0@=0'.format(define_name)], language: ['c'])
+ elif checks
+ if cc.links(code, name: name)
+ add_project_arguments(['-D@0@=1'.format(define_name)], language: ['c'])
+ else
+ add_project_arguments(['-D@0@=0'.format(define_name)], language: ['c'])
+ endif
+ endif
+endforeach
+
+# System libraries
+m_dep = cc.find_library('m', required: false)
+
+# Determine library type and the flags needed to build it
+if get_option('default_library') == 'both'
+ if host_machine.system() == 'windows'
+ error('default_library=both is not supported on Windows')
+ endif
+
+ library_type = 'both_libraries'
+ library_args = ['-DEXESS_INTERNAL']
+ prog_args = []
+elif get_option('default_library') == 'shared'
+ library_type = 'shared_library'
+ library_args = ['-DEXESS_INTERNAL']
+ prog_args = []
+else
+ library_type = 'static_library'
+ library_args = ['-DEXESS_INTERNAL', '-DEXESS_STATIC']
+ prog_args = ['-DEXESS_STATIC']
+endif
+
+c_headers = ['include/exess/exess.h']
+c_header_files = files(c_headers)
+
+sources = [
+ 'src/base64.c',
+ 'src/bigint.c',
+ 'src/boolean.c',
+ 'src/byte.c',
+ 'src/canonical.c',
+ 'src/coerce.c',
+ 'src/datatype.c',
+ 'src/date.c',
+ 'src/datetime.c',
+ 'src/decimal.c',
+ 'src/digits.c',
+ 'src/double.c',
+ 'src/duration.c',
+ 'src/float.c',
+ 'src/hex.c',
+ 'src/int.c',
+ 'src/int_math.c',
+ 'src/long.c',
+ 'src/read_utils.c',
+ 'src/scientific.c',
+ 'src/short.c',
+ 'src/soft_float.c',
+ 'src/strerror.c',
+ 'src/strtod.c',
+ 'src/time.c',
+ 'src/timezone.c',
+ 'src/ubyte.c',
+ 'src/uint.c',
+ 'src/ulong.c',
+ 'src/ushort.c',
+ 'src/variant.c',
+ 'src/write_utils.c',
+ 'src/year.c',
+]
+
+# Build shared and/or static library/libraries
+libexess = build_target(
+ versioned_name,
+ sources,
+ c_args: exess_c_args + library_args,
+ dependencies: [m_dep],
+ gnu_symbol_visibility: 'hidden',
+ include_directories: include_directories(['include']),
+ install: true,
+ target_type: library_type,
+ version: meson.project_version())
+
+# Generage pkg-config file
+pkg.generate(
+ libexess,
+ name: 'Exess',
+ filebase: versioned_name,
+ subdirs: [versioned_name],
+ version: meson.project_version(),
+ description: 'A simple and efficient regular expression implementation')
+
+# Install header to a versioned include directory
+install_headers(c_headers, subdir: versioned_name / 'exess')
+
+exess_dep = declare_dependency(
+ link_with: libexess,
+ include_directories: include_directories(['include']))
+
+subdir('bindings/cpp')
+
+if not get_option('docs').disabled()
+ subdir('doc')
+endif
+
+if get_option('tests')
+ if library_type == 'both_libraries'
+ libexess_static = libexess.get_static_lib()
+ elif library_type == 'shared_library'
+ libexess_static = static_library(
+ versioned_name,
+ sources,
+ include_directories: include_directories(['include', 'src']),
+ c_args: exess_c_args + library_args,
+ dependencies: [m_dep],
+ gnu_symbol_visibility: 'default')
+ else
+ libexess_static = libexess
+ endif
+
+ exess_static_dep = declare_dependency(
+ include_directories: include_directories(['include']),
+ dependencies: [m_dep],
+ link_with: libexess_static)
+
+ subdir('test')
+endif
+
+if meson.version().version_compare('>=0.53.0')
+ summary('Tests', get_option('tests'), bool_yn: true)
+
+ summary('Install prefix', get_option('prefix'))
+ summary('Headers', get_option('prefix') / get_option('includedir'))
+ summary('Libraries', get_option('prefix') / get_option('libdir'))
+endif
+
diff --git a/subprojects/exess/meson/meson.build b/subprojects/exess/meson/meson.build
new file mode 100644
index 00000000..5dd0de38
--- /dev/null
+++ b/subprojects/exess/meson/meson.build
@@ -0,0 +1,204 @@
+# General code to enable approximately all warnings.
+#
+# This is trivial for clang and MSVC, but GCC does not have such an option, and
+# has several esoteric warnings, so we need to enable everything we want
+# explicitly. We enable everything that does not require a value argument,
+# except for warnings that are only relevant for very old languages (earlier
+# than C99 or C++11) or non-standard extensions.
+#
+# Omitted common warnings:
+#
+# Wabi=
+# Waggregate-return
+# Walloc-size-larger-than=BYTES
+# Walloca-larger-than=BYTES
+# Wframe-larger-than=BYTES
+# Wlarger-than=BYTES
+# Wstack-usage=BYTES
+# Wsystem-headers
+# Wtraditional
+# Wtraditional-conversion
+# Wtrampolines
+# Wvla-larger-than=BYTES
+#
+# Omitted C warnings:
+#
+# Wc90-c99-compat
+# Wdeclaration-after-statement
+# Wtraditional
+# Wtraditional-conversion
+#
+# Omitted C++ warnings:
+#
+# Wnamespaces
+# Wtemplates
+
+gcc_common_warnings = [
+ '-Walloc-zero',
+ '-Walloca',
+ '-Wanalyzer-too-complex',
+ '-Warith-conversion',
+ '-Warray-bounds=2',
+ '-Wattribute-alias=2',
+ '-Wcast-align=strict',
+ '-Wcast-qual',
+ '-Wconversion',
+ '-Wdate-time',
+ '-Wdisabled-optimization',
+ '-Wdouble-promotion',
+ '-Wduplicated-branches',
+ '-Wduplicated-cond',
+ '-Wfloat-equal',
+ '-Wformat-overflow=2',
+ '-Wformat-signedness',
+ '-Wformat-truncation=2',
+ '-Wformat=2',
+ '-Wimplicit-fallthrough=2',
+ '-Winit-self',
+ '-Winline',
+ '-Winvalid-pch',
+ '-Wlogical-op',
+ '-Wmissing-declarations',
+ '-Wmissing-include-dirs',
+ '-Wmultichar',
+ '-Wnormalized=nfc',
+ '-Wnull-dereference',
+ '-Wpacked',
+ '-Wpadded',
+ '-Wredundant-decls',
+ '-Wscalar-storage-order',
+ '-Wshadow',
+ '-Wshift-overflow=2',
+ '-Wsizeof-array-argument',
+ '-Wstack-protector',
+ '-Wstrict-aliasing=3',
+ '-Wstrict-overflow=5',
+ '-Wstringop-overflow=3',
+ '-Wsuggest-attribute=cold',
+ '-Wsuggest-attribute=const',
+ '-Wsuggest-attribute=format',
+ '-Wsuggest-attribute=malloc',
+ '-Wsuggest-attribute=noreturn',
+ '-Wsuggest-attribute=pure',
+ '-Wswitch-default',
+ '-Wswitch-enum',
+ '-Wsync-nand',
+ '-Wundef',
+ '-Wunused-const-variable=2',
+ '-Wunused-macros',
+ '-Wvarargs',
+ '-Wvector-operation-performance',
+ '-Wvla',
+ '-Wwrite-strings',
+]
+
+gcc_c_warnings = [
+ '-Wbad-function-cast',
+ '-Wc++-compat',
+ '-Wc99-c11-compat',
+ '-Wdesignated-init',
+ '-Wdiscarded-array-qualifiers',
+ '-Wdiscarded-qualifiers',
+ '-Wincompatible-pointer-types',
+ '-Wjump-misses-init',
+ '-Wmissing-prototypes',
+ '-Wnested-externs',
+ '-Wold-style-definition',
+ '-Wstrict-prototypes',
+ '-Wunsuffixed-float-constants',
+]
+
+# Set all_c_warnings for the current C compiler
+if is_variable('cc') and not is_variable('all_c_warnings')
+ if cc.get_id() == 'clang'
+ all_c_warnings = ['-Weverything']
+ elif cc.get_id() == 'gcc'
+ all_c_warnings = gcc_common_warnings + [
+ '-Wbad-function-cast',
+ '-Wc++-compat',
+ '-Wc99-c11-compat',
+ '-Wdesignated-init',
+ '-Wdiscarded-array-qualifiers',
+ '-Wdiscarded-qualifiers',
+ '-Wincompatible-pointer-types',
+ '-Wjump-misses-init',
+ '-Wmissing-prototypes',
+ '-Wnested-externs',
+ '-Wold-style-definition',
+ '-Wstrict-prototypes',
+ '-Wunsuffixed-float-constants',
+ ]
+ elif cc.get_id() == 'msvc'
+ all_c_warnings = ['/Wall']
+ else
+ all_c_warnings = []
+ endif
+
+ all_c_warnings = cc.get_supported_arguments(all_c_warnings)
+
+endif
+
+# Set all_cpp_warnings for the current C++ compiler
+if is_variable('cpp') and not is_variable('all_cpp_warnings')
+ if cpp.get_id() == 'clang'
+ all_cpp_warnings = [
+ '-Weverything',
+ '-Wno-c++98-compat',
+ '-Wno-c++98-compat-pedantic'
+ ]
+ elif cpp.get_id() == 'gcc'
+ all_cpp_warnings = gcc_common_warnings + [
+ '-Wabi-tag',
+ '-Waligned-new=all',
+ '-Wcatch-value=3',
+ '-Wcomma-subscript',
+ '-Wconditionally-supported',
+ '-Wctor-dtor-privacy',
+ '-Wdeprecated-copy-dtor',
+ '-Weffc++',
+ '-Wextra-semi',
+ '-Wmismatched-tags',
+ '-Wmultiple-inheritance',
+ '-Wnoexcept',
+ '-Wnoexcept-type',
+ '-Wnon-virtual-dtor',
+ '-Wold-style-cast',
+ '-Woverloaded-virtual',
+ '-Wplacement-new=2',
+ '-Wredundant-tags',
+ '-Wregister',
+ '-Wsign-promo',
+ '-Wstrict-null-sentinel',
+ '-Wsuggest-final-methods',
+ '-Wsuggest-final-types',
+ '-Wsuggest-override',
+ '-Wuseless-cast',
+ '-Wvirtual-inheritance',
+ '-Wvolatile',
+ '-Wzero-as-null-pointer-constant',
+ ]
+ elif cpp.get_id() == 'msvc'
+ all_cpp_warnings = ['/Wall']
+ else
+ all_cpp_warnings = []
+ endif
+
+ all_cpp_warnings = cpp.get_supported_arguments(all_cpp_warnings)
+
+endif
+
+# Set all_objc_warnings for the current Objective C compiler
+if is_variable('objcc') and not is_variable('all_objc_warnings')
+ all_objc_warnings = []
+ if objcc.get_id() == 'clang'
+ all_objc_warnings = ['-Weverything']
+ elif objc.get_id() == 'gcc'
+ all_objc_warnings = gcc_common_warnings + [
+ '-Wno-direct-ivar-access',
+ ]
+ else
+ all_objc_warnings = []
+ endif
+
+ all_objc_warnings = objcc.get_supported_arguments(all_objc_warnings)
+endif
diff --git a/subprojects/exess/meson_options.txt b/subprojects/exess/meson_options.txt
new file mode 100644
index 00000000..2970a1c4
--- /dev/null
+++ b/subprojects/exess/meson_options.txt
@@ -0,0 +1,17 @@
+option('checks', type: 'boolean', value: true, yield: true,
+ description: 'Check for features with the build system')
+
+option('docs', type: 'feature', value: 'auto', yield: true,
+ description: 'Build documentation')
+
+option('strict', type: 'boolean', value: false, yield: true,
+ description: 'Enable ultra-strict warnings')
+
+option('tests', type: 'boolean', value: true, yield: true,
+ description: 'Build tests')
+
+option('use_builtin_clz', type: 'feature', value: 'auto', yield: true,
+ description: 'Use __builtin_clz')
+
+option('use_builtin_clzll', type: 'feature', value: 'auto', yield: true,
+ description: 'Use __builtin_clzll')
diff --git a/subprojects/exess/scripts/dox_to_sphinx.py b/subprojects/exess/scripts/dox_to_sphinx.py
new file mode 100755
index 00000000..557d0474
--- /dev/null
+++ b/subprojects/exess/scripts/dox_to_sphinx.py
@@ -0,0 +1,675 @@
+#!/usr/bin/env python3
+
+# Copyright 2020 David Robillard <d@drobilla.net>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""
+Write Sphinx markup from Doxygen XML.
+
+Takes a path to a directory of XML generated by Doxygen, and emits a directory
+with a reStructuredText file for every documented symbol.
+"""
+
+import argparse
+import os
+import sys
+import textwrap
+import xml.etree.ElementTree
+
+__author__ = "David Robillard"
+__date__ = "2020-11-18"
+__email__ = "d@drobilla.net"
+__license__ = "ISC"
+__version__ = __date__.replace("-", ".")
+
+
+def load_index(index_path):
+ """
+ Load the index from XML.
+
+ :returns: A dictionary from ID to skeleton records with basic information
+ for every documented entity. Some records have an ``xml_filename`` key
+ with the filename of a definition file. These files will be loaded later
+ to flesh out the records in the index.
+ """
+
+ root = xml.etree.ElementTree.parse(index_path).getroot()
+ index = {}
+
+ for compound in root:
+ compound_id = compound.get("refid")
+ compound_kind = compound.get("kind")
+ compound_name = compound.find("name").text
+ if compound_kind in ["dir", "file", "page"]:
+ continue
+
+ # Add record for compound (compounds appear only once in the index)
+ assert compound_id not in index
+ index[compound_id] = {
+ "kind": compound_kind,
+ "name": compound_name,
+ "xml_filename": compound_id + ".xml",
+ "children": [],
+ }
+
+ name_prefix = (
+ ("%s::" % compound_name) if compound_kind == "namespace" else ""
+ )
+
+ for child in compound.findall("member"):
+ if child.get("refid") in index:
+ assert compound_kind == "group"
+ continue
+
+ # Everything has a kind and a name
+ child_record = {
+ "kind": child.get("kind"),
+ "name": name_prefix + child.find("name").text,
+ }
+
+ if child.get("kind") == "enum":
+ # Enums are not compounds, but we want to resolve the parent of
+ # their values so they are not written as top level documents
+ child_record["children"] = []
+
+ if child.get("kind") == "enumvalue":
+ # Remove namespace prefix
+ child_record["name"] = child.find("name").text
+
+ if child.get("kind") == "variable":
+ if child_record["name"][0] == "@":
+ # Remove placeholder name from anonymous struct or union
+ child_record["name"] = ""
+ else:
+ # Remove namespace prefix
+ child_record["name"] = child.find("name").text
+
+ index[child.get("refid")] = child_record
+
+ return index
+
+
+def resolve_index(index, root):
+ """
+ Walk a definition document and extend the index for linking.
+
+ This does two things: sets the "parent" and "children" fields of all
+ applicable records, and sets the "strong" field of enums so that the
+ correct Sphinx role can be used when referring to them.
+ """
+
+ def add_child(index, parent_id, child_id):
+ parent = index[parent_id]
+ child = index[child_id]
+
+ if child["kind"] == "enumvalue":
+ assert parent["kind"] == "enum"
+ assert "parent" not in child or child["parent"] == parent_id
+ child["parent"] = parent_id
+
+ else:
+ if parent["kind"] in ["class", "struct", "union"]:
+ assert "parent" not in child or child["parent"] == parent_id
+ child["parent"] = parent_id
+
+ if child_id not in parent["children"]:
+ parent["children"] += [child_id]
+
+ compound = root.find("compounddef")
+ compound_kind = compound.get("kind")
+
+ if compound_kind == "group":
+ for subgroup in compound.findall("innergroup"):
+ add_child(index, compound.get("id"), subgroup.get("refid"))
+
+ for klass in compound.findall("innerclass"):
+ add_child(index, compound.get("id"), klass.get("refid"))
+
+ for section in compound.findall("sectiondef"):
+ if section.get("kind").startswith("private"):
+ for member in section.findall("memberdef"):
+ if member.get("id") in index:
+ del index[member.get("id")]
+ else:
+ for member in section.findall("memberdef"):
+ member_id = member.get("id")
+ add_child(index, compound.get("id"), member_id)
+
+ if member.get("kind") == "enum":
+ index[member_id]["strong"] = member.get("strong") == "yes"
+ for value in member.findall("enumvalue"):
+ add_child(index, member_id, value.get("id"))
+
+
+def sphinx_role(record, lang):
+ """
+ Return the Sphinx role used for a record.
+
+ This is used for the description directive like ".. c:function::", and
+ links like ":c:func:`foo`.
+ """
+
+ kind = record["kind"]
+
+ if kind in ["class", "function", "namespace", "struct", "union"]:
+ return lang + ":" + kind
+
+ if kind == "define":
+ return "c:macro"
+
+ if kind == "group":
+ return "ref"
+
+ if kind == "enum":
+ return lang + (":enum-class" if record["strong"] else ":enum")
+
+ if kind == "typedef":
+ return lang + ":type"
+
+ if kind == "enumvalue":
+ return lang + ":enumerator"
+
+ if kind == "variable":
+ return lang + (":member" if "parent" in record else ":var")
+
+ raise RuntimeError("No known role for kind '%s'" % kind)
+
+
+def child_identifier(lang, parent_name, child_name):
+ """
+ Return the identifier for an enum value or struct member.
+
+ Sphinx, for some reason, uses a different syntax for this in C and C++.
+ """
+
+ separator = "::" if lang == "cpp" else "."
+
+ return "%s%s%s" % (parent_name, separator, child_name)
+
+
+def link_markup(index, lang, refid):
+ """Return a Sphinx link for a Doxygen reference."""
+
+ record = index[refid]
+ kind, name = record["kind"], record["name"]
+ role = sphinx_role(record, lang)
+
+ if kind in ["class", "define", "enum", "struct", "typedef", "union"]:
+ return ":%s:`%s`" % (role, name)
+
+ if kind == "group":
+ return ":ref:`%s`" % name
+
+ if kind == "function":
+ return ":%s:func:`%s`" % (lang, name)
+
+ if kind == "enumvalue":
+ parent_name = index[record["parent"]]["name"]
+ return ":%s:`%s`" % (role, child_identifier(lang, parent_name, name))
+
+ if kind == "variable":
+ if "parent" not in record:
+ return ":%s:var:`%s`" % (lang, name)
+
+ parent_name = index[record["parent"]]["name"]
+ return ":%s:`%s`" % (role, child_identifier(lang, parent_name, name))
+
+ raise RuntimeError("Unknown link target kind: %s" % kind)
+
+
+def indent(markup, depth):
+ """
+ Indent markup to a depth level.
+
+ Like textwrap.indent() but takes an integer and works in reST indentation
+ levels for clarity."
+ """
+
+ return textwrap.indent(markup, " " * depth)
+
+
+def heading(text, level):
+ """
+ Return a ReST heading at a given level.
+
+ Follows the style in the Python documentation guide, see
+ <https://devguide.python.org/documenting/#sections>.
+ """
+
+ assert 1 <= level <= 6
+
+ chars = ("#", "*", "=", "-", "^", '"')
+ line = chars[level] * len(text)
+
+ return "%s%s\n%s\n\n" % (line + "\n" if level < 3 else "", text, line)
+
+
+def dox_to_rst(index, lang, node):
+ """
+ Convert documentation commands (docCmdGroup) to Sphinx markup.
+
+ This is used to convert the content of descriptions in the documentation.
+ It recursively parses all children tags and raises a RuntimeError if any
+ unknown tag is encountered.
+ """
+
+ def field_value(markup):
+ """Return a value for a field as a single line or indented block."""
+ if "\n" in markup.strip():
+ return "\n" + indent(markup, 1)
+
+ return " " + markup.strip()
+
+ if node.tag == "lsquo":
+ return "‘"
+
+ if node.tag == "rsquo":
+ return "’"
+
+ if node.tag == "computeroutput":
+ return "``%s``" % plain_text(node)
+
+ if node.tag == "itemizedlist":
+ markup = ""
+ for item in node.findall("listitem"):
+ assert len(item) == 1
+ markup += "\n- %s" % dox_to_rst(index, lang, item[0])
+
+ return markup
+
+ if node.tag == "para":
+ markup = node.text if node.text is not None else ""
+ for child in node:
+ markup += dox_to_rst(index, lang, child)
+ markup += child.tail if child.tail is not None else ""
+
+ return markup.strip() + "\n\n"
+
+ if node.tag == "parameterlist":
+ markup = ""
+ for item in node.findall("parameteritem"):
+ name = item.find("parameternamelist/parametername")
+ description = item.find("parameterdescription")
+ assert len(description) == 1
+ markup += "\n\n:param %s:%s" % (
+ name.text,
+ field_value(dox_to_rst(index, lang, description[0])),
+ )
+
+ return markup + "\n"
+
+ if node.tag == "programlisting":
+ return "\n.. code-block:: %s\n\n%s" % (
+ lang,
+ indent(plain_text(node), 1),
+ )
+
+ if node.tag == "ref":
+ refid = node.get("refid")
+ if refid not in index:
+ sys.stderr.write("warning: Unresolved link: %s\n" % refid)
+ return node.text
+
+ assert len(node) == 0
+ assert len(link_markup(index, lang, refid)) > 0
+ return link_markup(index, lang, refid)
+
+ if node.tag == "simplesect":
+ assert len(node) == 1
+
+ if node.get("kind") == "return":
+ return "\n:returns:" + field_value(
+ dox_to_rst(index, lang, node[0])
+ )
+
+ if node.get("kind") == "see":
+ return dox_to_rst(index, lang, node[0])
+
+ raise RuntimeError("Unknown simplesect kind: %s" % node.get("kind"))
+
+ if node.tag == "ulink":
+ return "`%s <%s>`_" % (node.text, node.get("url"))
+
+ raise RuntimeError("Unknown documentation command: %s" % node.tag)
+
+
+def description_markup(index, lang, node):
+ """Return the markup for a brief or detailed description."""
+
+ assert node.tag == "briefdescription" or node.tag == "detaileddescription"
+ assert not (node.tag == "briefdescription" and len(node) > 1)
+ assert len(node.text.strip()) == 0
+
+ return "".join([dox_to_rst(index, lang, child) for child in node]).strip()
+
+
+def set_descriptions(index, lang, definition, record):
+ """Set a record's brief/detailed descriptions from the XML definition."""
+
+ for tag in ["briefdescription", "detaileddescription"]:
+ node = definition.find(tag)
+ if node is not None:
+ record[tag] = description_markup(index, lang, node)
+
+
+def set_template_params(node, record):
+ """Set a record's template_params from the XML definition."""
+
+ template_param_list = node.find("templateparamlist")
+ if template_param_list is not None:
+ params = []
+ for param in template_param_list.findall("param"):
+ if param.find("declname") is not None:
+ # Value parameter
+ type_text = plain_text(param.find("type"))
+ name_text = plain_text(param.find("declname"))
+
+ params += ["%s %s" % (type_text, name_text)]
+ else:
+ # Type parameter
+ params += ["%s" % (plain_text(param.find("type")))]
+
+ record["template_params"] = "%s" % ", ".join(params)
+
+
+def plain_text(node):
+ """
+ Return the plain text of a node with all tags ignored.
+
+ This is needed where Doxygen may include refs but Sphinx needs plain text
+ because it parses things itself to generate links.
+ """
+
+ if node.tag == "sp":
+ markup = " "
+ elif node.text is not None:
+ markup = node.text
+ else:
+ markup = ""
+
+ for child in node:
+ markup += plain_text(child)
+ markup += child.tail if child.tail is not None else ""
+
+ return markup
+
+
+def local_name(name):
+ """Return a name with all namespace prefixes stripped."""
+
+ return name[name.rindex("::") + 2 :] if "::" in name else name
+
+
+def read_definition_doc(index, lang, root):
+ """Walk a definition document and update described records in the index."""
+
+ # Set descriptions for the compound itself
+ compound = root.find("compounddef")
+ compound_record = index[compound.get("id")]
+ set_descriptions(index, lang, compound, compound_record)
+ set_template_params(compound, compound_record)
+
+ if compound.find("title") is not None:
+ compound_record["title"] = compound.find("title").text.strip()
+
+ # Set documentation for all children
+ for section in compound.findall("sectiondef"):
+ if section.get("kind").startswith("private"):
+ continue
+
+ for member in section.findall("memberdef"):
+ kind = member.get("kind")
+ record = index[member.get("id")]
+ set_descriptions(index, lang, member, record)
+ set_template_params(member, record)
+
+ if compound.get("kind") in ["class", "struct", "union"]:
+ assert kind in ["function", "typedef", "variable"]
+ record["type"] = plain_text(member.find("type"))
+
+ if kind == "enum":
+ for value in member.findall("enumvalue"):
+ set_descriptions(
+ index, lang, value, index[value.get("id")]
+ )
+
+ elif kind == "function":
+ record["prototype"] = "%s %s%s" % (
+ plain_text(member.find("type")),
+ member.find("name").text,
+ member.find("argsstring").text,
+ )
+
+ elif kind == "typedef":
+ name = local_name(record["name"])
+ args_text = member.find("argsstring").text
+ target_text = plain_text(member.find("type"))
+ if args_text is not None: # Function pointer
+ assert target_text[-2:] == "(*" and args_text[0] == ")"
+ record["type"] = target_text + args_text
+ record["definition"] = target_text + name + args_text
+ else: # Normal named typedef
+ assert target_text is not None
+ record["type"] = target_text
+ if member.find("definition").text.startswith("using"):
+ record["definition"] = "%s = %s" % (
+ name,
+ target_text,
+ )
+ else:
+ record["definition"] = "%s %s" % (
+ target_text,
+ name,
+ )
+
+ elif kind == "variable":
+ record["type"] = plain_text(member.find("type"))
+ record["name"] = plain_text(member.find("name"))
+ record["definition"] = plain_text(member.find("definition"))
+
+ elif kind == "define":
+ record["initializer"] = member.find("initializer").text
+
+
+def declaration_string(record):
+ """
+ Return the string that describes a declaration.
+
+ This is what follows the directive, and is in C/C++ syntax, except without
+ keywords like "typedef" and "using" as expected by Sphinx. For example,
+ "struct ThingImpl Thing" or "void run(int value)".
+ """
+
+ kind = record["kind"]
+ result = ""
+
+ if "template_params" in record:
+ result = "template <%s> " % record["template_params"]
+
+ if kind == "function":
+ result += record["prototype"]
+ elif kind == "typedef":
+ result += record["definition"]
+ elif kind == "variable":
+ if "type" in record and "name" in record:
+ result += "%s %s" % (record["type"], local_name(record["name"]))
+ else:
+ result += record["definition"]
+ elif "type" in record:
+ result += "%s %s" % (record["type"], local_name(record["name"]))
+ else:
+ result += local_name(record["name"])
+
+ return result
+
+
+def document_markup(index, lang, record):
+ """Return the complete document that describes some documented entity."""
+
+ kind = record["kind"]
+ role = sphinx_role(record, lang)
+ name = record["name"]
+ markup = ""
+
+ if name != local_name(name):
+ markup += ".. cpp:namespace:: %s\n\n" % name[0 : name.rindex("::")]
+
+ # Write top-level directive
+ markup += ".. %s:: %s\n" % (role, declaration_string(record))
+
+ # Write main description blurb
+ markup += "\n" + indent(record["briefdescription"] + "\n", 1)
+ if len(record["detaileddescription"]) > 0:
+ markup += "\n" + indent(record["detaileddescription"], 1) + "\n"
+
+ assert (
+ kind in ["class", "enum", "namespace", "struct", "union"]
+ or "children" not in record
+ )
+
+ # Sphinx C++ namespaces work by setting a scope, they have no content
+ child_indent = 0 if kind == "namespace" else 1
+
+ # Write inline children if applicable
+ markup += "\n" if "children" in record else ""
+ for child_id in record.get("children", []):
+ child_record = index[child_id]
+ child_role = sphinx_role(child_record, lang)
+
+ if not child_record["name"]:
+ continue # Skip anonymous union member
+
+ child_header = ".. %s:: %s\n\n" % (
+ child_role,
+ declaration_string(child_record),
+ )
+
+ markup += "\n"
+ markup += indent(child_header, child_indent)
+ markup += indent(child_record["briefdescription"], child_indent + 1)
+ markup += indent(child_record["detaileddescription"], child_indent + 1)
+
+ return markup
+
+
+def symbol_filename(name):
+ """Adapt the name of a symbol to be suitable for use as a filename."""
+
+ return name.replace("::", "__")
+
+
+def emit_groups(index, lang, output_dir, force):
+ """Write a description file for every group documented in the index."""
+
+ for record in index.values():
+ if record["kind"] != "group":
+ continue
+
+ name = record["name"]
+ filename = os.path.join(output_dir, "%s.rst" % name)
+ if not force and os.path.exists(filename):
+ raise FileExistsError("File already exists: '%s'" % filename)
+
+ with open(filename, "w") as rst:
+ rst.write(".. _%s:\n\n" % name)
+ rst.write(heading(record["title"], 1))
+
+ # Get all child group and symbol names
+ child_groups = {}
+ child_symbols = {}
+ for child_id in record["children"]:
+ child = index[child_id]
+ assert child["name"][0] != "@"
+ if child["kind"] == "group":
+ child_groups[child["name"]] = child
+ else:
+ child_symbols[child["name"]] = child
+
+ # Emit description (document body)
+ if len(record["briefdescription"]) > 0:
+ rst.write(record["briefdescription"] + "\n\n")
+ if len(record["detaileddescription"]) > 0:
+ rst.write(record["detaileddescription"] + "\n\n")
+
+ if len(child_groups) > 0:
+ # Emit TOC for child groups
+ rst.write(".. toctree::\n\n")
+ for name, group in child_groups.items():
+ rst.write(indent(group["name"], 1) + "\n")
+
+ # Emit symbols in sorted order
+ for name, symbol in child_symbols.items():
+ rst.write("\n")
+ rst.write(document_markup(index, lang, symbol))
+ rst.write("\n")
+
+
+def run(index_xml_path, output_dir, language, force):
+ """Write a directory of Sphinx files from a Doxygen XML directory."""
+
+ # Build skeleton index from index.xml
+ xml_dir = os.path.dirname(index_xml_path)
+ index = load_index(index_xml_path)
+
+ # Load all definition documents
+ definition_docs = []
+ for record in index.values():
+ if "xml_filename" in record:
+ xml_path = os.path.join(xml_dir, record["xml_filename"])
+ definition_docs += [xml.etree.ElementTree.parse(xml_path)]
+
+ # Do an initial pass of the definition documents to resolve the index
+ for root in definition_docs:
+ resolve_index(index, root)
+
+ # Finally read the documentation from definition documents
+ for root in definition_docs:
+ read_definition_doc(index, language, root)
+
+ # Create output directory
+ try:
+ os.makedirs(output_dir)
+ except OSError:
+ pass
+
+ # Emit output files
+ emit_groups(index, language, output_dir, force)
+
+
+if __name__ == "__main__":
+ ap = argparse.ArgumentParser(
+ usage="%(prog)s [OPTION]... XML_DIR OUTPUT_DIR",
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+
+ ap.add_argument(
+ "-f",
+ "--force",
+ action="store_true",
+ help="overwrite files",
+ )
+
+ ap.add_argument(
+ "-l",
+ "--language",
+ default="c",
+ choices=["c", "cpp"],
+ help="language domain for output",
+ )
+
+ ap.add_argument("index_xml_path", help="path index.xml from Doxygen")
+ ap.add_argument("output_dir", help="output directory")
+
+ run(**vars(ap.parse_args(sys.argv[1:])))
diff --git a/subprojects/exess/src/.clang-tidy b/subprojects/exess/src/.clang-tidy
new file mode 100644
index 00000000..8023398e
--- /dev/null
+++ b/subprojects/exess/src/.clang-tidy
@@ -0,0 +1,12 @@
+Checks: >
+ *,
+ -*-magic-numbers,
+ -*-uppercase-literal-suffix,
+ -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
+ -hicpp-multiway-paths-covered,
+ -llvm-header-guard,
+ -llvmlibc-*,
+ -misc-no-recursion,
+WarningsAsErrors: '*'
+HeaderFilterRegex: '.*'
+FormatStyle: file
diff --git a/subprojects/exess/src/attributes.h b/subprojects/exess/src/attributes.h
new file mode 100644
index 00000000..1575113b
--- /dev/null
+++ b/subprojects/exess/src/attributes.h
@@ -0,0 +1,30 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_ATTRIBUTES_H
+#define EXESS_ATTRIBUTES_H
+
+#ifdef __GNUC__
+# define EXESS_I_PURE_FUNC __attribute__((pure))
+# define EXESS_I_CONST_FUNC __attribute__((const))
+# define EXESS_I_MALLOC_FUNC __attribute__((malloc))
+#else
+# define EXESS_I_PURE_FUNC
+# define EXESS_I_CONST_FUNC
+# define EXESS_I_MALLOC_FUNC
+#endif
+
+#endif // EXESS_ATTRIBUTES_H
diff --git a/subprojects/exess/src/base64.c b/subprojects/exess/src/base64.c
new file mode 100644
index 00000000..de64ee68
--- /dev/null
+++ b/subprojects/exess/src/base64.c
@@ -0,0 +1,163 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "macros.h"
+#include "read_utils.h"
+#include "string_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdint.h>
+#include <string.h>
+
+// Map a 6-bit base64 group to a base64 digit
+static inline uint8_t
+map(const unsigned group)
+{
+ assert(group < 64);
+
+ // See <http://tools.ietf.org/html/rfc3548#section-3>.
+ static const uint8_t b64_map[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+ return b64_map[group];
+}
+
+// Unmap a base64 digit to the numeric value used for decoding
+static inline uint8_t
+unmap(const uint8_t in)
+{
+ /* Table indexed by encoded characters that contains the numeric value used
+ for decoding, shifted up by 47 to be in the range of printable ASCII. A
+ '$' is a placeholder for characters not in the base64 alphabet. */
+ static const uint8_t b64_unmap[] =
+ "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$m$$$ncdefghijkl$$$$$$"
+ "$/0123456789:;<=>?@ABCDEFGH$$$$$$IJKLMNOPQRSTUVWXYZ[\\]^_`ab$$$$"
+ "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"
+ "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$";
+
+ return (uint8_t)(b64_unmap[in] - 47u);
+}
+
+static char
+next_char(const char* const str, size_t* const i)
+{
+ *i += skip_whitespace(str + *i);
+
+ return str[*i];
+}
+
+size_t
+exess_base64_decoded_size(const size_t length)
+{
+ return (length * 3) / 4 + 2;
+}
+
+ExessResult
+exess_read_base64(ExessBlob* const out, const char* const str)
+{
+ uint8_t* const uout = (uint8_t*)out->data;
+ const uint8_t* const ustr = (const uint8_t*)str;
+ size_t i = 0u;
+ size_t o = 0u;
+
+ while (str[i]) {
+ // Skip leading whitespace
+ i += skip_whitespace(str + i);
+ if (!str[i]) {
+ break;
+ }
+
+ // Read next chunk of 4 input characters
+ uint8_t in[] = "====";
+ for (size_t j = 0; j < 4; ++j) {
+ const char c = next_char(str, &i);
+ if (!is_base64(c)) {
+ return result(EXESS_EXPECTED_BASE64, i);
+ }
+
+ in[j] = ustr[i++];
+ }
+
+ if (in[0] == '=' || in[1] == '=' || (in[2] == '=' && in[3] != '=')) {
+ return result(EXESS_BAD_VALUE, i);
+ }
+
+ const size_t n_bytes = 1u + (in[2] != '=') + (in[3] != '=');
+ if (o + n_bytes > out->size) {
+ return result(EXESS_NO_SPACE, i);
+ }
+
+ const uint8_t a1 = (uint8_t)(unmap(in[0]) << 2u);
+ const uint8_t a2 = unmap(in[1]) >> 4u;
+
+ uout[o++] = a1 | a2;
+
+ if (in[2] != '=') {
+ const uint8_t b1 = (uint8_t)(unmap(in[1]) << 4u) & 0xF0u;
+ const uint8_t b2 = unmap(in[2]) >> 2u;
+
+ uout[o++] = b1 | b2;
+ }
+
+ if (in[3] != '=') {
+ const uint8_t c1 = (uint8_t)(unmap(in[2]) << 6u) & 0xC0u;
+ const uint8_t c2 = unmap(in[3]);
+
+ uout[o++] = c1 | c2;
+ }
+ }
+
+ out->size = o;
+ return result(EXESS_SUCCESS, i);
+}
+
+ExessResult
+exess_write_base64(const size_t data_size,
+ const void* const data,
+ const size_t buf_size,
+ char* const buf)
+{
+ const size_t length = (data_size + 2) / 3 * 4;
+ if (!buf) {
+ return result(EXESS_SUCCESS, length);
+ }
+
+ if (buf_size < length + 1) {
+ return result(EXESS_NO_SPACE, 0);
+ }
+
+ uint8_t* const out = (uint8_t*)buf;
+
+ size_t o = 0;
+ for (size_t i = 0; i < data_size; i += 3, o += 4) {
+ uint8_t in[4] = {0, 0, 0, 0};
+ const size_t n_in = MIN(3, data_size - i);
+ memcpy(in, (const uint8_t*)data + i, n_in);
+
+ out[o] = map(in[0] >> 2u);
+ out[o + 1] = map(((in[0] & 0x03u) << 4u) | ((in[1] & 0xF0u) >> 4u));
+ out[o + 2] =
+ ((n_in > 1u) ? map(((in[1] & 0x0Fu) << 2u) | ((in[2] & 0xC0u) >> 6u))
+ : '=');
+
+ out[o + 3] = ((n_in > 2u) ? map(in[2] & 0x3Fu) : '=');
+ }
+
+ return end_write(EXESS_SUCCESS, buf_size, buf, o);
+}
diff --git a/subprojects/exess/src/bigint.c b/subprojects/exess/src/bigint.c
new file mode 100644
index 00000000..8c8a95c8
--- /dev/null
+++ b/subprojects/exess/src/bigint.c
@@ -0,0 +1,605 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "bigint.h"
+#include "macros.h"
+
+#include "int_math.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+
+typedef uint64_t Hugit;
+
+static const uint32_t bigit_mask = ~(uint32_t)0;
+static const uint64_t carry_mask = (uint64_t) ~(uint32_t)0 << 32u;
+
+typedef struct {
+ unsigned bigits;
+ unsigned bits;
+} Offset;
+
+static inline Offset
+make_offset(const unsigned i)
+{
+ const unsigned bigits = i / BIGINT_BIGIT_BITS;
+ const unsigned bits = i - bigits * BIGINT_BIGIT_BITS;
+
+ const Offset offset = {bigits, bits};
+ return offset;
+}
+
+#ifndef NDEBUG
+static inline bool
+exess_bigint_is_clamped(const ExessBigint* num)
+{
+ return num->n_bigits == 0 || num->bigits[num->n_bigits - 1];
+}
+#endif
+
+void
+exess_bigint_shift_left(ExessBigint* num, const unsigned amount)
+{
+ assert(exess_bigint_is_clamped(num));
+ if (amount == 0 || num->n_bigits == 0) {
+ return;
+ }
+
+ const Offset offset = make_offset(amount);
+
+ assert(num->n_bigits + offset.bigits < BIGINT_MAX_BIGITS);
+ num->n_bigits += offset.bigits + (bool)offset.bits;
+
+ if (offset.bits == 0) { // Simple bigit-aligned shift
+ for (unsigned i = num->n_bigits - 1; i >= offset.bigits; --i) {
+ num->bigits[i] = num->bigits[i - offset.bigits];
+ }
+ } else { // Bigit + sub-bigit bit offset shift
+ const unsigned right_shift = BIGINT_BIGIT_BITS - offset.bits;
+ for (unsigned i = num->n_bigits - offset.bigits - 1; i > 0; --i) {
+ num->bigits[i + offset.bigits] =
+ (num->bigits[i] << offset.bits) | (num->bigits[i - 1] >> right_shift);
+ }
+
+ num->bigits[offset.bigits] = num->bigits[0] << offset.bits;
+ }
+
+ // Zero LSBs
+ for (unsigned i = 0; i < offset.bigits; ++i) {
+ num->bigits[i] = 0;
+ }
+
+ exess_bigint_clamp(num);
+ assert(exess_bigint_is_clamped(num));
+}
+
+void
+exess_bigint_zero(ExessBigint* num)
+{
+ static const ExessBigint zero = {{0}, 0};
+
+ *num = zero;
+}
+
+void
+exess_bigint_set(ExessBigint* num, const ExessBigint* value)
+{
+ *num = *value;
+}
+
+void
+exess_bigint_set_u32(ExessBigint* num, const uint32_t value)
+{
+ exess_bigint_zero(num);
+
+ num->bigits[0] = value;
+ num->n_bigits = (bool)value;
+}
+
+void
+exess_bigint_clamp(ExessBigint* num)
+{
+ while (num->n_bigits > 0 && num->bigits[num->n_bigits - 1] == 0) {
+ --num->n_bigits;
+ }
+}
+
+void
+exess_bigint_set_u64(ExessBigint* num, const uint64_t value)
+{
+ exess_bigint_zero(num);
+
+ num->bigits[0] = (Bigit)(value & bigit_mask);
+ num->bigits[1] = (Bigit)(value >> BIGINT_BIGIT_BITS);
+ num->n_bigits = num->bigits[1] ? 2u : num->bigits[0] ? 1u : 0u;
+}
+
+void
+exess_bigint_set_pow10(ExessBigint* num, const unsigned exponent)
+{
+ exess_bigint_set_u32(num, 1);
+ exess_bigint_multiply_pow10(num, exponent);
+}
+
+static uint32_t
+read_u32(const char* const str, uint32_t* result, uint32_t* n_digits)
+{
+ static const size_t uint32_digits10 = 9;
+
+ *result = *n_digits = 0;
+
+ uint32_t i = 0;
+ for (; str[i] && *n_digits < uint32_digits10; ++i) {
+ if (str[i] >= '0' && str[i] <= '9') {
+ *result = *result * 10u + (unsigned)(str[i] - '0');
+ *n_digits += 1;
+ } else if (str[i] != '.') {
+ break;
+ }
+ }
+
+ return i;
+}
+
+void
+exess_bigint_set_decimal_string(ExessBigint* num, const char* const str)
+{
+ exess_bigint_zero(num);
+
+ uint32_t pos = 0;
+ uint32_t n_digits = 0;
+ uint32_t n_read = 0;
+ uint32_t word = 0;
+ while ((n_read = read_u32(str + pos, &word, &n_digits))) {
+ exess_bigint_multiply_u32(num, (uint32_t)POW10[n_digits]);
+ exess_bigint_add_u32(num, word);
+ pos += n_read;
+ }
+
+ exess_bigint_clamp(num);
+}
+
+void
+exess_bigint_set_hex_string(ExessBigint* num, const char* const str)
+{
+ exess_bigint_zero(num);
+
+ // Read digits from right to left until we run off the beginning
+ const int length = (int)strlen(str);
+ char digit_buf[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
+ int i = length - 8;
+ for (; i >= 0; i -= 8) {
+ memcpy(digit_buf, str + i, 8);
+ num->bigits[num->n_bigits++] = (Bigit)strtoll(digit_buf, NULL, 16);
+ }
+
+ // Read leftovers into MSB if necessary
+ if (i > -8) {
+ memset(digit_buf, 0, sizeof(digit_buf));
+ memcpy(digit_buf, str, 8u + (unsigned)i);
+ num->bigits[num->n_bigits++] = (Bigit)strtoll(digit_buf, NULL, 16);
+ }
+
+ exess_bigint_clamp(num);
+}
+
+void
+exess_bigint_multiply_u32(ExessBigint* num, const uint32_t factor)
+{
+ switch (factor) {
+ case 0:
+ exess_bigint_zero(num);
+ return;
+ case 1:
+ return;
+ default:
+ break;
+ }
+
+ Hugit carry = 0;
+ for (unsigned i = 0; i < num->n_bigits; ++i) {
+ const Hugit p = (Hugit)factor * num->bigits[i];
+ const Hugit hugit = p + (carry & bigit_mask);
+
+ num->bigits[i] = (Bigit)(hugit & bigit_mask);
+
+ carry = (hugit >> 32u) + (carry >> 32u);
+ }
+
+ for (; carry; carry >>= 32u) {
+ assert(num->n_bigits + 1 <= BIGINT_MAX_BIGITS);
+ num->bigits[num->n_bigits++] = (Bigit)carry;
+ }
+}
+
+void
+exess_bigint_multiply_u64(ExessBigint* num, const uint64_t factor)
+{
+ switch (factor) {
+ case 0:
+ exess_bigint_zero(num);
+ return;
+ case 1:
+ return;
+ default:
+ break;
+ }
+
+ const Hugit f_lo = factor & bigit_mask;
+ const Hugit f_hi = factor >> 32u;
+
+ Hugit carry = 0;
+ for (unsigned i = 0; i < num->n_bigits; ++i) {
+ const Hugit p_lo = f_lo * num->bigits[i];
+ const Hugit p_hi = f_hi * num->bigits[i];
+ const Hugit hugit = p_lo + (carry & bigit_mask);
+
+ num->bigits[i] = (Bigit)(hugit & bigit_mask);
+ carry = p_hi + (hugit >> 32u) + (carry >> 32u);
+ }
+
+ for (; carry; carry >>= 32u) {
+ assert(num->n_bigits + 1 <= BIGINT_MAX_BIGITS);
+ num->bigits[num->n_bigits++] = (Bigit)(carry & bigit_mask);
+ }
+}
+
+void
+exess_bigint_multiply_pow10(ExessBigint* num, const unsigned exponent)
+{
+ /* To reduce multiplication, we exploit 10^e = (2*5)^e = 2^e * 5^e to
+ factor out an exponentiation by 5 instead of 10. So, we first multiply
+ by 5^e (hard), then by 2^e (just a single left shift). */
+
+ // 5^27, the largest power of 5 that fits in 64 bits
+ static const uint64_t pow5_27 = 7450580596923828125ull;
+
+ // Powers of 5 up to 5^13, the largest that fits in 32 bits
+ static const uint32_t pow5[] = {
+ 1,
+ 5,
+ 5 * 5,
+ 5 * 5 * 5,
+ 5 * 5 * 5 * 5,
+ 5 * 5 * 5 * 5 * 5,
+ 5 * 5 * 5 * 5 * 5 * 5,
+ 5 * 5 * 5 * 5 * 5 * 5 * 5,
+ 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5,
+ 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5,
+ 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5,
+ 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5,
+ 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5,
+ 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5 * 5,
+ };
+
+ if (exponent == 0 || num->n_bigits == 0) {
+ return;
+ }
+
+ // Multiply by 5^27 until e < 27 so we can switch to 32 bits
+ unsigned e = exponent;
+ while (e >= 27) {
+ exess_bigint_multiply_u64(num, pow5_27);
+ e -= 27;
+ }
+
+ // Multiply by 5^13 until e < 13 so we have only one multiplication left
+ while (e >= 13) {
+ exess_bigint_multiply_u32(num, pow5[13]);
+ e -= 13;
+ }
+
+ // Multiply by the final 5^e (which may be zero, making this a noop)
+ exess_bigint_multiply_u32(num, pow5[e]);
+
+ // Finally multiply by 2^e
+ exess_bigint_shift_left(num, exponent);
+}
+
+int
+exess_bigint_compare(const ExessBigint* lhs, const ExessBigint* rhs)
+{
+ if (lhs->n_bigits < rhs->n_bigits) {
+ return -1;
+ }
+
+ if (lhs->n_bigits > rhs->n_bigits) {
+ return 1;
+ }
+
+ for (int i = (int)lhs->n_bigits - 1; i >= 0; --i) {
+ const Bigit bigit_l = lhs->bigits[i];
+ const Bigit bigit_r = rhs->bigits[i];
+ if (bigit_l < bigit_r) {
+ return -1;
+ }
+
+ if (bigit_l > bigit_r) {
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+int
+exess_bigint_plus_compare(const ExessBigint* l,
+ const ExessBigint* p,
+ const ExessBigint* c)
+{
+ assert(exess_bigint_is_clamped(l));
+ assert(exess_bigint_is_clamped(p));
+ assert(exess_bigint_is_clamped(c));
+
+ if (l->n_bigits < p->n_bigits) {
+ return exess_bigint_plus_compare(p, l, c);
+ }
+
+ if (l->n_bigits + 1 < c->n_bigits) {
+ return -1;
+ }
+
+ if (l->n_bigits > c->n_bigits) {
+ return 1;
+ }
+
+ if (p->n_bigits < l->n_bigits && l->n_bigits < c->n_bigits) {
+ return -1;
+ }
+
+ Hugit borrow = 0;
+ for (int i = (int)c->n_bigits - 1; i >= 0; --i) {
+ const Bigit ai = l->bigits[i];
+ const Bigit bi = p->bigits[i];
+ const Bigit ci = c->bigits[i];
+ const Hugit sum = (Hugit)ai + bi;
+
+ if (sum > ci + borrow) {
+ return 1;
+ }
+
+ if ((borrow += ci - sum) > 1) {
+ return -1;
+ }
+
+ borrow <<= 32u;
+ }
+
+ return borrow ? -1 : 0;
+}
+
+void
+exess_bigint_add_u32(ExessBigint* lhs, const uint32_t rhs)
+{
+ if (lhs->n_bigits == 0) {
+ exess_bigint_set_u32(lhs, rhs);
+ return;
+ }
+
+ Hugit sum = (Hugit)lhs->bigits[0] + rhs;
+ Bigit carry = (Bigit)(sum >> 32u);
+
+ lhs->bigits[0] = (Bigit)(sum & bigit_mask);
+
+ unsigned i = 1;
+ for (; carry; ++i) {
+ assert(carry == 0 || carry == 1);
+
+ sum = (Hugit)carry + lhs->bigits[i];
+ lhs->bigits[i] = (Bigit)(sum & bigit_mask);
+ carry = (Bigit)((sum & carry_mask) >> 32u);
+ }
+
+ lhs->n_bigits = MAX(i, lhs->n_bigits);
+ assert(exess_bigint_is_clamped(lhs));
+}
+
+void
+exess_bigint_add(ExessBigint* lhs, const ExessBigint* rhs)
+{
+ assert(MAX(lhs->n_bigits, rhs->n_bigits) + 1 <= BIGINT_MAX_BIGITS);
+
+ bool carry = 0;
+ unsigned i = 0;
+ for (; i < rhs->n_bigits; ++i) {
+ const Hugit sum = (Hugit)lhs->bigits[i] + rhs->bigits[i] + carry;
+
+ lhs->bigits[i] = (Bigit)(sum & bigit_mask);
+ carry = (sum & carry_mask) >> 32u;
+ }
+
+ for (; carry; ++i) {
+ const Hugit sum = (Hugit)lhs->bigits[i] + carry;
+
+ lhs->bigits[i] = (Bigit)(sum & bigit_mask);
+ carry = (sum & carry_mask) >> 32u;
+ }
+
+ lhs->n_bigits = MAX(i, lhs->n_bigits);
+ assert(exess_bigint_is_clamped(lhs));
+}
+
+void
+exess_bigint_subtract(ExessBigint* lhs, const ExessBigint* rhs)
+{
+ assert(exess_bigint_is_clamped(lhs));
+ assert(exess_bigint_is_clamped(rhs));
+ assert(exess_bigint_compare(lhs, rhs) >= 0);
+
+ bool borrow = 0;
+ unsigned i = 0;
+ for (i = 0; i < rhs->n_bigits; ++i) {
+ const Bigit l = lhs->bigits[i];
+ const Bigit r = rhs->bigits[i];
+
+ lhs->bigits[i] = l - r - borrow;
+ borrow = l < r || (l == r && borrow);
+ }
+
+ for (; borrow; ++i) {
+ const Bigit l = lhs->bigits[i];
+
+ lhs->bigits[i] -= borrow;
+
+ borrow = l == 0;
+ }
+
+ exess_bigint_clamp(lhs);
+}
+
+static unsigned
+exess_bigint_leading_zeros(const ExessBigint* num)
+{
+ return 32 * (BIGINT_MAX_BIGITS - num->n_bigits) +
+ exess_clz32(num->bigits[num->n_bigits - 1]);
+}
+
+// EXESS_I_PURE_FUNC
+static Bigit
+exess_bigint_left_shifted_bigit_i(const ExessBigint* num,
+ const Offset amount,
+ const unsigned index)
+{
+ /* assert(exess_bigint_is_clamped(num)); */
+ if (amount.bigits == 0 && amount.bits == 0) {
+ return num->bigits[index];
+ }
+
+ if (index < amount.bigits) {
+ return 0;
+ }
+
+ if (amount.bits == 0) { // Simple bigit-aligned shift
+ return num->bigits[index - amount.bigits];
+ }
+
+ if (index == amount.bigits) { // Last non-zero bigit
+ return num->bigits[0] << amount.bits;
+ }
+
+ // Bigit + sub-bigit bit offset shift
+ const unsigned right_shift = BIGINT_BIGIT_BITS - amount.bits;
+ return (num->bigits[index - amount.bigits] << amount.bits) |
+ (num->bigits[index - amount.bigits - 1] >> right_shift);
+}
+
+Bigit
+exess_bigint_left_shifted_bigit(const ExessBigint* num,
+ const unsigned amount,
+ const unsigned index)
+{
+ return exess_bigint_left_shifted_bigit_i(num, make_offset(amount), index);
+}
+
+void
+exess_bigint_subtract_left_shifted(ExessBigint* lhs,
+ const ExessBigint* rhs,
+ const unsigned amount)
+{
+ assert(exess_bigint_is_clamped(lhs));
+ assert(exess_bigint_is_clamped(rhs));
+#ifndef NDEBUG
+ {
+ ExessBigint check_rhs = *rhs;
+ exess_bigint_shift_left(&check_rhs, amount);
+ assert(exess_bigint_compare(lhs, &check_rhs) >= 0);
+ }
+#endif
+
+ const Offset offset = make_offset(amount);
+ const unsigned r_n_bigits = rhs->n_bigits + offset.bigits + (bool)offset.bits;
+
+ bool borrow = 0;
+ unsigned i = 0;
+ for (i = 0; i < r_n_bigits; ++i) {
+ const Bigit l = lhs->bigits[i];
+ const Bigit r = exess_bigint_left_shifted_bigit_i(rhs, offset, i);
+
+ lhs->bigits[i] = l - r - borrow;
+ borrow = l < r || ((l == r) && borrow);
+ }
+
+ for (; borrow; ++i) {
+ const Bigit l = lhs->bigits[i];
+
+ lhs->bigits[i] -= borrow;
+
+ borrow = l == 0;
+ }
+
+ exess_bigint_clamp(lhs);
+}
+
+uint32_t
+exess_bigint_divmod(ExessBigint* lhs, const ExessBigint* rhs)
+{
+ assert(exess_bigint_is_clamped(lhs));
+ assert(exess_bigint_is_clamped(rhs));
+ assert(rhs->n_bigits > 0);
+ if (lhs->n_bigits < rhs->n_bigits) {
+ return 0;
+ }
+
+ uint32_t result = 0;
+ const Bigit r0 = rhs->bigits[rhs->n_bigits - 1];
+ const unsigned rlz = exess_bigint_leading_zeros(rhs);
+
+ // Shift and subtract until the LHS does not have more bigits
+ int big_steps = 0;
+ while (lhs->n_bigits > rhs->n_bigits) {
+ const unsigned llz = exess_bigint_leading_zeros(lhs);
+ const unsigned shift = rlz - llz - 1;
+
+ result += 1u << shift;
+ exess_bigint_subtract_left_shifted(lhs, rhs, shift);
+ ++big_steps;
+ }
+
+ // Handle simple termination cases
+ int cmp = exess_bigint_compare(lhs, rhs);
+ if (cmp < 0) {
+ return result;
+ }
+
+ if (cmp > 0 && lhs->n_bigits == 1) {
+ assert(rhs->n_bigits == 1);
+ const Bigit l0 = lhs->bigits[lhs->n_bigits - 1];
+
+ lhs->bigits[lhs->n_bigits - 1] = l0 % r0;
+ lhs->n_bigits -= (lhs->bigits[lhs->n_bigits - 1] == 0);
+ return result + l0 / r0;
+ }
+
+ // Both now have the same number of digits, finish with subtraction
+ int final_steps = 0;
+ for (; cmp >= 0; cmp = exess_bigint_compare(lhs, rhs)) {
+ const unsigned llz = exess_bigint_leading_zeros(lhs);
+ if (rlz == llz) {
+ // Both have the same number of leading zeros, just subtract
+ exess_bigint_subtract(lhs, rhs);
+ return result + 1;
+ }
+
+ const unsigned shift = rlz - llz - 1;
+ result += 1u << shift;
+ exess_bigint_subtract_left_shifted(lhs, rhs, shift);
+ ++final_steps;
+ }
+
+ return result;
+}
diff --git a/subprojects/exess/src/bigint.h b/subprojects/exess/src/bigint.h
new file mode 100644
index 00000000..088052f6
--- /dev/null
+++ b/subprojects/exess/src/bigint.h
@@ -0,0 +1,118 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_BIGINT_H
+#define EXESS_BIGINT_H
+
+#include "attributes.h"
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+
+typedef uint32_t Bigit;
+
+/* We need enough precision for any double, the "largest" of which (using
+ absolute exponents) is the smallest subnormal ~= 5e-324. This is 1076 bits
+ long, but we need a bit more space for arithmetic. This is absurd, but such
+ is decimal. These are only used on the stack so it doesn't hurt too much.
+*/
+
+#define BIGINT_MAX_SIGNIFICANT_BITS 1280u
+#define BIGINT_BIGIT_BITS 32u
+#define BIGINT_MAX_BIGITS (BIGINT_MAX_SIGNIFICANT_BITS / BIGINT_BIGIT_BITS)
+
+typedef struct {
+ Bigit bigits[BIGINT_MAX_BIGITS];
+ unsigned n_bigits;
+} ExessBigint;
+
+void
+exess_bigint_zero(ExessBigint* num);
+
+size_t
+exess_bigint_print_hex(FILE* stream, const ExessBigint* num);
+
+void
+exess_bigint_clamp(ExessBigint* num);
+
+void
+exess_bigint_shift_left(ExessBigint* num, unsigned amount);
+
+void
+exess_bigint_set(ExessBigint* num, const ExessBigint* value);
+
+void
+exess_bigint_set_u32(ExessBigint* num, uint32_t value);
+
+void
+exess_bigint_set_u64(ExessBigint* num, uint64_t value);
+
+void
+exess_bigint_set_pow10(ExessBigint* num, unsigned exponent);
+
+void
+exess_bigint_set_decimal_string(ExessBigint* num, const char* str);
+
+void
+exess_bigint_set_hex_string(ExessBigint* num, const char* str);
+
+void
+exess_bigint_multiply_u32(ExessBigint* num, uint32_t factor);
+
+void
+exess_bigint_multiply_u64(ExessBigint* num, uint64_t factor);
+
+void
+exess_bigint_multiply_pow10(ExessBigint* num, unsigned exponent);
+
+EXESS_I_PURE_FUNC
+int
+exess_bigint_compare(const ExessBigint* lhs, const ExessBigint* rhs);
+
+void
+exess_bigint_add_u32(ExessBigint* lhs, uint32_t rhs);
+
+void
+exess_bigint_add(ExessBigint* lhs, const ExessBigint* rhs);
+
+void
+exess_bigint_subtract(ExessBigint* lhs, const ExessBigint* rhs);
+
+EXESS_I_PURE_FUNC
+Bigit
+exess_bigint_left_shifted_bigit(const ExessBigint* num,
+ unsigned amount,
+ unsigned index);
+
+/// Faster implementation of exess_bigint_subtract(lhs, rhs << amount)
+void
+exess_bigint_subtract_left_shifted(ExessBigint* lhs,
+ const ExessBigint* rhs,
+ unsigned amount);
+
+/// Faster implementation of exess_bigint_compare(l + p, c)
+EXESS_I_PURE_FUNC
+int
+exess_bigint_plus_compare(const ExessBigint* l,
+ const ExessBigint* p,
+ const ExessBigint* c);
+
+/// Divide and set `lhs` to modulo
+uint32_t
+exess_bigint_divmod(ExessBigint* lhs, const ExessBigint* rhs);
+
+#endif // EXESS_BIGINT_H
diff --git a/subprojects/exess/src/boolean.c b/subprojects/exess/src/boolean.c
new file mode 100644
index 00000000..32657da9
--- /dev/null
+++ b/subprojects/exess/src/boolean.c
@@ -0,0 +1,69 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdbool.h>
+#include <string.h>
+
+ExessResult
+exess_read_boolean(bool* const out, const char* const str)
+{
+ size_t i = skip_whitespace(str);
+ ExessResult r = {EXESS_EXPECTED_BOOLEAN, i};
+
+ *out = false;
+
+ switch (str[i]) {
+ case '0':
+ return end_read(EXESS_SUCCESS, str, ++i);
+
+ case '1':
+ *out = true;
+ return end_read(EXESS_SUCCESS, str, ++i);
+
+ case 't':
+ if (!strncmp(str + i, "true", 4)) {
+ *out = true;
+ return end_read(EXESS_SUCCESS, str, i + 4u);
+ }
+ break;
+
+ case 'f':
+ if (!strncmp(str + i, "false", 5)) {
+ return end_read(EXESS_SUCCESS, str, i + 5u);
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return end_read(r.status, str, r.count);
+}
+
+ExessResult
+exess_write_boolean(const bool value, const size_t buf_size, char* const buf)
+{
+ return end_write(EXESS_SUCCESS,
+ buf_size,
+ buf,
+ value ? write_string(4, "true", buf_size, buf, 0)
+ : write_string(5, "false", buf_size, buf, 0));
+}
diff --git a/subprojects/exess/src/byte.c b/subprojects/exess/src/byte.c
new file mode 100644
index 00000000..4754d82d
--- /dev/null
+++ b/subprojects/exess/src/byte.c
@@ -0,0 +1,45 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+exess_read_byte(int8_t* const out, const char* const str)
+{
+ int64_t long_out = 0;
+ const ExessResult r = exess_read_long(&long_out, str);
+ if (r.status) {
+ return r;
+ }
+
+ if (long_out < INT8_MIN || long_out > INT8_MAX) {
+ return result(EXESS_OUT_OF_RANGE, r.count);
+ }
+
+ *out = (int8_t)long_out;
+ return r;
+}
+
+ExessResult
+exess_write_byte(const int8_t value, const size_t buf_size, char* const buf)
+{
+ return exess_write_long(value, buf_size, buf);
+}
diff --git a/subprojects/exess/src/canonical.c b/subprojects/exess/src/canonical.c
new file mode 100644
index 00000000..f70aaecc
--- /dev/null
+++ b/subprojects/exess/src/canonical.c
@@ -0,0 +1,309 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+#include "string_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdbool.h>
+#include <stddef.h>
+
+typedef enum {
+ EXESS_NEGATIVE,
+ EXESS_ZERO,
+ EXESS_POSITIVE,
+} ExessIntegerKind;
+
+/// Return true iff `c` is "+" or "-"
+static inline bool
+is_sign(const int c)
+{
+ return c == '+' || c == '-';
+}
+
+/// Return true iff `c` is "0"
+static inline bool
+is_zero(const int c)
+{
+ return c == '0';
+}
+
+/// Return true iff `c` is "."
+static inline bool
+is_point(const int c)
+{
+ return c == '.';
+}
+
+// Scan forwards as long as `pred` returns true for characters
+static inline size_t
+scan(bool (*pred)(const int), const char* const str, size_t i)
+{
+ while (pred(str[i])) {
+ ++i;
+ }
+
+ return i;
+}
+
+// Skip the next character if `pred` returns true for it
+static inline size_t
+skip(bool (*pred)(const int), const char* const str, const size_t i)
+{
+ return i + (pred(str[i]) ? 1 : 0);
+}
+
+static ExessResult
+write_decimal(const char* const str, const size_t buf_size, char* const buf)
+{
+ size_t i = 0;
+
+ const size_t sign = scan(is_space, str, i); // Sign
+ const size_t leading = skip(is_sign, str, sign); // First digit
+ if (str[leading] != '.' && !is_digit(str[leading])) {
+ return result(EXESS_EXPECTED_DIGIT, sign);
+ }
+
+ const size_t first = scan(is_zero, str, leading); // First non-zero
+ const size_t point = scan(is_digit, str, first); // Decimal point
+ size_t last = scan(is_digit, str, skip(is_point, str, point)); // Last digit
+ const size_t end = scan(is_space, str, last); // Last non-space
+
+ const ExessStatus st = is_end(str[end]) ? EXESS_SUCCESS : EXESS_EXPECTED_END;
+
+ // Ignore trailing zeros
+ if (str[point] == '.') {
+ while (str[last - 1] == '0') {
+ --last;
+ }
+ }
+
+ // Add leading sign only if the number is negative
+ size_t o = 0;
+ if (str[sign] == '-') {
+ o += write_char('-', buf_size, buf, o);
+ }
+
+ // Handle zero as a special case (no non-zero digits to copy)
+ if (first == last) {
+ o += write_string(3, "0.0", buf_size, buf, o);
+ return result(EXESS_SUCCESS, o);
+ }
+
+ // Add leading zero if needed to have at least one digit before the point
+ if (str[first] == '.') {
+ o += write_char('0', buf_size, buf, o);
+ }
+
+ // Add digits
+ o += write_string(last - first, str + first, buf_size, buf, o);
+
+ if (str[point] != '.') {
+ // Add missing decimal suffix
+ o += write_string(2, ".0", buf_size, buf, o);
+ } else if (point == last - 1) {
+ // Add missing trailing zero after point
+ o += write_char('0', buf_size, buf, o);
+ }
+
+ return result(st, o);
+}
+
+static ExessResult
+write_integer(const char* const str,
+ const size_t buf_size,
+ char* const buf,
+ ExessIntegerKind* const kind)
+{
+ const size_t sign = scan(is_space, str, 0); // Sign
+ const size_t leading = skip(is_sign, str, sign); // First digit
+ if (!is_digit(str[leading])) {
+ return result(EXESS_EXPECTED_DIGIT, sign);
+ }
+
+ const size_t first = scan(is_zero, str, leading); // First non-zero
+ const size_t last = scan(is_digit, str, first); // Last digit
+ const size_t end = scan(is_space, str, last); // Last non-space
+
+ const ExessStatus st = is_end(str[end]) ? EXESS_SUCCESS : EXESS_EXPECTED_END;
+
+ // Handle zero as a special case (no non-zero digits to copy)
+ size_t o = 0;
+ if (first == last) {
+ o += write_char('0', buf_size, buf, o);
+ *kind = EXESS_ZERO;
+ return result(EXESS_SUCCESS, o);
+ }
+
+ // Add leading sign only if the number is negative
+ if (str[sign] == '-') {
+ *kind = EXESS_NEGATIVE;
+ o += write_char('-', buf_size, buf, o);
+ } else {
+ *kind = EXESS_POSITIVE;
+ }
+
+ // Add digits
+ o += write_string(last - first, str + first, buf_size, buf, o);
+
+ return result(st, o);
+}
+
+static ExessResult
+write_hex(const char* const str, const size_t buf_size, char* const buf)
+{
+ size_t i = 0;
+ size_t o = 0;
+
+ for (; str[i]; ++i) {
+ if (is_hexdig(str[i])) {
+ o += write_char(str[i], buf_size, buf, o);
+ } else if (!is_space(str[i])) {
+ return result(EXESS_EXPECTED_HEX, o);
+ }
+ }
+
+ if (o == 0 || o % 2 != 0) {
+ return result(EXESS_EXPECTED_HEX, o);
+ }
+
+ return result(EXESS_SUCCESS, o);
+}
+
+static ExessResult
+write_base64(const char* const str, const size_t buf_size, char* const buf)
+{
+ size_t i = 0;
+ size_t o = 0;
+
+ for (; str[i]; ++i) {
+ if (is_base64(str[i])) {
+ o += write_char(str[i], buf_size, buf, o);
+ } else if (!is_space(str[i])) {
+ return result(EXESS_EXPECTED_BASE64, o);
+ }
+ }
+
+ if (o == 0 || o % 4 != 0) {
+ return result(EXESS_EXPECTED_BASE64, o);
+ }
+
+ return result(EXESS_SUCCESS, o);
+}
+
+static ExessResult
+write_bounded(const char* const str,
+ const ExessDatatype datatype,
+ const size_t buf_size,
+ char* const buf)
+{
+ ExessVariant variant = {EXESS_NOTHING, {EXESS_SUCCESS}};
+ const ExessResult r = exess_read_variant(&variant, datatype, str);
+
+ return r.status ? r : exess_write_variant(variant, buf_size, buf);
+}
+
+ExessResult
+exess_write_canonical(const char* const str,
+ const ExessDatatype datatype,
+ const size_t buf_size,
+ char* const buf)
+{
+ ExessIntegerKind kind = EXESS_ZERO;
+ ExessResult r = {EXESS_UNSUPPORTED, 0};
+
+ switch (datatype) {
+ case EXESS_NOTHING:
+ break;
+
+ case EXESS_BOOLEAN:
+ r = write_bounded(str, datatype, buf_size, buf);
+ break;
+
+ case EXESS_DECIMAL:
+ r = write_decimal(str, buf_size, buf);
+ break;
+
+ case EXESS_DOUBLE:
+ case EXESS_FLOAT:
+ r = write_bounded(str, datatype, buf_size, buf);
+ break;
+
+ case EXESS_INTEGER:
+ r = write_integer(str, buf_size, buf, &kind);
+ break;
+
+ case EXESS_NON_POSITIVE_INTEGER:
+ r = write_integer(str, buf_size, buf, &kind);
+ if (kind == EXESS_POSITIVE) {
+ r.status = EXESS_BAD_VALUE;
+ }
+ break;
+
+ case EXESS_NEGATIVE_INTEGER:
+ r = write_integer(str, buf_size, buf, &kind);
+ if (kind == EXESS_ZERO || kind == EXESS_POSITIVE) {
+ r.status = EXESS_BAD_VALUE;
+ }
+ break;
+
+ case EXESS_LONG:
+ case EXESS_INT:
+ case EXESS_SHORT:
+ case EXESS_BYTE:
+ r = write_bounded(str, datatype, buf_size, buf);
+ break;
+
+ case EXESS_NON_NEGATIVE_INTEGER:
+ r = write_integer(str, buf_size, buf, &kind);
+ if (kind == EXESS_NEGATIVE) {
+ r.status = EXESS_BAD_VALUE;
+ }
+ break;
+
+ case EXESS_ULONG:
+ case EXESS_UINT:
+ case EXESS_USHORT:
+ case EXESS_UBYTE:
+ r = write_bounded(str, datatype, buf_size, buf);
+ break;
+
+ case EXESS_POSITIVE_INTEGER:
+ r = write_integer(str, buf_size, buf, &kind);
+ if (kind == EXESS_NEGATIVE || kind == EXESS_ZERO) {
+ r.status = EXESS_BAD_VALUE;
+ }
+ break;
+
+ case EXESS_DURATION:
+ case EXESS_DATETIME:
+ case EXESS_TIME:
+ case EXESS_DATE:
+ r = write_bounded(str, datatype, buf_size, buf);
+ break;
+
+ case EXESS_HEX:
+ r = write_hex(str, buf_size, buf);
+ break;
+
+ case EXESS_BASE64:
+ r = write_base64(str, buf_size, buf);
+ }
+
+ return end_write(r.status, buf_size, buf, r.count);
+}
diff --git a/subprojects/exess/src/coerce.c b/subprojects/exess/src/coerce.c
new file mode 100644
index 00000000..1a0c35dc
--- /dev/null
+++ b/subprojects/exess/src/coerce.c
@@ -0,0 +1,422 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "exess/exess.h"
+
+#include <math.h>
+#include <stdint.h>
+
+/* Limits for the range of integers that can be exactly represented in floating
+ point types. Note that these limits are one less than the largest value,
+ since values larger than that may round to it which causes problems with
+ perfect round-tripping. For example, 16777217 when parsed as a float will
+ result in 1.6777216E7, which a "lossless" coercion would then convert to
+ 16777216. */
+
+#define MAX_FLOAT_INT 16777215
+#define MAX_DOUBLE_INT 9007199254740991L
+
+static ExessVariant
+coerce_long_in_range(const ExessVariant variant,
+ const int64_t min,
+ const int64_t max)
+{
+ const ExessVariant result = exess_coerce(variant, EXESS_LONG, EXESS_LOSSLESS);
+ if (result.datatype == EXESS_LONG) {
+ if (result.value.as_long < min || result.value.as_long > max) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+ }
+
+ return result;
+}
+
+static ExessVariant
+coerce_to_long(const ExessVariant variant, const ExessCoercionFlags coercions)
+{
+ switch (variant.datatype) {
+ case EXESS_NOTHING:
+ return variant;
+
+ case EXESS_BOOLEAN:
+ return exess_make_long(variant.value.as_bool);
+
+ case EXESS_DECIMAL:
+ case EXESS_DOUBLE:
+ if (!(coercions & (ExessCoercionFlags)EXESS_ROUND) &&
+ variant.value.as_double > trunc(variant.value.as_double)) {
+ return exess_make_nothing(EXESS_WOULD_ROUND);
+ }
+
+ if (variant.value.as_double < (double)-MAX_DOUBLE_INT ||
+ variant.value.as_double > (double)MAX_DOUBLE_INT) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_long(lrint(variant.value.as_double));
+
+ case EXESS_FLOAT:
+ if (!(coercions & (ExessCoercionFlags)EXESS_ROUND) &&
+ variant.value.as_float > truncf(variant.value.as_float)) {
+ return exess_make_nothing(EXESS_WOULD_ROUND);
+ }
+
+ if (variant.value.as_float < (float)-MAX_FLOAT_INT ||
+ variant.value.as_float > (float)MAX_FLOAT_INT) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_long(lrintf(variant.value.as_float));
+
+ case EXESS_INTEGER:
+ case EXESS_NON_POSITIVE_INTEGER:
+ case EXESS_NEGATIVE_INTEGER:
+ case EXESS_LONG:
+ return exess_make_long(variant.value.as_long);
+
+ case EXESS_INT:
+ return exess_make_long(variant.value.as_int);
+
+ case EXESS_SHORT:
+ return exess_make_long(variant.value.as_short);
+
+ case EXESS_BYTE:
+ return exess_make_long(variant.value.as_byte);
+
+ case EXESS_NON_NEGATIVE_INTEGER:
+ return exess_make_long(variant.value.as_long);
+
+ case EXESS_ULONG:
+ if (variant.value.as_ulong > INT64_MAX) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_long((int64_t)variant.value.as_ulong);
+
+ case EXESS_UINT:
+ return exess_make_long(variant.value.as_uint);
+
+ case EXESS_USHORT:
+ return exess_make_long(variant.value.as_ushort);
+
+ case EXESS_UBYTE:
+ return exess_make_long(variant.value.as_ubyte);
+
+ case EXESS_POSITIVE_INTEGER:
+ if (variant.value.as_ulong > INT64_MAX) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_long((int64_t)variant.value.as_ulong);
+
+ case EXESS_DURATION:
+ case EXESS_DATETIME:
+ case EXESS_TIME:
+ case EXESS_DATE:
+ case EXESS_HEX:
+ case EXESS_BASE64:
+ break;
+ }
+
+ return exess_make_nothing(EXESS_UNSUPPORTED);
+}
+
+static ExessVariant
+coerce_ulong_in_range(const ExessVariant variant, const uint64_t max)
+{
+ const ExessVariant result =
+ exess_coerce(variant, EXESS_ULONG, EXESS_LOSSLESS);
+
+ if (result.datatype == EXESS_ULONG) {
+ if (variant.value.as_ulong > max) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+ }
+
+ return result;
+}
+
+static ExessVariant
+coerce_to_ulong(const ExessVariant value, const ExessCoercionFlags coercions)
+{
+ switch (value.datatype) {
+ case EXESS_NOTHING:
+ return value;
+
+ case EXESS_BOOLEAN:
+ return exess_make_ulong(value.value.as_bool);
+
+ case EXESS_DECIMAL:
+ case EXESS_DOUBLE:
+ if (!(coercions & (ExessCoercionFlags)EXESS_ROUND) &&
+ value.value.as_double > trunc(value.value.as_double)) {
+ return exess_make_nothing(EXESS_WOULD_ROUND);
+ }
+
+ if (value.value.as_double < 0.0 ||
+ value.value.as_double > (double)MAX_DOUBLE_INT) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_ulong((uint64_t)lrint(value.value.as_double));
+
+ case EXESS_FLOAT:
+ if (!(coercions & (ExessCoercionFlags)EXESS_ROUND) &&
+ value.value.as_float > truncf(value.value.as_float)) {
+ return exess_make_nothing(EXESS_WOULD_ROUND);
+ }
+
+ if (value.value.as_float < 0.0f ||
+ value.value.as_float > (float)MAX_FLOAT_INT) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_ulong((uint64_t)lrintf(value.value.as_float));
+
+ case EXESS_INTEGER:
+ case EXESS_NON_POSITIVE_INTEGER:
+ case EXESS_NEGATIVE_INTEGER:
+ case EXESS_LONG:
+ if (value.value.as_long < 0) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_ulong((uint64_t)value.value.as_long);
+
+ case EXESS_INT:
+ if (value.value.as_int < 0) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_ulong((uint64_t)value.value.as_int);
+
+ case EXESS_SHORT:
+ if (value.value.as_short < 0) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_ulong((uint64_t)value.value.as_short);
+
+ case EXESS_BYTE:
+ if (value.value.as_byte < 0) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ return exess_make_ulong((uint64_t)value.value.as_byte);
+
+ case EXESS_NON_NEGATIVE_INTEGER:
+ case EXESS_ULONG:
+ return exess_make_ulong(value.value.as_ulong);
+
+ case EXESS_UINT:
+ return exess_make_ulong(value.value.as_uint);
+
+ case EXESS_USHORT:
+ return exess_make_ulong(value.value.as_ushort);
+
+ case EXESS_UBYTE:
+ return exess_make_ulong(value.value.as_ubyte);
+
+ case EXESS_POSITIVE_INTEGER:
+ return exess_make_ulong(value.value.as_ulong);
+
+ case EXESS_DURATION:
+ case EXESS_DATETIME:
+ case EXESS_TIME:
+ case EXESS_DATE:
+ case EXESS_HEX:
+ case EXESS_BASE64:
+ break;
+ }
+
+ return exess_make_nothing(EXESS_UNSUPPORTED);
+}
+
+ExessVariant
+exess_coerce(const ExessVariant value,
+ const ExessDatatype datatype,
+ const ExessCoercionFlags coercions)
+{
+ if (datatype == value.datatype) {
+ return value;
+ }
+
+ ExessVariant result = value;
+
+ switch (datatype) {
+ case EXESS_NOTHING:
+ break;
+
+ case EXESS_BOOLEAN:
+ result = exess_coerce(value, EXESS_LONG, coercions);
+ if (result.datatype == EXESS_LONG) {
+ if (!(coercions & (ExessCoercionFlags)EXESS_TRUNCATE) &&
+ result.value.as_long != 0 && result.value.as_long != 1) {
+ return exess_make_nothing(EXESS_WOULD_TRUNCATE);
+ }
+
+ return exess_make_boolean(result.value.as_long != 0);
+ }
+ break;
+
+ case EXESS_DECIMAL:
+ // FIXME
+
+ case EXESS_DOUBLE:
+ if (value.datatype == EXESS_DECIMAL) {
+ return exess_make_double(value.value.as_double);
+ }
+
+ if (value.datatype == EXESS_FLOAT) {
+ return exess_make_double((double)value.value.as_float);
+ }
+
+ result = coerce_long_in_range(value, -MAX_DOUBLE_INT, MAX_DOUBLE_INT);
+ if (result.datatype == EXESS_LONG) {
+ return exess_make_double((double)result.value.as_long);
+ }
+
+ break;
+
+ case EXESS_FLOAT:
+ if (value.datatype == EXESS_DECIMAL || value.datatype == EXESS_DOUBLE) {
+ if (!(coercions & (ExessCoercionFlags)EXESS_REDUCE_PRECISION)) {
+ return exess_make_nothing(EXESS_WOULD_REDUCE_PRECISION);
+ }
+
+ return exess_make_float((float)result.value.as_double);
+ } else {
+ result = coerce_long_in_range(value, -MAX_FLOAT_INT, MAX_FLOAT_INT);
+ if (result.datatype == EXESS_LONG) {
+ return exess_make_float((float)result.value.as_long);
+ }
+ }
+
+ break;
+
+ case EXESS_INTEGER:
+ result = coerce_to_long(value, coercions);
+ break;
+
+ case EXESS_NON_POSITIVE_INTEGER:
+ result = coerce_to_long(value, coercions);
+ if (result.datatype == EXESS_LONG && result.value.as_long > 0) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+
+ break;
+
+ case EXESS_NEGATIVE_INTEGER:
+ result = coerce_to_long(value, coercions);
+ if (result.datatype == EXESS_LONG && result.value.as_long >= 0) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+ break;
+
+ case EXESS_LONG:
+ return coerce_to_long(value, coercions);
+
+ case EXESS_INT:
+ result = coerce_long_in_range(value, INT32_MIN, INT32_MAX);
+ break;
+
+ case EXESS_SHORT:
+ result = coerce_long_in_range(value, INT16_MIN, INT16_MAX);
+ break;
+
+ case EXESS_BYTE:
+ result = coerce_long_in_range(value, INT8_MIN, INT8_MAX);
+ break;
+
+ case EXESS_NON_NEGATIVE_INTEGER:
+ case EXESS_ULONG:
+ result = coerce_to_ulong(value, coercions);
+ break;
+
+ case EXESS_UINT:
+ result = coerce_ulong_in_range(value, UINT32_MAX);
+ break;
+
+ case EXESS_USHORT:
+ result = coerce_ulong_in_range(value, UINT16_MAX);
+ break;
+
+ case EXESS_UBYTE:
+ result = coerce_ulong_in_range(value, UINT8_MAX);
+ break;
+
+ case EXESS_POSITIVE_INTEGER:
+ result = coerce_to_ulong(value, coercions);
+ if (result.datatype == EXESS_ULONG && result.value.as_ulong == 0u) {
+ return exess_make_nothing(EXESS_OUT_OF_RANGE);
+ }
+ break;
+
+ case EXESS_DURATION:
+ case EXESS_DATETIME:
+ return exess_make_nothing(EXESS_UNSUPPORTED);
+
+ case EXESS_TIME:
+ if (value.datatype != EXESS_DATETIME) {
+ return exess_make_nothing(EXESS_UNSUPPORTED);
+ }
+
+ if (coercions & (ExessCoercionFlags)EXESS_TRUNCATE) {
+ const ExessTime time = {
+ {value.value.as_datetime.is_utc ? 0 : EXESS_LOCAL},
+ value.value.as_datetime.hour,
+ value.value.as_datetime.minute,
+ value.value.as_datetime.second,
+ value.value.as_datetime.nanosecond};
+
+ return exess_make_time(time);
+ }
+
+ return exess_make_nothing(EXESS_WOULD_TRUNCATE);
+
+ case EXESS_DATE:
+ if (value.datatype != EXESS_DATETIME) {
+ return exess_make_nothing(EXESS_UNSUPPORTED);
+ }
+
+ if (coercions & (ExessCoercionFlags)EXESS_TRUNCATE) {
+ const ExessDate date = {
+ value.value.as_datetime.year,
+ value.value.as_datetime.month,
+ value.value.as_datetime.day,
+ {value.value.as_datetime.is_utc ? 0 : EXESS_LOCAL}};
+ return exess_make_date(date);
+ }
+
+ return exess_make_nothing(EXESS_WOULD_TRUNCATE);
+
+ case EXESS_HEX:
+ return (value.datatype == EXESS_BASE64)
+ ? exess_make_hex(value.value.as_blob)
+ : exess_make_nothing(EXESS_UNSUPPORTED);
+
+ case EXESS_BASE64:
+ return (value.datatype == EXESS_HEX)
+ ? exess_make_base64(value.value.as_blob)
+ : exess_make_nothing(EXESS_UNSUPPORTED);
+ }
+
+ if (result.datatype != EXESS_NOTHING) {
+ result.datatype = datatype;
+ }
+
+ return result;
+}
diff --git a/subprojects/exess/src/datatype.c b/subprojects/exess/src/datatype.c
new file mode 100644
index 00000000..c7789597
--- /dev/null
+++ b/subprojects/exess/src/datatype.c
@@ -0,0 +1,79 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "exess/exess.h"
+
+#include <stdbool.h>
+#include <string.h>
+
+#define N_DATATYPES 24
+
+static const char* EXESS_NONNULL const uris[N_DATATYPES + 1] = {
+ NULL, //
+ EXESS_XSD_URI "boolean", //
+ EXESS_XSD_URI "decimal", //
+ EXESS_XSD_URI "double", //
+ EXESS_XSD_URI "float", //
+ EXESS_XSD_URI "integer", //
+ EXESS_XSD_URI "nonPositiveInteger", //
+ EXESS_XSD_URI "negativeInteger", //
+ EXESS_XSD_URI "long", //
+ EXESS_XSD_URI "int", //
+ EXESS_XSD_URI "short", //
+ EXESS_XSD_URI "byte", //
+ EXESS_XSD_URI "nonNegativeInteger", //
+ EXESS_XSD_URI "unsignedLong", //
+ EXESS_XSD_URI "unsignedInt", //
+ EXESS_XSD_URI "unsignedShort", //
+ EXESS_XSD_URI "unsignedByte", //
+ EXESS_XSD_URI "positiveInteger", //
+ EXESS_XSD_URI "duration", //
+ EXESS_XSD_URI "datetime", //
+ EXESS_XSD_URI "time", //
+ EXESS_XSD_URI "date", //
+ EXESS_XSD_URI "hexBinary", //
+ EXESS_XSD_URI "base64Binary", //
+};
+
+const char*
+exess_datatype_uri(const ExessDatatype datatype)
+{
+ return (datatype > EXESS_NOTHING && datatype <= EXESS_BASE64) ? uris[datatype]
+ : NULL;
+}
+
+ExessDatatype
+exess_datatype_from_uri(const char* const uri)
+{
+ static const size_t xsd_len = sizeof(EXESS_XSD_URI) - 1;
+
+ if (!strncmp(uri, EXESS_XSD_URI, xsd_len)) {
+ const char* const name = uri + xsd_len;
+ for (size_t i = 1; i < N_DATATYPES; ++i) {
+ if (!strcmp(name, uris[i] + xsd_len)) {
+ return (ExessDatatype)i;
+ }
+ }
+ }
+
+ return EXESS_NOTHING;
+}
+
+bool
+exess_datatype_is_bounded(const ExessDatatype datatype)
+{
+ return (datatype < N_DATATYPES) ? exess_max_lengths[datatype] != 0 : false;
+}
diff --git a/subprojects/exess/src/date.c b/subprojects/exess/src/date.c
new file mode 100644
index 00000000..77b336f7
--- /dev/null
+++ b/subprojects/exess/src/date.c
@@ -0,0 +1,112 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "date_utils.h"
+#include "read_utils.h"
+#include "timezone.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+ExessResult
+read_date_numbers(ExessDate* const out, const char* const str)
+{
+ // Read year at the beginning
+ size_t i = skip_whitespace(str);
+ ExessResult r = read_year_number(&out->year, str + i);
+ if (r.status) {
+ return result(r.status, i + r.count);
+ }
+
+ // Read year-month delimiter
+ i += r.count;
+ if (str[i] != '-') {
+ return result(EXESS_EXPECTED_DASH, i);
+ }
+
+ // Read month
+ ++i;
+ if ((r = read_two_digit_number(&out->month, 1, 12, str + i)).status) {
+ return result(r.status, i + r.count);
+ }
+
+ // Read month-day delimiter
+ i += r.count;
+ if (str[i] != '-') {
+ return result(EXESS_EXPECTED_DASH, i);
+ }
+
+ // Read day
+ ++i;
+ if ((r = read_two_digit_number(&out->day, 1, 31, str + i)).status) {
+ return result(r.status, i + r.count);
+ }
+
+ // Check that day is in range
+ i += r.count;
+ if (out->day > days_in_month(out->year, out->month)) {
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ return result(EXESS_SUCCESS, i);
+}
+
+ExessResult
+exess_read_date(ExessDate* const out, const char* const str)
+{
+ memset(out, 0, sizeof(*out));
+
+ // Read YYYY-MM-DD numbers
+ size_t i = skip_whitespace(str);
+ ExessResult r = read_date_numbers(out, str + i);
+
+ i += r.count;
+ if (r.status || is_end(str[i])) {
+ out->zone.quarter_hours = EXESS_LOCAL;
+ return result(r.status, i);
+ }
+
+ // Read timezone
+ r = exess_read_timezone(&out->zone, str + i);
+
+ return result(r.status, i + r.count);
+}
+
+ExessResult
+exess_write_date(const ExessDate value, const size_t buf_size, char* const buf)
+{
+ if (value.month < 1 || value.month > 12 || value.day < 1 || value.day > 31) {
+ return end_write(EXESS_BAD_VALUE, buf_size, buf, 0);
+ }
+
+ ExessResult r = write_year_number(value.year, buf_size, buf);
+ size_t o = r.count;
+ if (r.status) {
+ return end_write(r.status, buf_size, buf, o);
+ }
+
+ o += write_char('-', buf_size, buf, o);
+ o += write_two_digit_number(value.month, buf_size, buf, o);
+ o += write_char('-', buf_size, buf, o);
+ o += write_two_digit_number(value.day, buf_size, buf, o);
+
+ r = write_timezone(value.zone, buf_size, buf, o);
+
+ return end_write(r.status, buf_size, buf, o + r.count);
+}
diff --git a/subprojects/exess/src/date_utils.h b/subprojects/exess/src/date_utils.h
new file mode 100644
index 00000000..b864925e
--- /dev/null
+++ b/subprojects/exess/src/date_utils.h
@@ -0,0 +1,65 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_DATE_UTILS_H
+#define EXESS_DATE_UTILS_H
+
+#include "exess/exess.h"
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+static inline bool
+is_leap_year(const int64_t year)
+{
+ if (year % 4) {
+ return false;
+ }
+
+ if (year % 100) {
+ return true;
+ }
+
+ if (year % 400) {
+ return false;
+ }
+
+ return true;
+}
+
+static inline uint8_t
+days_in_month(const int16_t year, const uint8_t month)
+{
+ return month == 2u ? (is_leap_year(year) ? 29u : 28u)
+ : (uint8_t)(30u + (month + (month / 8u)) % 2u);
+}
+
+ExessResult
+read_year_number(int16_t* out, const char* str);
+
+ExessResult
+write_year_number(int16_t value, size_t buf_size, char* buf);
+
+/// Read YYYY-MM-DD date numbers without a timezone
+ExessResult
+read_date_numbers(ExessDate* out, const char* str);
+
+EXESS_CONST_FUNC
+size_t
+exess_timezone_string_length(ExessTimezone value);
+
+#endif // EXESS_DATE_UTILS_H
diff --git a/subprojects/exess/src/datetime.c b/subprojects/exess/src/datetime.c
new file mode 100644
index 00000000..0fe56afe
--- /dev/null
+++ b/subprojects/exess/src/datetime.c
@@ -0,0 +1,265 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "date_utils.h"
+#include "read_utils.h"
+#include "string_utils.h"
+#include "time_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+static inline ExessDateTime
+infinite_future(const bool is_utc)
+{
+ const ExessDateTime result = {INT16_MAX,
+ UINT8_MAX,
+ UINT8_MAX,
+ is_utc,
+ UINT8_MAX,
+ UINT8_MAX,
+ UINT8_MAX,
+ UINT32_MAX};
+
+ return result;
+}
+
+static inline ExessDateTime
+infinite_past(const bool is_utc)
+{
+ const ExessDateTime result = {INT16_MIN, 0, 0, is_utc, 0, 0, 0, 0};
+
+ return result;
+}
+
+static int32_t
+modulo(const int32_t a, const int32_t low, const int32_t high)
+{
+ return ((a - low) % (high - low)) + low;
+}
+
+static int32_t
+quotient(const int32_t a, const int32_t low, const int32_t high)
+{
+ return (a - low) / (high - low);
+}
+
+ExessDateTime
+exess_add_datetime_duration(const ExessDateTime s, const ExessDuration d)
+{
+ /*
+ See <https://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes>.
+ This algorithm is modified to support subtraction when d is negative.
+ */
+
+ const int32_t d_year = d.months / 12;
+ const int32_t d_month = d.months % 12;
+ const int32_t d_day = d.seconds / (24 * 60 * 60);
+ const int32_t d_hour = d.seconds / 60 / 60 % 24;
+ const int32_t d_minute = d.seconds / 60 % 60;
+ const int32_t d_second = d.seconds % 60;
+ const int32_t d_nanosecond = d.nanoseconds;
+
+ ExessDateTime e = {0, 0u, 0u, s.is_utc, 0u, 0u, 0u, 0u};
+ int32_t temp = 0;
+ int32_t carry = 0;
+
+ // Months (may be modified additionally below)
+ temp = s.month + d_month;
+ if (temp <= 0) {
+ e.month = (uint8_t)(12 + modulo(temp, 1, 13));
+ carry = quotient(temp, 1, 13) - 1;
+ } else {
+ e.month = (uint8_t)modulo(temp, 1, 13);
+ carry = quotient(temp, 1, 13);
+ }
+
+ // Years (may be modified additionally below)
+ temp = s.year + d_year + carry;
+ if (temp > INT16_MAX) {
+ return infinite_future(s.is_utc);
+ }
+
+ if (temp < INT16_MIN) {
+ return infinite_past(s.is_utc);
+ }
+
+ e.year = (int16_t)temp;
+
+ // Nanoseconds
+ temp = (int32_t)s.nanosecond + d_nanosecond;
+ if (temp < 0) {
+ e.nanosecond = (uint32_t)(1000000000 + (temp % 1000000000));
+ carry = temp / 1000000000 - 1;
+ } else {
+ e.nanosecond = (uint32_t)(temp % 1000000000);
+ carry = temp / 1000000000;
+ }
+
+ // Seconds
+ temp = s.second + d_second + carry;
+ if (temp < 0) {
+ e.second = (uint8_t)(60 + (temp % 60));
+ carry = temp / 60 - 1;
+ } else {
+ e.second = (uint8_t)(temp % 60);
+ carry = temp / 60;
+ }
+
+ // Minutes
+ temp = s.minute + d_minute + carry;
+ if (temp < 0) {
+ e.minute = (uint8_t)(60 + (temp % 60));
+ carry = temp / 60 - 1;
+ } else {
+ e.minute = (uint8_t)(temp % 60);
+ carry = temp / 60;
+ }
+
+ // Hours
+ temp = s.hour + d_hour + carry;
+ if (temp < 0) {
+ e.hour = (uint8_t)(24 + (temp % 24));
+ carry = temp / 24 - 1;
+ } else {
+ e.hour = (uint8_t)(temp % 24);
+ carry = temp / 24;
+ }
+
+ /*
+ Carry days into months and years as necessary. Note that the algorithm in
+ the spec first clamps here, but we don't because no such datetime should
+ exist (exess_read_datetime refuses to read them)
+ */
+ int32_t day = s.day + d_day + carry;
+ while (day < 1 || day > days_in_month(e.year, e.month)) {
+ if (day < 1) {
+ if (e.month == 1) {
+ if (e.year == INT16_MIN) {
+ return infinite_past(s.is_utc);
+ }
+
+ --e.year;
+ e.month = 12;
+ day += days_in_month(e.year, e.month);
+ } else {
+ --e.month;
+ day += days_in_month(e.year, e.month);
+ }
+ } else {
+ day -= days_in_month(e.year, e.month);
+ if (++e.month > 12) {
+ if (e.year == INT16_MAX) {
+ return infinite_future(s.is_utc);
+ }
+
+ ++e.year;
+ e.month = (uint8_t)modulo(e.month, 1, 13);
+ }
+ }
+ }
+
+ e.day = (uint8_t)day;
+
+ return e;
+}
+
+ExessResult
+exess_read_datetime(ExessDateTime* const out, const char* const str)
+{
+ out->year = 0;
+ out->month = 0;
+ out->day = 0;
+
+ // Read date
+ ExessDate date = {0, 0u, 0u, {EXESS_LOCAL}};
+ const ExessResult dr = read_date_numbers(&date, str);
+ if (dr.status) {
+ return dr;
+ }
+
+ size_t i = dr.count;
+ if (str[i] != 'T') {
+ return result(EXESS_EXPECTED_TIME_SEP, i);
+ }
+
+ ++i;
+
+ // Read time
+ ExessTime time = {{INT8_MAX}, 0u, 0u, 0u, 0u};
+ const ExessResult tr = exess_read_time(&time, str + i);
+ if (tr.status) {
+ return result(tr.status, i + tr.count);
+ }
+
+ i += tr.count;
+
+ const ExessDateTime datetime = {date.year,
+ date.month,
+ date.day,
+ time.zone.quarter_hours != EXESS_LOCAL,
+ time.hour,
+ time.minute,
+ time.second,
+ time.nanosecond};
+
+ if (datetime.is_utc) {
+ const ExessDuration tz_duration = {
+ 0u, -time.zone.quarter_hours * 15 * 60, 0};
+
+ *out = exess_add_datetime_duration(datetime, tz_duration);
+ } else {
+ *out = datetime;
+ }
+
+ return result(EXESS_SUCCESS, i);
+}
+
+ExessResult
+exess_write_datetime(const ExessDateTime value,
+ const size_t buf_size,
+ char* const buf)
+{
+ const ExessTimezone local = {EXESS_LOCAL};
+ const ExessDate date = {value.year, value.month, value.day, local};
+ const ExessTimezone zone = {value.is_utc ? 0 : EXESS_LOCAL};
+ const ExessTime time = {
+ zone, value.hour, value.minute, value.second, value.nanosecond};
+
+ if (!in_range(value.month, 1, 12) || !in_range(value.day, 1, 31) ||
+ !in_range(value.hour, 0, 24) || !in_range(value.minute, 0, 59) ||
+ !in_range(value.second, 0, 59) || value.nanosecond > 999999999) {
+ return end_write(EXESS_BAD_VALUE, buf_size, buf, 0);
+ }
+
+ // Write date
+ ExessResult dr = exess_write_date(date, buf_size, buf);
+ if (dr.status) {
+ return end_write(dr.status, buf_size, buf, dr.count);
+ }
+
+ // Write time delimiter
+ size_t o = dr.count + write_char('T', buf_size, buf, dr.count);
+
+ // Write time with timezone
+ const ExessResult tr = write_time(time, buf_size, buf, o);
+
+ return end_write(tr.status, buf_size, buf, o + tr.count);
+}
diff --git a/subprojects/exess/src/decimal.c b/subprojects/exess/src/decimal.c
new file mode 100644
index 00000000..a8ce8ad5
--- /dev/null
+++ b/subprojects/exess/src/decimal.c
@@ -0,0 +1,267 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "decimal.h"
+#include "digits.h"
+#include "read_utils.h"
+#include "string_utils.h"
+#include "strtod.h"
+#include "warnings.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <math.h>
+#include <stdbool.h>
+#include <string.h>
+
+#include <assert.h>
+
+typedef enum {
+ EXESS_POINT_AFTER, ///< Decimal point is after all significant digits
+ EXESS_POINT_BEFORE, ///< Decimal point is before all significant digits
+ EXESS_POINT_BETWEEN, ///< Decimal point is between significant digits
+} ExessPointLocation;
+
+typedef struct {
+ ExessPointLocation point_loc; ///< Location of decimal point
+ unsigned n_zeros_before; ///< Number of extra zeros before point
+ unsigned n_zeros_after; ///< Number of extra zeros after point
+} DecimalMetrics;
+
+static DecimalMetrics
+decimal_metrics(const ExessDecimalDouble count)
+{
+ const int expt =
+ count.expt >= 0 ? (count.expt - (int)count.n_digits + 1) : count.expt;
+
+ DecimalMetrics metrics = {EXESS_POINT_AFTER, 0u, 0u};
+
+ if (count.expt >= (int)count.n_digits - 1) {
+ metrics.point_loc = EXESS_POINT_AFTER;
+ metrics.n_zeros_before = (unsigned)count.expt - (count.n_digits - 1u);
+ metrics.n_zeros_after = 1u;
+ } else if (count.expt < 0) {
+ metrics.point_loc = EXESS_POINT_BEFORE;
+ metrics.n_zeros_before = 1u;
+ metrics.n_zeros_after = (unsigned)(-expt - 1);
+ } else {
+ metrics.point_loc = EXESS_POINT_BETWEEN;
+ }
+
+ return metrics;
+}
+
+static ExessNumberKind
+number_kind(const double d)
+{
+ EXESS_DISABLE_CONVERSION_WARNINGS
+ const int fpclass = fpclassify(d);
+ const bool is_negative = signbit(d);
+ EXESS_RESTORE_WARNINGS
+
+ switch (fpclass) {
+ case FP_ZERO:
+ return is_negative ? EXESS_NEGATIVE_ZERO : EXESS_POSITIVE_ZERO;
+ case FP_INFINITE:
+ return is_negative ? EXESS_NEGATIVE_INFINITY : EXESS_POSITIVE_INFINITY;
+ case FP_NORMAL:
+ case FP_SUBNORMAL:
+ return is_negative ? EXESS_NEGATIVE : EXESS_POSITIVE;
+ default:
+ break;
+ }
+
+ return EXESS_NAN;
+}
+
+ExessDecimalDouble
+exess_measure_decimal(const double d, const unsigned max_precision)
+{
+ ExessDecimalDouble value = {number_kind(d), 0, 0, {0}};
+
+ if (value.kind != EXESS_NEGATIVE && value.kind != EXESS_POSITIVE) {
+ return value;
+ }
+
+ // Get decimal digits
+ const double abs_d = fabs(d);
+ const ExessDigitCount count =
+ exess_digits(abs_d, value.digits, max_precision);
+
+ assert(count.count == 1 || value.digits[count.count - 1] != '0');
+
+ value.n_digits = count.count;
+ value.expt = count.expt;
+
+ return value;
+}
+
+ExessDecimalDouble
+exess_measure_float(const float f)
+{
+ return exess_measure_decimal((double)f, FLT_DECIMAL_DIG);
+}
+
+ExessDecimalDouble
+exess_measure_double(const double d)
+{
+ return exess_measure_decimal(d, DBL_DECIMAL_DIG);
+}
+
+static size_t
+exess_decimal_double_string_length(const ExessDecimalDouble decimal)
+{
+ switch (decimal.kind) {
+ case EXESS_NEGATIVE:
+ break;
+ case EXESS_NEGATIVE_INFINITY:
+ return 0;
+ case EXESS_NEGATIVE_ZERO:
+ return 4;
+ case EXESS_POSITIVE_ZERO:
+ return 3;
+ case EXESS_POSITIVE:
+ break;
+ case EXESS_POSITIVE_INFINITY:
+ case EXESS_NAN:
+ return 0;
+ }
+
+ const DecimalMetrics metrics = decimal_metrics(decimal);
+ const unsigned n_zeros = metrics.n_zeros_before + metrics.n_zeros_after;
+ const bool is_negative = decimal.kind == EXESS_NEGATIVE;
+
+ return is_negative + decimal.n_digits + 1 + n_zeros;
+}
+
+static size_t
+copy_digits(char* const dest, const char* const src, const size_t n)
+{
+ memcpy(dest, src, n);
+ return n;
+}
+
+static size_t
+set_zeros(char* const dest, const size_t n)
+{
+ memset(dest, '0', n);
+ return n;
+}
+
+static ExessResult
+read_decimal_number(double* const out, const char* const str)
+{
+ *out = (double)NAN;
+
+ if (str[0] == '+' || str[0] == '-') {
+ if (str[1] != '.' && !is_digit(str[1])) {
+ return result(EXESS_EXPECTED_DIGIT, 1);
+ }
+ } else if (str[0] != '.' && !is_digit(str[0])) {
+ return result(EXESS_EXPECTED_DIGIT, 0);
+ }
+
+ const size_t i = skip_whitespace(str);
+ ExessDecimalDouble in = {EXESS_NAN, 0u, 0, {0}};
+ const ExessResult r = parse_decimal(&in, str + i);
+
+ if (!r.status) {
+ *out = parsed_double_to_double(in);
+ }
+
+ return result(r.status, i + r.count);
+}
+
+ExessResult
+exess_read_decimal(double* const out, const char* const str)
+{
+ const size_t i = skip_whitespace(str);
+ const ExessResult r = read_decimal_number(out, str + i);
+
+ return end_read(r.status, str, i + r.count);
+}
+
+ExessResult
+exess_write_decimal_double(const ExessDecimalDouble decimal,
+ const size_t buf_size,
+ char* const buf)
+{
+ if (!buf) {
+ return result(EXESS_SUCCESS, exess_decimal_double_string_length(decimal));
+ }
+
+ size_t i = 0;
+ if (buf_size < 3) {
+ return end_write(EXESS_NO_SPACE, buf_size, buf, 0);
+ }
+
+ switch (decimal.kind) {
+ case EXESS_NEGATIVE:
+ buf[i++] = '-';
+ break;
+ case EXESS_NEGATIVE_INFINITY:
+ return end_write(EXESS_BAD_VALUE, buf_size, buf, 0);
+ case EXESS_NEGATIVE_ZERO:
+ return write_special(4, "-0.0", buf_size, buf);
+ case EXESS_POSITIVE_ZERO:
+ return write_special(3, "0.0", buf_size, buf);
+ case EXESS_POSITIVE:
+ break;
+ case EXESS_POSITIVE_INFINITY:
+ case EXESS_NAN:
+ return end_write(EXESS_BAD_VALUE, buf_size, buf, 0);
+ }
+
+ const DecimalMetrics metrics = decimal_metrics(decimal);
+ const unsigned n_zeros = metrics.n_zeros_before + metrics.n_zeros_after;
+ if (buf_size - i <= decimal.n_digits + 1 + n_zeros) {
+ return end_write(EXESS_NO_SPACE, buf_size, buf, 0);
+ }
+
+ if (metrics.point_loc == EXESS_POINT_AFTER) {
+ i += copy_digits(buf + i, decimal.digits, decimal.n_digits);
+ i += set_zeros(buf + i, metrics.n_zeros_before);
+ buf[i++] = '.';
+ buf[i++] = '0';
+ } else if (metrics.point_loc == EXESS_POINT_BEFORE) {
+ buf[i++] = '0';
+ buf[i++] = '.';
+ i += set_zeros(buf + i, metrics.n_zeros_after);
+ i += copy_digits(buf + i, decimal.digits, decimal.n_digits);
+ } else {
+ assert(metrics.point_loc == EXESS_POINT_BETWEEN);
+ assert(decimal.expt >= -1);
+
+ const size_t n_before = (size_t)decimal.expt + 1u;
+ const size_t n_after = decimal.n_digits - n_before;
+
+ i += copy_digits(buf + i, decimal.digits, n_before);
+ buf[i++] = '.';
+ memcpy(buf + i, decimal.digits + n_before, n_after);
+ i += n_after;
+ }
+
+ return end_write(EXESS_SUCCESS, buf_size, buf, i);
+}
+
+ExessResult
+exess_write_decimal(const double value, const size_t n, char* const buf)
+{
+ const ExessDecimalDouble decimal = exess_measure_double(value);
+
+ return exess_write_decimal_double(decimal, n, buf);
+}
diff --git a/subprojects/exess/src/decimal.h b/subprojects/exess/src/decimal.h
new file mode 100644
index 00000000..7c5aa963
--- /dev/null
+++ b/subprojects/exess/src/decimal.h
@@ -0,0 +1,63 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_DECIMAL_H
+#define EXESS_DECIMAL_H
+
+#include "exess/exess.h"
+
+#include <stddef.h>
+
+// Define C11 numeric constants if the compiler hasn't already
+#ifndef FLT_DECIMAL_DIG
+# define FLT_DECIMAL_DIG 9
+#endif
+#ifndef DBL_DECIMAL_DIG
+# define DBL_DECIMAL_DIG 17
+#endif
+
+typedef enum {
+ EXESS_NEGATIVE,
+ EXESS_NEGATIVE_INFINITY,
+ EXESS_NEGATIVE_ZERO,
+ EXESS_POSITIVE_ZERO,
+ EXESS_POSITIVE,
+ EXESS_POSITIVE_INFINITY,
+ EXESS_NAN,
+} ExessNumberKind;
+
+typedef struct {
+ ExessNumberKind kind; ///< Kind of number
+ int expt; ///< Power of 10 exponent
+ unsigned n_digits; ///< Number of significant digits
+ char digits[DBL_DECIMAL_DIG + 2]; ///< Significant digits
+} ExessDecimalDouble;
+
+ExessDecimalDouble
+exess_measure_decimal(double d, unsigned max_precision);
+
+ExessDecimalDouble
+exess_measure_float(float f);
+
+ExessDecimalDouble
+exess_measure_double(double d);
+
+ExessResult
+exess_write_decimal_double(ExessDecimalDouble decimal,
+ size_t buf_size,
+ char* buf);
+
+#endif // EXESS_DECIMAL_H
diff --git a/subprojects/exess/src/digits.c b/subprojects/exess/src/digits.c
new file mode 100644
index 00000000..dd303269
--- /dev/null
+++ b/subprojects/exess/src/digits.c
@@ -0,0 +1,243 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "digits.h"
+
+#include "bigint.h"
+#include "ieee_float.h"
+#include "soft_float.h"
+#include "warnings.h"
+
+#include <assert.h>
+#include <math.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+/*
+ This is more or less just an implementation of the classic rational number
+ based floating point print routine ("Dragon4"). See "How to Print
+ Floating-Point Numbers Accurately" by Guy L. Steele Jr. and Jon L White for
+ the canonical source. The basic idea is to find a big rational between 1 and
+ 10 where value = (numer / denom) * 10^e, then continuously divide it to
+ generate decimal digits.
+
+ Unfortunately, this algorithm requires pretty massive bigints to work
+ correctly for all doubles, and isn't particularly fast. Something like
+ Grisu3 could be added to improve performance, but that has the annoying
+ property of needing a more precise fallback in some cases, meaning it would
+ only add more code, not replace any. Since this is already a pretty
+ ridiculous amount of code, I'll hold off on this until it becomes a problem,
+ or somebody comes up with a better algorithm.
+*/
+
+/// Return true if the number is within the lower boundary
+static bool
+within_lower(const ExessBigint* const numer,
+ const ExessBigint* const d_lower,
+ const bool is_even)
+{
+ return is_even ? exess_bigint_compare(numer, d_lower) <= 0
+ : exess_bigint_compare(numer, d_lower) < 0;
+}
+
+/// Return true if the number is within the upper boundary
+static bool
+within_upper(const ExessBigint* const numer,
+ const ExessBigint* const denom,
+ const ExessBigint* const d_upper,
+ const bool is_even)
+{
+ return is_even ? exess_bigint_plus_compare(numer, d_upper, denom) >= 0
+ : exess_bigint_plus_compare(numer, d_upper, denom) > 0;
+}
+
+/**
+ Find values so that 0.1 <= numer/denom < 1 or 1 <= numer/denom < 10.
+
+ @param significand Double significand.
+ @param exponent Double exponent (base 2).
+ @param decimal_power Decimal exponent (log10 of the double).
+ @param[out] numer Numerator of rational number.
+ @param[out] denom Denominator of rational number.
+ @param[out] delta Distance to the lower and upper boundaries.
+*/
+static void
+calculate_initial_values(const uint64_t significand,
+ const int exponent,
+ const int decimal_power,
+ const bool lower_is_closer,
+ ExessBigint* const numer,
+ ExessBigint* const denom,
+ ExessBigint* const delta)
+{
+ /* Use a common denominator of 2^1 so that boundary distance is an integer.
+ If the lower boundary is closer, we need to scale everything but the
+ lower boundary to compensate, so add another factor of two here (this is
+ faster than shifting them again later as in the paper). */
+ const unsigned lg_denom = 1u + lower_is_closer;
+
+ if (exponent >= 0) {
+ // delta = 2^e
+ exess_bigint_set_u32(delta, 1);
+ exess_bigint_shift_left(delta, (unsigned)exponent);
+
+ // numer = f * 2^e
+ exess_bigint_set_u64(numer, significand);
+ exess_bigint_shift_left(numer, (unsigned)exponent + lg_denom);
+
+ // denom = 10^d
+ exess_bigint_set_pow10(denom, (unsigned)decimal_power);
+ exess_bigint_shift_left(denom, lg_denom);
+ } else if (decimal_power >= 0) {
+ // delta = 2^e, which is just 1 here since 2^-e is in the denominator
+ exess_bigint_set_u32(delta, 1);
+
+ // numer = f
+ exess_bigint_set_u64(numer, significand);
+ exess_bigint_shift_left(numer, lg_denom);
+
+ // denom = 10^d * 2^-e
+ exess_bigint_set_pow10(denom, (unsigned)decimal_power);
+ exess_bigint_shift_left(denom, (unsigned)-exponent + lg_denom);
+ } else {
+ // delta = 10^d
+ exess_bigint_set_pow10(delta, (unsigned)-decimal_power);
+
+ // numer = f * 10^-d
+ exess_bigint_set(numer, delta);
+ exess_bigint_multiply_u64(numer, significand);
+ exess_bigint_shift_left(numer, lg_denom);
+
+ // denom = 2^-exponent
+ exess_bigint_set_u32(denom, 1);
+ exess_bigint_shift_left(denom, (unsigned)-exponent + lg_denom);
+ }
+}
+
+#ifndef NDEBUG
+static bool
+check_initial_values(const ExessBigint* const numer,
+ const ExessBigint* const denom,
+ const ExessBigint* const d_upper)
+{
+ ExessBigint upper = *numer;
+ exess_bigint_add(&upper, d_upper);
+ assert(exess_bigint_compare(&upper, denom) >= 0);
+
+ const uint32_t div = exess_bigint_divmod(&upper, denom);
+ assert(div >= 1 && div < 10);
+ return true;
+}
+#endif
+
+static unsigned
+emit_digits(ExessBigint* const numer,
+ const ExessBigint* const denom,
+ ExessBigint* const d_lower,
+ ExessBigint* const d_upper,
+ const bool is_even,
+ char* const buffer,
+ const size_t max_digits)
+{
+ unsigned length = 0;
+ for (size_t i = 0; i < max_digits; ++i) {
+ // Emit the next digit
+ const uint32_t digit = exess_bigint_divmod(numer, denom);
+ assert(digit <= 9);
+ buffer[length++] = (char)('0' + digit);
+
+ // Check for termination
+ const bool within_low = within_lower(numer, d_lower, is_even);
+ const bool within_high = within_upper(numer, denom, d_upper, is_even);
+ if (!within_low && !within_high) {
+ exess_bigint_multiply_u32(numer, 10);
+ exess_bigint_multiply_u32(d_lower, 10);
+ if (d_lower != d_upper) {
+ exess_bigint_multiply_u32(d_upper, 10);
+ }
+ } else {
+ if (!within_low || (within_high && exess_bigint_plus_compare(
+ numer, numer, denom) >= 0)) {
+ // In high only, or halfway and the next digit is > 5, round up
+ assert(buffer[length - 1] != '9');
+ buffer[length - 1]++;
+ }
+
+ break;
+ }
+ }
+
+ return length;
+}
+
+ExessDigitCount
+exess_digits(const double d, char* const buf, const unsigned max_digits)
+{
+ EXESS_DISABLE_CONVERSION_WARNINGS
+ assert(isfinite(d) && fpclassify(d) != FP_ZERO);
+ EXESS_RESTORE_WARNINGS
+
+ const ExessSoftFloat value = soft_float_from_double(d);
+ const int power = (int)lrint(log10(d));
+ const bool is_even = !(value.f & 1u);
+ const bool lower_is_closer = double_lower_boundary_is_closer(d);
+
+ // Calculate initial values so that v = (numer / denom) * 10^power
+ ExessBigint numer;
+ ExessBigint denom;
+ ExessBigint d_lower;
+ calculate_initial_values(
+ value.f, value.e, power, lower_is_closer, &numer, &denom, &d_lower);
+
+ ExessBigint d_upper_storage;
+ ExessBigint* d_upper = NULL;
+ if (lower_is_closer) {
+ // Scale upper boundary to account for the closer lower boundary
+ // (the numerator and denominator were already scaled above)
+ d_upper_storage = d_lower;
+ d_upper = &d_upper_storage;
+ exess_bigint_shift_left(d_upper, 1);
+ } else {
+ d_upper = &d_lower; // Boundaries are the same, reuse the lower
+ }
+
+ // Scale if necessary to make 1 <= (numer + delta) / denom < 10
+ ExessDigitCount count = {0, 0};
+ if (within_upper(&numer, &denom, d_upper, is_even)) {
+ count.expt = power;
+ } else {
+ count.expt = power - 1;
+ exess_bigint_multiply_u32(&numer, 10);
+ exess_bigint_multiply_u32(&d_lower, 10);
+ if (d_upper != &d_lower) {
+ exess_bigint_multiply_u32(d_upper, 10);
+ }
+ }
+
+ // Write digits to output
+ assert(check_initial_values(&numer, &denom, d_upper));
+ count.count =
+ emit_digits(&numer, &denom, &d_lower, d_upper, is_even, buf, max_digits);
+
+ // Trim trailing zeros
+ while (count.count > 1 && buf[count.count - 1] == '0') {
+ buf[--count.count] = 0;
+ }
+
+ buf[count.count] = '\0';
+ return count;
+}
diff --git a/subprojects/exess/src/digits.h b/subprojects/exess/src/digits.h
new file mode 100644
index 00000000..0753a0af
--- /dev/null
+++ b/subprojects/exess/src/digits.h
@@ -0,0 +1,38 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_DIGITS_H
+#define EXESS_DIGITS_H
+
+typedef struct {
+ unsigned count; ///< Number of digits
+ int expt; ///< Power of 10 exponent
+} ExessDigitCount;
+
+/**
+ Write significant digits digits for `d` into `buf`.
+
+ Writes only significant digits, without any leading or trailing zeros. The
+ actual number is given by the exponent in the return value.
+
+ @param d The number to convert to digits, must be finite and non-zero.
+ @param buf The output buffer at least `max_digits` long.
+ @param max_digits The maximum number of digits to write.
+*/
+ExessDigitCount
+exess_digits(double d, char* buf, unsigned max_digits);
+
+#endif // EXESS_DIGITS_H
diff --git a/subprojects/exess/src/double.c b/subprojects/exess/src/double.c
new file mode 100644
index 00000000..2934010d
--- /dev/null
+++ b/subprojects/exess/src/double.c
@@ -0,0 +1,53 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "decimal.h"
+#include "read_utils.h"
+#include "scientific.h"
+#include "strtod.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <math.h>
+#include <string.h>
+
+ExessResult
+exess_read_double(double* const out, const char* const str)
+{
+ *out = (double)NAN;
+
+ const size_t i = skip_whitespace(str);
+ ExessDecimalDouble in = {EXESS_NAN, 0u, 0, {0}};
+ const ExessResult r = parse_double(&in, str + i);
+
+ if (!r.status) {
+ *out = parsed_double_to_double(in);
+ }
+
+ return result(r.status, i + r.count);
+}
+
+ExessResult
+exess_write_double(const double value, const size_t buf_size, char* const buf)
+{
+ const ExessDecimalDouble decimal = exess_measure_double(value);
+ const ExessResult r =
+ buf ? exess_write_scientific(decimal, buf_size, buf)
+ : result(EXESS_SUCCESS, exess_scientific_string_length(decimal));
+
+ return end_write(r.status, buf_size, buf, r.count);
+}
diff --git a/subprojects/exess/src/duration.c b/subprojects/exess/src/duration.c
new file mode 100644
index 00000000..0a753fd3
--- /dev/null
+++ b/subprojects/exess/src/duration.c
@@ -0,0 +1,322 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+#include "string_utils.h"
+#include "time_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <math.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+typedef enum { YEAR, MONTH, DAY, HOUR, MINUTE, SECOND } Field;
+
+static ExessStatus
+set_field(ExessDuration* const out,
+ const Field current_field,
+ const Field field,
+ const uint32_t value)
+{
+ if (value >= INT32_MAX) {
+ return EXESS_OUT_OF_RANGE;
+ }
+
+ if (field < current_field) {
+ return EXESS_BAD_ORDER;
+ }
+
+ switch (field) {
+ case YEAR:
+ out->months = (int32_t)(12 * lrint(value));
+ break;
+ case MONTH:
+ out->months = (int32_t)(out->months + lrint(value));
+ break;
+ case DAY:
+ out->seconds = (int32_t)(24 * 60 * 60 * lrint(value));
+ break;
+ case HOUR:
+ out->seconds = (int32_t)(out->seconds + 60 * 60 * lrint(value));
+ break;
+ case MINUTE:
+ out->seconds = (int32_t)(out->seconds + 60 * lrint(value));
+ break;
+ case SECOND:
+ out->seconds = (int32_t)(out->seconds + lrint(value));
+ break;
+ }
+
+ return EXESS_SUCCESS;
+}
+
+static ExessResult
+read_date(ExessDuration* const out, const Field field, const char* const str)
+{
+ uint32_t value = 0;
+ ExessResult r = exess_read_uint(&value, str);
+ if (r.status > EXESS_EXPECTED_END) {
+ return r;
+ }
+
+ size_t i = r.count;
+ switch (str[i]) {
+ case 'Y':
+ if ((r.status = set_field(out, field, YEAR, value))) {
+ return r;
+ }
+
+ ++i;
+ if (str[i] != 'T' && !is_end(str[i])) {
+ r = read_date(out, MONTH, str + i);
+ i += r.count;
+ }
+ break;
+
+ case 'M':
+ if ((r.status = set_field(out, field, MONTH, value))) {
+ return r;
+ }
+
+ ++i;
+ if (str[i] != 'T' && !is_end(str[i])) {
+ r = read_date(out, DAY, str + i);
+ i += r.count;
+ }
+ break;
+
+ case 'D':
+ if ((r.status = set_field(out, field, DAY, value))) {
+ return r;
+ }
+
+ ++i;
+ break;
+
+ default:
+ return result(EXESS_EXPECTED_DATE_TAG, i);
+ }
+
+ return result(r.status, i);
+}
+
+static ExessResult
+read_time(ExessDuration* const out, const Field field, const char* const str)
+{
+ uint32_t value = 0;
+ ExessResult r = exess_read_uint(&value, str);
+ if (r.status > EXESS_EXPECTED_END) {
+ return r;
+ }
+
+ size_t i = r.count;
+ ExessResult next = {EXESS_SUCCESS, 0};
+ switch (str[i]) {
+ case '.': {
+ if (!is_digit(str[++i])) {
+ return result(EXESS_EXPECTED_DIGIT, i);
+ }
+
+ uint32_t nanoseconds = 0;
+
+ r = read_nanoseconds(&nanoseconds, str + i);
+ i += r.count;
+
+ if (str[i] != 'S') {
+ return result(EXESS_EXPECTED_TIME_TAG, i);
+ }
+
+ r.status = set_field(out, field, SECOND, value);
+ out->nanoseconds = (int32_t)nanoseconds;
+
+ break;
+ }
+
+ case 'H':
+ r.status = set_field(out, field, HOUR, value);
+ if (!is_end(str[i + 1])) {
+ next = read_time(out, MINUTE, str + i + 1);
+ }
+ break;
+
+ case 'M':
+ r.status = set_field(out, field, MINUTE, value);
+ if (!is_end(str[i + 1])) {
+ next = read_time(out, SECOND, str + i + 1);
+ }
+ break;
+
+ case 'S':
+ r.status = set_field(out, field, SECOND, value);
+ break;
+
+ default:
+ return result(EXESS_EXPECTED_TIME_TAG, i);
+ }
+
+ if (r.status) {
+ return r;
+ }
+
+ return result(next.status, i + 1 + next.count);
+}
+
+ExessResult
+exess_read_duration(ExessDuration* const out, const char* const str)
+{
+ memset(out, 0, sizeof(*out));
+
+ size_t i = skip_whitespace(str);
+ bool is_negative = false;
+ if (str[i] == '-') {
+ is_negative = true;
+ ++i;
+ }
+
+ if (str[i] != 'P') {
+ return result(EXESS_EXPECTED_DURATION, i);
+ }
+
+ ++i;
+
+ if (str[i] != 'T') {
+ ExessResult r = read_date(out, YEAR, str + i);
+ if (r.status) {
+ return result(r.status, i + r.count);
+ }
+
+ i += r.count;
+
+ if (!is_end(str[i]) && str[i] != 'T') {
+ return result(EXESS_EXPECTED_TIME_SEP, i);
+ }
+ }
+
+ if (str[i] == 'T') {
+ ++i;
+
+ ExessResult r = read_time(out, HOUR, str + i);
+ if (r.status) {
+ return result(r.status, i + r.count);
+ }
+
+ i += r.count;
+ }
+
+ if (is_negative) {
+ out->months = -out->months;
+ out->seconds = -out->seconds;
+ out->nanoseconds = -out->nanoseconds;
+ }
+
+ return end_read(EXESS_SUCCESS, str, i);
+}
+
+static size_t
+write_int_field(ExessResult* r,
+ const uint32_t value,
+ const char tag,
+ const size_t buf_size,
+ char* const buf,
+ const size_t i)
+{
+ if (!r->status) {
+ if (value == 0) {
+ *r = result(EXESS_SUCCESS, 0);
+ } else if (!buf) {
+ *r = exess_write_uint(value, buf_size, buf);
+ ++r->count;
+ } else {
+ *r = exess_write_uint(value, buf_size - i, buf + i);
+ if (!r->status) {
+ buf[i + r->count++] = tag;
+ }
+ }
+ }
+
+ return r->count;
+}
+
+ExessResult
+exess_write_duration(const ExessDuration value,
+ const size_t buf_size,
+ char* const buf)
+{
+ // Write zero as a special case
+ size_t i = 0;
+ if (value.months == 0 && value.seconds == 0 && value.nanoseconds == 0) {
+ i += write_string(3, "P0Y", buf_size, buf, i);
+ return end_write(EXESS_SUCCESS, buf_size, buf, i);
+ }
+
+ if (value.months == INT32_MIN || value.seconds == INT32_MIN) {
+ return end_write(EXESS_OUT_OF_RANGE, buf_size, buf, 0);
+ }
+
+ const bool is_negative =
+ (value.months < 0 || value.seconds < 0 || value.nanoseconds < 0);
+
+ if (is_negative &&
+ (value.months > 0 || value.seconds > 0 || value.nanoseconds > 0)) {
+ return end_write(EXESS_BAD_VALUE, buf_size, buf, 0);
+ }
+
+ // Write duration prefix
+ if (value.months < 0 || value.seconds < 0 || value.nanoseconds < 0) {
+ i += write_string(2, "-P", buf_size, buf, i);
+ } else {
+ i += write_char('P', buf_size, buf, i);
+ }
+
+ const uint32_t abs_years = (uint32_t)(abs(value.months) / 12);
+ const uint32_t abs_months = (uint32_t)(abs(value.months) % 12);
+ const uint32_t abs_days = (uint32_t)(abs(value.seconds) / (24 * 60 * 60));
+ const uint32_t abs_hours = (uint32_t)(abs(value.seconds) / 60 / 60 % 24);
+ const uint32_t abs_minutes = (uint32_t)(abs(value.seconds) / 60 % 60);
+ const uint8_t abs_seconds = (uint8_t)(abs(value.seconds) % 60);
+ const uint32_t abs_nanoseconds = (uint32_t)abs(value.nanoseconds);
+
+ // Write date segments if present
+ ExessResult r = result(EXESS_SUCCESS, 0);
+ i += write_int_field(&r, abs_years, 'Y', buf_size, buf, i);
+ i += write_int_field(&r, abs_months, 'M', buf_size, buf, i);
+ i += write_int_field(&r, abs_days, 'D', buf_size, buf, i);
+
+ // Write time segments if present
+ const bool has_time = abs_hours + abs_minutes + abs_seconds + abs_nanoseconds;
+ if (has_time && !r.status) {
+ i += write_char('T', buf_size, buf, i);
+ i += write_int_field(&r, abs_hours, 'H', buf_size, buf, i);
+ i += write_int_field(&r, abs_minutes, 'M', buf_size, buf, i);
+
+ if (abs_seconds != 0 || abs_nanoseconds != 0) {
+ r = write_digits(abs_seconds, buf_size, buf, i);
+ i += r.count;
+
+ if (!r.status && abs_nanoseconds > 0) {
+ i += write_nanoseconds(abs_nanoseconds, buf_size, buf, i);
+ }
+
+ i += write_char('S', buf_size, buf, i);
+ }
+ }
+
+ return end_write(r.status, buf_size, buf, i);
+}
diff --git a/subprojects/exess/src/exess_config.h b/subprojects/exess/src/exess_config.h
new file mode 100644
index 00000000..4325484a
--- /dev/null
+++ b/subprojects/exess/src/exess_config.h
@@ -0,0 +1,79 @@
+/*
+ Copyright 2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+/*
+ Configuration header that defines reasonable defaults at compile time.
+
+ This allows compile-time configuration from the command line (typically via
+ the build system) while still allowing the source to be built without any
+ configuration. The build system can define EXESS_NO_DEFAULT_CONFIG to disable
+ defaults, in which case it must define things like HAVE_FEATURE to enable
+ features. The design here ensures that compiler warnings or
+ include-what-you-use will catch any mistakes.
+*/
+
+#ifndef EXESS_CONFIG_H
+#define EXESS_CONFIG_H
+
+// Define version unconditionally so a warning will catch a mismatch
+#define EXESS_VERSION "0.0.1"
+
+#if !defined(EXESS_NO_DEFAULT_CONFIG)
+
+// GCC and clang: __builtin_clz()
+# ifndef HAVE_BUILTIN_CLZ
+# if defined(__has_builtin)
+# if __has_builtin(__builtin_clz)
+# define HAVE_BUILTIN_CLZ 1
+# else
+# define HAVE_BUILTIN_CLZ 0
+# endif
+# elif defined(__GNUC__)
+# define HAVE_BUILTIN_CLZ 1
+# else
+# define HAVE_BUILTIN_CLZ 0
+# endif
+# endif
+
+// GCC and clang: __builtin_clz()
+# ifndef HAVE_BUILTIN_CLZLL
+# if defined(__has_builtin)
+# if __has_builtin(__builtin_clzll)
+# define HAVE_BUILTIN_CLZLL 1
+# else
+# define HAVE_BUILTIN_CLZLL 0
+# endif
+# elif defined(__GNUC__)
+# define HAVE_BUILTIN_CLZLL 1
+# else
+# define HAVE_BUILTIN_CLZLL 0
+# endif
+# endif
+
+#endif // !defined(EXESS_NO_DEFAULT_CONFIG)
+
+/*
+ Make corresponding USE_FEATURE defines based on the HAVE_FEATURE defines from
+ above or the command line. The code checks for these using #if (not #ifdef),
+ so there will be an undefined warning if it checks for an unknown feature,
+ and this header is always required by any code that checks for features, even
+ if the build system defines them all.
+*/
+
+#define USE_BUILTIN_CLZ HAVE_BUILTIN_CLZ
+#define USE_BUILTIN_CLZLL HAVE_BUILTIN_CLZLL
+
+#endif // EXESS_CONFIG_H
diff --git a/subprojects/exess/src/float.c b/subprojects/exess/src/float.c
new file mode 100644
index 00000000..bcc6d49a
--- /dev/null
+++ b/subprojects/exess/src/float.c
@@ -0,0 +1,44 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "decimal.h"
+#include "read_utils.h"
+#include "scientific.h"
+
+#include "exess/exess.h"
+
+#include <math.h>
+#include <string.h>
+
+ExessResult
+exess_read_float(float* const out, const char* const str)
+{
+ double value = (double)NAN;
+ const ExessResult r = exess_read_double(&value, str);
+
+ *out = (float)value;
+
+ return r;
+}
+
+ExessResult
+exess_write_float(const float value, const size_t buf_size, char* const buf)
+{
+ const ExessDecimalDouble decimal = exess_measure_float(value);
+
+ return buf ? exess_write_scientific(decimal, buf_size, buf)
+ : result(EXESS_SUCCESS, exess_scientific_string_length(decimal));
+}
diff --git a/subprojects/exess/src/hex.c b/subprojects/exess/src/hex.c
new file mode 100644
index 00000000..bd9de4d6
--- /dev/null
+++ b/subprojects/exess/src/hex.c
@@ -0,0 +1,131 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+#include "string_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdint.h>
+#include <string.h>
+
+/// Hex encoding table
+static const char hex_map[] = "0123456789ABCDEF";
+
+static inline uint8_t
+decode_nibble(const char c)
+{
+ if (is_digit(c)) {
+ return (uint8_t)(c - '0');
+ }
+
+ if (c >= 'A' && c <= 'F') {
+ return (uint8_t)(10 + c - 'A');
+ }
+
+ if (c >= 'a' && c <= 'f') {
+ return (uint8_t)(10 + c - 'a');
+ }
+
+ return UINT8_MAX;
+}
+
+static char
+next_char(const char* const str, size_t* const i)
+{
+ *i += skip_whitespace(str + *i);
+
+ return str[*i];
+}
+
+size_t
+exess_hex_decoded_size(const size_t length)
+{
+ return length / 2;
+}
+
+ExessResult
+exess_read_hex(ExessBlob* const out, const char* const str)
+{
+ uint8_t* const uout = (uint8_t*)out->data;
+ size_t i = 0u;
+ size_t o = 0u;
+
+ while (str[i]) {
+ const char hi_char = next_char(str, &i);
+ if (!hi_char) {
+ break;
+ }
+
+ ++i;
+
+ const uint8_t hi = decode_nibble(hi_char);
+ if (hi == UINT8_MAX) {
+ return result(EXESS_EXPECTED_HEX, i);
+ }
+
+ const char lo_char = next_char(str, &i);
+ if (!lo_char) {
+ return result(EXESS_EXPECTED_HEX, i);
+ }
+
+ ++i;
+
+ const uint8_t lo = decode_nibble(lo_char);
+ if (lo == UINT8_MAX) {
+ return result(EXESS_EXPECTED_HEX, i);
+ }
+
+ if (o >= out->size) {
+ return result(EXESS_NO_SPACE, i);
+ }
+
+ uout[o++] = (uint8_t)(hi << 4u) | lo;
+ }
+
+ out->size = o;
+ return result(EXESS_SUCCESS, i);
+}
+
+ExessResult
+exess_write_hex(const size_t data_size,
+ const void* const data,
+ const size_t buf_size,
+ char* const buf)
+{
+ const size_t length = 2 * data_size;
+ if (!buf) {
+ return result(EXESS_SUCCESS, length);
+ }
+
+ if (buf_size < length + 1) {
+ return result(EXESS_NO_SPACE, 0);
+ }
+
+ const uint8_t* const in = (const uint8_t*)data;
+ size_t o = 0u;
+
+ for (size_t i = 0; i < data_size; ++i) {
+ const uint8_t hi = (in[i] & 0xF0u) >> 4u;
+ const uint8_t lo = (in[i] & 0x0Fu);
+
+ buf[o++] = hex_map[hi];
+ buf[o++] = hex_map[lo];
+ }
+
+ return end_write(EXESS_SUCCESS, buf_size, buf, o);
+}
diff --git a/subprojects/exess/src/ieee_float.h b/subprojects/exess/src/ieee_float.h
new file mode 100644
index 00000000..5c70bf4b
--- /dev/null
+++ b/subprojects/exess/src/ieee_float.h
@@ -0,0 +1,63 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_IEEE_FLOAT_H
+#define EXESS_IEEE_FLOAT_H
+
+#include <float.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+static const unsigned dbl_physical_mant_dig = DBL_MANT_DIG - 1u;
+static const uint64_t dbl_mant_mask = 0x000FFFFFFFFFFFFFull;
+static const uint64_t dbl_expt_mask = 0x7FF0000000000000ul;
+static const uint64_t dbl_hidden_bit = 0x0010000000000000ul;
+static const int dbl_expt_bias = 0x3FF + DBL_MANT_DIG - 1;
+static const int dbl_subnormal_expt = -0x3FF - DBL_MANT_DIG + 2;
+
+/// Return the raw representation of a float
+static inline uint32_t
+float_to_rep(const float d)
+{
+ uint32_t rep = 0;
+ memcpy(&rep, &d, sizeof(rep));
+ return rep;
+}
+
+/// Return the raw representation of a double
+static inline uint64_t
+double_to_rep(const double d)
+{
+ uint64_t rep = 0;
+ memcpy(&rep, &d, sizeof(rep));
+ return rep;
+}
+
+/// Return true if the lower boundary is closer than the upper boundary
+static inline bool
+double_lower_boundary_is_closer(const double d)
+{
+ const uint64_t rep = double_to_rep(d);
+ const uint64_t mant = rep & dbl_mant_mask;
+ const uint64_t expt = rep & dbl_expt_mask;
+ const bool is_subnormal = expt == 0;
+
+ // True when f = 2^(p-1) (except for the smallest normal)
+ return !is_subnormal && mant == 0;
+}
+
+#endif // EXESS_IEEE_FLOAT_H
diff --git a/subprojects/exess/src/int.c b/subprojects/exess/src/int.c
new file mode 100644
index 00000000..232cbcc4
--- /dev/null
+++ b/subprojects/exess/src/int.c
@@ -0,0 +1,45 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+exess_read_int(int32_t* const out, const char* const str)
+{
+ int64_t long_out = 0;
+ const ExessResult r = exess_read_long(&long_out, str);
+ if (r.status) {
+ return r;
+ }
+
+ if (long_out < INT32_MIN || long_out > INT32_MAX) {
+ return result(EXESS_OUT_OF_RANGE, r.count);
+ }
+
+ *out = (int32_t)long_out;
+ return r;
+}
+
+ExessResult
+exess_write_int(const int32_t value, const size_t buf_size, char* const buf)
+{
+ return exess_write_long(value, buf_size, buf);
+}
diff --git a/subprojects/exess/src/int_math.c b/subprojects/exess/src/int_math.c
new file mode 100644
index 00000000..ce263b43
--- /dev/null
+++ b/subprojects/exess/src/int_math.c
@@ -0,0 +1,78 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "int_math.h"
+
+#include "exess_config.h"
+
+#include <assert.h>
+
+unsigned
+exess_clz32(const uint32_t i)
+{
+ assert(i != 0);
+
+#if USE_BUILTIN_CLZ
+ return (unsigned)__builtin_clz(i);
+#else
+ unsigned n = 32u;
+ uint32_t bits = i;
+ for (unsigned s = 16; s > 0; s >>= 1) {
+ const uint32_t left = bits >> s;
+ if (left) {
+ n -= s;
+ bits = left;
+ }
+ }
+ return n - bits;
+#endif
+}
+
+unsigned
+exess_clz64(const uint64_t i)
+{
+ assert(i != 0);
+
+#if USE_BUILTIN_CLZLL
+ return (unsigned)__builtin_clzll(i);
+#else
+ return i & 0xFFFFFFFF00000000 ? exess_clz32((uint32_t)(i >> 32u))
+ : 32u + exess_clz32(i & 0xFFFFFFFF);
+#endif
+}
+
+uint64_t
+exess_ilog2(const uint64_t i)
+{
+ assert(i != 0);
+ return (64u - exess_clz64(i | 1u)) - 1u;
+}
+
+uint64_t
+exess_ilog10(const uint64_t i)
+{
+ // See https://graphics.stanford.edu/~seander/bithacks.html#IntegerLog10
+ const uint64_t log2 = exess_ilog2(i);
+ const uint64_t t = (log2 + 1u) * 1233u >> 12u;
+
+ return t - (i < POW10[t]) + (i == 0);
+}
+
+uint8_t
+exess_num_digits(const uint64_t i)
+{
+ return i == 0u ? 1u : (uint8_t)(exess_ilog10(i) + 1u);
+}
diff --git a/subprojects/exess/src/int_math.h b/subprojects/exess/src/int_math.h
new file mode 100644
index 00000000..a4a57140
--- /dev/null
+++ b/subprojects/exess/src/int_math.h
@@ -0,0 +1,72 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_INTMATH_H
+#define EXESS_INTMATH_H
+
+#include "attributes.h"
+
+#include <stdint.h>
+
+static const int uint64_digits10 = 19;
+
+static const uint64_t POW10[] = {1ull,
+ 10ull,
+ 100ull,
+ 1000ull,
+ 10000ull,
+ 100000ull,
+ 1000000ull,
+ 10000000ull,
+ 100000000ull,
+ 1000000000ull,
+ 10000000000ull,
+ 100000000000ull,
+ 1000000000000ull,
+ 10000000000000ull,
+ 100000000000000ull,
+ 1000000000000000ull,
+ 10000000000000000ull,
+ 100000000000000000ull,
+ 1000000000000000000ull,
+ 10000000000000000000ull};
+
+/// Return the number of leading zeros in `i`
+EXESS_I_CONST_FUNC
+unsigned
+exess_clz32(uint32_t i);
+
+/// Return the number of leading zeros in `i`
+EXESS_I_CONST_FUNC
+unsigned
+exess_clz64(uint64_t i);
+
+/// Return the log base 2 of `i`
+EXESS_I_CONST_FUNC
+uint64_t
+exess_ilog2(uint64_t i);
+
+/// Return the log base 10 of `i`
+EXESS_I_CONST_FUNC
+uint64_t
+exess_ilog10(uint64_t i);
+
+/// Return the number of decimal digits required to represent `i`
+EXESS_I_CONST_FUNC
+uint8_t
+exess_num_digits(uint64_t i);
+
+#endif // EXESS_INTMATH_H
diff --git a/subprojects/exess/src/long.c b/subprojects/exess/src/long.c
new file mode 100644
index 00000000..2fbcdf59
--- /dev/null
+++ b/subprojects/exess/src/long.c
@@ -0,0 +1,110 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "int_math.h"
+#include "read_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+exess_read_long(int64_t* const out, const char* const str)
+{
+ *out = 0;
+
+ // Read leading sign if present
+ size_t i = skip_whitespace(str);
+ int sign = 1;
+ if (str[i] == '-') {
+ sign = -1;
+ ++i;
+ } else if (str[i] == '+') {
+ ++i;
+ }
+
+ // Read digits
+ uint64_t magnitude = 0;
+ ExessResult r = exess_read_ulong(&magnitude, str + i);
+ if (r.status > EXESS_EXPECTED_END) {
+ return result(r.status, i + r.count);
+ }
+
+ i += r.count;
+
+ if (sign > 0) {
+ if (magnitude > (uint64_t)INT64_MAX) {
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ *out = (int64_t)magnitude;
+ return end_read(EXESS_SUCCESS, str, i);
+ }
+
+ const uint64_t min_magnitude = (uint64_t)(-(INT64_MIN + 1)) + 1;
+ if (magnitude > min_magnitude) {
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ if (magnitude == min_magnitude) {
+ *out = INT64_MIN;
+ } else {
+ *out = -(int64_t)magnitude;
+ }
+
+ return end_read(r.status, str, i);
+}
+
+static size_t
+exess_long_string_length(const int64_t value)
+{
+ if (value == INT64_MIN) {
+ return 20;
+ }
+
+ if (value < 0) {
+ return 1u + exess_num_digits((uint64_t)-value);
+ }
+
+ return exess_num_digits((uint64_t)value);
+}
+
+ExessResult
+exess_write_long(const int64_t value, const size_t buf_size, char* const buf)
+{
+ if (!buf) {
+ return result(EXESS_SUCCESS, exess_long_string_length(value));
+ }
+
+ if (value == INT64_MIN) {
+ return end_write(
+ EXESS_SUCCESS,
+ buf_size,
+ buf,
+ write_string(20, "-9223372036854775808", buf_size, buf, 0));
+ }
+
+ const bool is_negative = value < 0;
+ const uint64_t abs_value = (uint64_t)(is_negative ? -value : value);
+
+ size_t i = (is_negative) ? write_char('-', buf_size, buf, 0) : 0;
+ ExessResult r = write_digits(abs_value, buf_size, buf, i);
+
+ return end_write(r.status, buf_size, buf, i + r.count);
+}
diff --git a/subprojects/exess/src/macros.h b/subprojects/exess/src/macros.h
new file mode 100644
index 00000000..80ed68f5
--- /dev/null
+++ b/subprojects/exess/src/macros.h
@@ -0,0 +1,31 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_MACROS_H
+#define EXESS_MACROS_H
+
+#define MIN(x, y) ((x) < (y) ? (x) : (y))
+#define MAX(x, y) ((x) > (y) ? (x) : (y))
+#define CLAMP(x, l, h) MAX(l, MIN(h, x))
+
+#define SET_IF(pointer, value) \
+ do { \
+ if (pointer) { \
+ *(pointer) = (value); \
+ } \
+ } while (0)
+
+#endif // EXESS_MACROS_H
diff --git a/subprojects/exess/src/read_utils.c b/subprojects/exess/src/read_utils.c
new file mode 100644
index 00000000..823fe97f
--- /dev/null
+++ b/subprojects/exess/src/read_utils.c
@@ -0,0 +1,51 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+#include "string_utils.h"
+
+#include "exess/exess.h"
+
+ExessResult
+read_two_digit_number(uint8_t* const out,
+ const uint8_t min_value,
+ const uint8_t max_value,
+ const char* const str)
+{
+ size_t i = 0;
+
+ // Read digits
+ size_t d = 0;
+ for (; d < 2; ++d, ++i) {
+ if (is_digit(str[i])) {
+ *out = (uint8_t)((*out * 10) + (str[i] - '0'));
+ } else {
+ break;
+ }
+ }
+
+ // Ensure there are exactly the expected number of digits
+ if (d != 2) {
+ return result(EXESS_EXPECTED_DIGIT, i);
+ }
+
+ // Ensure value is in range
+ if (*out < min_value || *out > max_value) {
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ return result(EXESS_SUCCESS, i);
+}
diff --git a/subprojects/exess/src/read_utils.h b/subprojects/exess/src/read_utils.h
new file mode 100644
index 00000000..3f7e1f56
--- /dev/null
+++ b/subprojects/exess/src/read_utils.h
@@ -0,0 +1,77 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_READ_UTILS_H
+#define EXESS_READ_UTILS_H
+
+#include "string_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+static inline size_t
+skip_whitespace(const char* const str)
+{
+ size_t i = 0;
+ while (is_space(str[i])) {
+ ++i;
+ }
+
+ return i;
+}
+
+static inline bool
+is_end(const char c)
+{
+ switch (c) {
+ case '\0':
+ case ' ':
+ case '\f':
+ case '\n':
+ case '\r':
+ case '\t':
+ case '\v':
+ return true;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+static inline ExessResult
+result(const ExessStatus status, const size_t count)
+{
+ const ExessResult r = {status, count};
+ return r;
+}
+
+ExessResult
+read_two_digit_number(uint8_t* out,
+ uint8_t min_value,
+ uint8_t max_value,
+ const char* str);
+
+static inline ExessResult
+end_read(const ExessStatus status, const char* str, const size_t i)
+{
+ return result((status || is_end(str[i])) ? status : EXESS_EXPECTED_END, i);
+}
+
+#endif // EXESS_READ_UTILS_H
diff --git a/subprojects/exess/src/scientific.c b/subprojects/exess/src/scientific.c
new file mode 100644
index 00000000..3e441a86
--- /dev/null
+++ b/subprojects/exess/src/scientific.c
@@ -0,0 +1,125 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "scientific.h"
+#include "decimal.h"
+#include "int_math.h"
+#include "read_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+size_t
+exess_scientific_string_length(const ExessDecimalDouble value)
+{
+ switch (value.kind) {
+ case EXESS_NEGATIVE:
+ break;
+ case EXESS_NEGATIVE_INFINITY:
+ return 4;
+ case EXESS_NEGATIVE_ZERO:
+ return 6;
+ case EXESS_POSITIVE_ZERO:
+ return 5;
+ case EXESS_POSITIVE:
+ break;
+ case EXESS_POSITIVE_INFINITY:
+ case EXESS_NAN:
+ return 3;
+ }
+
+ const unsigned n_expt_digits =
+ (unsigned)exess_num_digits((unsigned)abs(value.expt));
+
+ return ((value.kind == EXESS_NEGATIVE) + // Sign
+ value.n_digits + 1 + // Digits and point
+ (value.n_digits <= 1) + // Added '0' after point
+ 1 + // 'E'
+ (value.expt < 0) + // Exponent sign
+ n_expt_digits); // Exponent digits
+}
+
+ExessResult
+exess_write_scientific(const ExessDecimalDouble value,
+ const size_t n,
+ char* const buf)
+{
+ size_t i = 0;
+
+ if (n < 4) {
+ return result(EXESS_NO_SPACE, 0);
+ }
+
+ switch (value.kind) {
+ case EXESS_NEGATIVE:
+ buf[i++] = '-';
+ break;
+ case EXESS_NEGATIVE_INFINITY:
+ return write_special(4, "-INF", n, buf);
+ case EXESS_NEGATIVE_ZERO:
+ return write_special(6, "-0.0E0", n, buf);
+ case EXESS_POSITIVE_ZERO:
+ return write_special(5, "0.0E0", n, buf);
+ case EXESS_POSITIVE:
+ break;
+ case EXESS_POSITIVE_INFINITY:
+ return write_special(3, "INF", n, buf);
+ case EXESS_NAN:
+ return write_special(3, "NaN", n, buf);
+ }
+
+ if (n - i <= value.n_digits + 1) {
+ buf[0] = '\0';
+ return result(EXESS_NO_SPACE, 0);
+ }
+
+ // Write mantissa, with decimal point after the first (normal form)
+ buf[i++] = value.digits[0];
+ buf[i++] = '.';
+ if (value.n_digits > 1) {
+ memcpy(buf + i, value.digits + 1, value.n_digits - 1);
+ i += value.n_digits - 1;
+ } else {
+ buf[i++] = '0';
+ }
+
+ // Write exponent
+
+ const unsigned n_expt_digits = exess_num_digits((unsigned)abs(value.expt));
+
+ if (n - i <= 1u + (value.expt < 0) + n_expt_digits) {
+ buf[0] = '\0';
+ return result(EXESS_NO_SPACE, 0);
+ }
+
+ buf[i++] = 'E';
+ if (value.expt < 0) {
+ buf[i++] = '-';
+ }
+
+ unsigned abs_expt = (unsigned)abs(value.expt);
+ char* s = buf + i + n_expt_digits;
+
+ *s-- = '\0';
+ do {
+ *s-- = (char)('0' + (abs_expt % 10));
+ } while ((abs_expt /= 10) > 0);
+
+ return result(EXESS_SUCCESS, i + n_expt_digits);
+}
diff --git a/subprojects/exess/src/scientific.h b/subprojects/exess/src/scientific.h
new file mode 100644
index 00000000..96c3096a
--- /dev/null
+++ b/subprojects/exess/src/scientific.h
@@ -0,0 +1,35 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_SCIENTIFIC_H
+#define EXESS_SCIENTIFIC_H
+
+#include "decimal.h"
+
+#include "exess/exess.h"
+
+#include <stddef.h>
+
+EXESS_CONST_FUNC
+size_t
+exess_scientific_string_length(ExessDecimalDouble value);
+
+ExessResult
+exess_write_scientific(ExessDecimalDouble value,
+ size_t n,
+ char* EXESS_NONNULL buf);
+
+#endif // EXESS_SCIENTIFIC_H
diff --git a/subprojects/exess/src/short.c b/subprojects/exess/src/short.c
new file mode 100644
index 00000000..9241b75a
--- /dev/null
+++ b/subprojects/exess/src/short.c
@@ -0,0 +1,45 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+exess_read_short(int16_t* const out, const char* const str)
+{
+ int64_t long_out = 0;
+ const ExessResult r = exess_read_long(&long_out, str);
+ if (r.status) {
+ return r;
+ }
+
+ if (long_out < INT16_MIN || long_out > INT16_MAX) {
+ return result(EXESS_OUT_OF_RANGE, r.count);
+ }
+
+ *out = (int16_t)long_out;
+ return r;
+}
+
+ExessResult
+exess_write_short(const int16_t value, const size_t buf_size, char* const buf)
+{
+ return exess_write_long(value, buf_size, buf);
+}
diff --git a/subprojects/exess/src/soft_float.c b/subprojects/exess/src/soft_float.c
new file mode 100644
index 00000000..bea9a176
--- /dev/null
+++ b/subprojects/exess/src/soft_float.c
@@ -0,0 +1,161 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "soft_float.h"
+
+#include "ieee_float.h"
+#include "int_math.h"
+
+#include <assert.h>
+#include <math.h>
+#include <stdint.h>
+
+/// 10^k for k = min_dec_expt, min_dec_expt + dec_expt_step, ..., max_dec_expt
+static const ExessSoftFloat soft_pow10[] = {
+ {0xFA8FD5A0081C0288, -1220}, {0xBAAEE17FA23EBF76, -1193},
+ {0x8B16FB203055AC76, -1166}, {0xCF42894A5DCE35EA, -1140},
+ {0x9A6BB0AA55653B2D, -1113}, {0xE61ACF033D1A45DF, -1087},
+ {0xAB70FE17C79AC6CA, -1060}, {0xFF77B1FCBEBCDC4F, -1034},
+ {0xBE5691EF416BD60C, -1007}, {0x8DD01FAD907FFC3C, -980},
+ {0xD3515C2831559A83, -954}, {0x9D71AC8FADA6C9B5, -927},
+ {0xEA9C227723EE8BCB, -901}, {0xAECC49914078536D, -874},
+ {0x823C12795DB6CE57, -847}, {0xC21094364DFB5637, -821},
+ {0x9096EA6F3848984F, -794}, {0xD77485CB25823AC7, -768},
+ {0xA086CFCD97BF97F4, -741}, {0xEF340A98172AACE5, -715},
+ {0xB23867FB2A35B28E, -688}, {0x84C8D4DFD2C63F3B, -661},
+ {0xC5DD44271AD3CDBA, -635}, {0x936B9FCEBB25C996, -608},
+ {0xDBAC6C247D62A584, -582}, {0xA3AB66580D5FDAF6, -555},
+ {0xF3E2F893DEC3F126, -529}, {0xB5B5ADA8AAFF80B8, -502},
+ {0x87625F056C7C4A8B, -475}, {0xC9BCFF6034C13053, -449},
+ {0x964E858C91BA2655, -422}, {0xDFF9772470297EBD, -396},
+ {0xA6DFBD9FB8E5B88F, -369}, {0xF8A95FCF88747D94, -343},
+ {0xB94470938FA89BCF, -316}, {0x8A08F0F8BF0F156B, -289},
+ {0xCDB02555653131B6, -263}, {0x993FE2C6D07B7FAC, -236},
+ {0xE45C10C42A2B3B06, -210}, {0xAA242499697392D3, -183},
+ {0xFD87B5F28300CA0E, -157}, {0xBCE5086492111AEB, -130},
+ {0x8CBCCC096F5088CC, -103}, {0xD1B71758E219652C, -77},
+ {0x9C40000000000000, -50}, {0xE8D4A51000000000, -24},
+ {0xAD78EBC5AC620000, 3}, {0x813F3978F8940984, 30},
+ {0xC097CE7BC90715B3, 56}, {0x8F7E32CE7BEA5C70, 83},
+ {0xD5D238A4ABE98068, 109}, {0x9F4F2726179A2245, 136},
+ {0xED63A231D4C4FB27, 162}, {0xB0DE65388CC8ADA8, 189},
+ {0x83C7088E1AAB65DB, 216}, {0xC45D1DF942711D9A, 242},
+ {0x924D692CA61BE758, 269}, {0xDA01EE641A708DEA, 295},
+ {0xA26DA3999AEF774A, 322}, {0xF209787BB47D6B85, 348},
+ {0xB454E4A179DD1877, 375}, {0x865B86925B9BC5C2, 402},
+ {0xC83553C5C8965D3D, 428}, {0x952AB45CFA97A0B3, 455},
+ {0xDE469FBD99A05FE3, 481}, {0xA59BC234DB398C25, 508},
+ {0xF6C69A72A3989F5C, 534}, {0xB7DCBF5354E9BECE, 561},
+ {0x88FCF317F22241E2, 588}, {0xCC20CE9BD35C78A5, 614},
+ {0x98165AF37B2153DF, 641}, {0xE2A0B5DC971F303A, 667},
+ {0xA8D9D1535CE3B396, 694}, {0xFB9B7CD9A4A7443C, 720},
+ {0xBB764C4CA7A44410, 747}, {0x8BAB8EEFB6409C1A, 774},
+ {0xD01FEF10A657842C, 800}, {0x9B10A4E5E9913129, 827},
+ {0xE7109BFBA19C0C9D, 853}, {0xAC2820D9623BF429, 880},
+ {0x80444B5E7AA7CF85, 907}, {0xBF21E44003ACDD2D, 933},
+ {0x8E679C2F5E44FF8F, 960}, {0xD433179D9C8CB841, 986},
+ {0x9E19DB92B4E31BA9, 1013}, {0xEB96BF6EBADF77D9, 1039},
+ {0xAF87023B9BF0EE6B, 1066}};
+
+ExessSoftFloat
+soft_float_from_double(const double d)
+{
+ assert(d >= 0.0);
+
+ const uint64_t rep = double_to_rep(d);
+ const uint64_t frac = rep & dbl_mant_mask;
+ const int expt = (int)((rep & dbl_expt_mask) >> dbl_physical_mant_dig);
+
+ if (expt == 0) { // Subnormal
+ ExessSoftFloat v = {frac, dbl_subnormal_expt};
+ return v;
+ }
+
+ const ExessSoftFloat v = {frac + dbl_hidden_bit, expt - dbl_expt_bias};
+ return v;
+}
+
+double
+soft_float_to_double(const ExessSoftFloat v)
+{
+ return ldexp((double)v.f, v.e);
+}
+
+ExessSoftFloat
+soft_float_normalize(ExessSoftFloat value)
+{
+ const unsigned amount = exess_clz64(value.f);
+
+ value.f <<= amount;
+ value.e -= (int)amount;
+
+ return value;
+}
+
+ExessSoftFloat
+soft_float_multiply(const ExessSoftFloat lhs, const ExessSoftFloat rhs)
+{
+ static const uint64_t mask = 0xFFFFFFFF;
+ static const uint64_t round = 1u << 31u;
+
+ const uint64_t l0 = lhs.f >> 32u;
+ const uint64_t l1 = lhs.f & mask;
+ const uint64_t r0 = rhs.f >> 32u;
+ const uint64_t r1 = rhs.f & mask;
+ const uint64_t l0r0 = l0 * r0;
+ const uint64_t l1r0 = l1 * r0;
+ const uint64_t l0r1 = l0 * r1;
+ const uint64_t l1r1 = l1 * r1;
+ const uint64_t mid = (l1r1 >> 32u) + (l0r1 & mask) + (l1r0 & mask) + round;
+
+ const ExessSoftFloat r = {l0r0 + (l0r1 >> 32u) + (l1r0 >> 32u) + (mid >> 32u),
+ lhs.e + rhs.e + 64};
+
+ return r;
+}
+
+ExessSoftFloat
+soft_float_exact_pow10(const int expt)
+{
+ static const ExessSoftFloat table[8] = {{0xA000000000000000, -60},
+ {0xC800000000000000, -57},
+ {0xFA00000000000000, -54},
+ {0x9C40000000000000, -50},
+ {0xC350000000000000, -47},
+ {0xF424000000000000, -44},
+ {0x9896800000000000, -40}};
+
+ assert(expt > 0);
+ assert(expt < dec_expt_step);
+ return table[expt - 1];
+}
+
+ExessSoftFloat
+soft_float_pow10_under(const int exponent, int* pow10_exponent)
+{
+ assert(exponent >= min_dec_expt);
+ assert(exponent < max_dec_expt + dec_expt_step);
+
+ const int cache_offset = -min_dec_expt;
+ const int index = (exponent + cache_offset) / dec_expt_step;
+
+ *pow10_exponent = min_dec_expt + index * dec_expt_step;
+
+ assert(*pow10_exponent <= exponent);
+ assert(exponent < *pow10_exponent + dec_expt_step);
+
+ return soft_pow10[index];
+}
diff --git a/subprojects/exess/src/soft_float.h b/subprojects/exess/src/soft_float.h
new file mode 100644
index 00000000..b766ae5e
--- /dev/null
+++ b/subprojects/exess/src/soft_float.h
@@ -0,0 +1,71 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_SOFT_FLOAT_H
+#define EXESS_SOFT_FLOAT_H
+
+#include "attributes.h"
+
+#include <stdint.h>
+
+typedef struct {
+ uint64_t f; ///< Significand
+ int e; ///< Exponent
+} ExessSoftFloat;
+
+static const int min_dec_expt = -348;
+static const int max_dec_expt = 340;
+static const int dec_expt_step = 8;
+
+/// Convert `d` to a soft float
+EXESS_I_CONST_FUNC
+ExessSoftFloat
+soft_float_from_double(double d);
+
+/// Convert `v` to a double
+double
+soft_float_to_double(ExessSoftFloat v);
+
+/// Normalize `value` so the MSb of its significand is 1
+EXESS_I_CONST_FUNC
+ExessSoftFloat
+soft_float_normalize(ExessSoftFloat value);
+
+/// Multiply `lhs` by `rhs` and return the result
+EXESS_I_CONST_FUNC
+ExessSoftFloat
+soft_float_multiply(ExessSoftFloat lhs, ExessSoftFloat rhs);
+
+/// Return exactly 10^e for e in [0...dec_expt_step]
+EXESS_I_CONST_FUNC
+ExessSoftFloat
+soft_float_exact_pow10(int expt);
+
+/**
+ Return a cached power of 10 with exponent not greater than `max_exponent`.
+
+ Valid only for `max_exponent` values from min_dec_expt to max_dec_expt +
+ dec_expt_step. The returned power's exponent is a multiple of
+ dec_expt_step.
+
+ @param max_exponent Maximum decimal exponent of the result.
+ @param[out] pow10_exponent Set to the decimal exponent of the result.
+ @return A cached power of 10 as a soft float.
+*/
+ExessSoftFloat
+soft_float_pow10_under(int max_exponent, int* pow10_exponent);
+
+#endif // EXESS_SOFT_FLOAT_H
diff --git a/subprojects/exess/src/strerror.c b/subprojects/exess/src/strerror.c
new file mode 100644
index 00000000..b2167cac
--- /dev/null
+++ b/subprojects/exess/src/strerror.c
@@ -0,0 +1,70 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "exess/exess.h"
+
+const char*
+exess_strerror(ExessStatus status)
+{
+ switch (status) {
+ case EXESS_SUCCESS:
+ return "Success";
+ case EXESS_EXPECTED_END:
+ return "Expected end of value";
+ case EXESS_EXPECTED_BOOLEAN:
+ return "Expected \"false\", \"true\", \"0\" or \"1\"";
+ case EXESS_EXPECTED_INTEGER:
+ return "Expected an integer value";
+ case EXESS_EXPECTED_DURATION:
+ return "Expected a duration starting with 'P'";
+ case EXESS_EXPECTED_SIGN:
+ return "Expected '-' or '+'";
+ case EXESS_EXPECTED_DIGIT:
+ return "Expected a digit";
+ case EXESS_EXPECTED_COLON:
+ return "Expected ':'";
+ case EXESS_EXPECTED_DASH:
+ return "Expected '-'";
+ case EXESS_EXPECTED_TIME_SEP:
+ return "Expected 'T'";
+ case EXESS_EXPECTED_TIME_TAG:
+ return "Expected 'H', 'M', or 'S'";
+ case EXESS_EXPECTED_DATE_TAG:
+ return "Expected 'Y', 'M', or 'D'";
+ case EXESS_EXPECTED_HEX:
+ return "Expected a hexadecimal character";
+ case EXESS_EXPECTED_BASE64:
+ return "Expected a base64 character";
+ case EXESS_BAD_ORDER:
+ return "Invalid field order";
+ case EXESS_BAD_VALUE:
+ return "Invalid value";
+ case EXESS_OUT_OF_RANGE:
+ return "Value outside valid range";
+ case EXESS_NO_SPACE:
+ return "Insufficient space";
+ case EXESS_WOULD_REDUCE_PRECISION:
+ return "Precision reducing coercion required";
+ case EXESS_WOULD_ROUND:
+ return "Rounding coercion required";
+ case EXESS_WOULD_TRUNCATE:
+ return "Truncating coercion required";
+ case EXESS_UNSUPPORTED:
+ return "Unsupported value";
+ }
+
+ return "Unknown error";
+}
diff --git a/subprojects/exess/src/string_utils.h b/subprojects/exess/src/string_utils.h
new file mode 100644
index 00000000..8acb2ffd
--- /dev/null
+++ b/subprojects/exess/src/string_utils.h
@@ -0,0 +1,74 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_STRING_UTILS_H
+#define EXESS_STRING_UTILS_H
+
+#include <stdbool.h>
+
+/// Return true if `c` lies within [`min`...`max`] (inclusive)
+static inline bool
+in_range(const int c, const int min, const int max)
+{
+ return (c >= min && c <= max);
+}
+
+/// Return true if `c` is a whitespace character
+static inline bool
+is_space(const int c)
+{
+ switch (c) {
+ case ' ':
+ case '\f':
+ case '\n':
+ case '\r':
+ case '\t':
+ case '\v':
+ return true;
+ default:
+ return false;
+ }
+}
+
+/// ALPHA ::= [A-Za-z]
+static inline bool
+is_alpha(const int c)
+{
+ return in_range(c, 'A', 'Z') || in_range(c, 'a', 'z');
+}
+
+/// DIGIT ::= [0-9]
+static inline bool
+is_digit(const int c)
+{
+ return in_range(c, '0', '9');
+}
+
+/// HEXDIG ::= DIGIT | "A" | "B" | "C" | "D" | "E" | "F"
+static inline bool
+is_hexdig(const int c)
+{
+ return is_digit(c) || in_range(c, 'A', 'F');
+}
+
+/// BASE64 ::= ALPHA | DIGIT | "+" | "/" | "="
+static inline bool
+is_base64(const int c)
+{
+ return is_alpha(c) || is_digit(c) || c == '+' || c == '/' || c == '=';
+}
+
+#endif // EXESS_STRING_UTILS_H
diff --git a/subprojects/exess/src/strtod.c b/subprojects/exess/src/strtod.c
new file mode 100644
index 00000000..55a0b082
--- /dev/null
+++ b/subprojects/exess/src/strtod.c
@@ -0,0 +1,405 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "strtod.h"
+#include "bigint.h"
+#include "decimal.h"
+#include "ieee_float.h"
+#include "int_math.h"
+#include "macros.h"
+#include "read_utils.h"
+#include "soft_float.h"
+#include "string_utils.h"
+
+#include <assert.h>
+#include <float.h>
+#include <math.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+static inline int
+read_sign(const char** const sptr)
+{
+ if (**sptr == '-') {
+ ++(*sptr);
+ return -1;
+ }
+
+ if (**sptr == '+') {
+ ++(*sptr);
+ }
+
+ return 1;
+}
+
+ExessResult
+parse_decimal(ExessDecimalDouble* const out, const char* const str)
+{
+ memset(out, 0, sizeof(*out));
+
+ // Read leading sign if necessary
+ const char* s = str;
+ const int sign = read_sign(&s);
+ int n_leading_before = 0;
+
+ out->kind = (sign < 0) ? EXESS_NEGATIVE : EXESS_POSITIVE;
+
+ // Check that the first character is valid
+ if (*s != '.' && !is_digit(*s)) {
+ return result(EXESS_EXPECTED_DIGIT, (size_t)(s - str));
+ }
+
+ // Skip leading zeros before decimal point
+ while (*s == '0') {
+ ++s;
+ ++n_leading_before;
+ }
+
+ // Skip leading zeros after decimal point
+ int n_leading_after = 0; // Zeros skipped after decimal point
+ bool after_point = false; // True if we are after the decimal point
+ if (*s == '.') {
+ after_point = true;
+ for (++s; *s == '0'; ++s) {
+ ++n_leading_after;
+ }
+ }
+
+ // Read significant digits of the mantissa into a 64-bit integer
+ uint64_t frac = 0; // Fraction value (ignoring decimal point)
+ int n_before = 0; // Number of digits before decimal point
+ int n_after = 0; // Number of digits after decimal point
+ for (; out->n_digits < DBL_DECIMAL_DIG + 1; ++s) {
+ if (is_digit(*s)) {
+ frac = (frac * 10) + (unsigned)(*s - '0');
+ n_before += !after_point;
+ n_after += after_point;
+ out->digits[out->n_digits++] = *s;
+ } else if (*s == '.' && !after_point) {
+ after_point = true;
+ } else {
+ break;
+ }
+ }
+
+ // Skip extra digits
+ int n_extra_before = 0;
+ int n_extra_after = 0;
+ for (;; ++s) {
+ if (*s == '.' && !after_point) {
+ after_point = true;
+ } else if (is_digit(*s)) {
+ n_extra_before += !after_point;
+ n_extra_after += after_point;
+ } else {
+ break;
+ }
+ }
+
+ // Calculate final output exponent
+ out->expt = n_extra_before - n_after - n_leading_after;
+
+ if (out->n_digits == 0) {
+ out->kind =
+ out->kind == EXESS_NEGATIVE ? EXESS_NEGATIVE_ZERO : EXESS_POSITIVE_ZERO;
+ }
+
+ return result(EXESS_SUCCESS, (size_t)(s - str));
+}
+
+ExessResult
+parse_double(ExessDecimalDouble* const out, const char* const str)
+{
+ memset(out, 0, sizeof(*out));
+
+ // Handle non-numeric special cases
+
+ if (!strcmp(str, "NaN")) {
+ out->kind = EXESS_NAN;
+ return result(EXESS_SUCCESS, 3u);
+ }
+
+ if (!strcmp(str, "-INF")) {
+ out->kind = EXESS_NEGATIVE_INFINITY;
+ return result(EXESS_SUCCESS, 4u);
+ }
+
+ if (!strcmp(str, "INF")) {
+ out->kind = EXESS_POSITIVE_INFINITY;
+ return result(EXESS_SUCCESS, 3u);
+ }
+
+ if (!strcmp(str, "+INF")) {
+ out->kind = EXESS_POSITIVE_INFINITY;
+ return result(EXESS_SUCCESS, 4u);
+ }
+
+ // Read mantissa as a decimal
+ const ExessResult r = parse_decimal(out, str);
+ if (r.status) {
+ return r;
+ }
+
+ const char* s = str + r.count;
+
+ // Read exponent
+ int abs_expt = 0;
+ int expt_sign = 1;
+ if (*s == 'e' || *s == 'E') {
+ ++s;
+
+ if (*s != '-' && *s != '+' && !is_digit(*s)) {
+ return result(EXESS_EXPECTED_DIGIT, (size_t)(s - str));
+ }
+
+ expt_sign = read_sign(&s);
+ while (is_digit(*s)) {
+ abs_expt = (abs_expt * 10) + (*s++ - '0');
+ }
+ }
+
+ // Calculate final output exponent
+ out->expt += expt_sign * abs_expt;
+
+ if (out->n_digits == 0) {
+ out->kind = out->kind < EXESS_POSITIVE_ZERO ? EXESS_NEGATIVE_ZERO
+ : EXESS_POSITIVE_ZERO;
+ }
+
+ return result(EXESS_SUCCESS, (size_t)(s - str));
+}
+
+static uint64_t
+normalize(ExessSoftFloat* value, const uint64_t error)
+{
+ const int original_e = value->e;
+
+ *value = soft_float_normalize(*value);
+
+ assert(value->e <= original_e);
+ return error << (unsigned)(original_e - value->e);
+}
+
+/**
+ Return the error added by floating point multiplication.
+
+ Should be l + r + l*r/(2^64) + 0.5, but we short the denominator to 63 due
+ to lack of precision, which effectively rounds up.
+*/
+static inline uint64_t
+product_error(const uint64_t lerror,
+ const uint64_t rerror,
+ const uint64_t half_ulp)
+{
+ return lerror + rerror + ((lerror * rerror) >> 63u) + half_ulp;
+}
+
+/**
+ Guess the binary floating point value for decimal input.
+
+ @param significand Significand from the input.
+ @param expt10 Decimal exponent from the input.
+ @param n_digits Number of decimal digits in the significand.
+ @param[out] guess Either the exact number, or its predecessor.
+ @return True if `guess` is correct.
+*/
+static bool
+sftod(const uint64_t significand,
+ const int expt10,
+ const int n_digits,
+ ExessSoftFloat* const guess)
+{
+ assert(expt10 <= max_dec_expt);
+ assert(expt10 >= min_dec_expt);
+
+ /* The general idea here is to try and find a power of 10 that we can
+ multiply by the significand to get the number. We get one from the
+ cache which is possibly too small, then multiply by another power of 10
+ to make up the difference if necessary. For example, with a target
+ power of 10^70, if we get 10^68 from the cache, then we multiply again
+ by 10^2. This, as well as normalization, accumulates error, which is
+ tracked throughout to know if we got the precise number. */
+
+ // Use a common denominator of 2^3 to avoid fractions
+ static const unsigned lg_denom = 3;
+ static const uint64_t denom = 1u << 3u;
+ static const uint64_t half_ulp = 4u;
+
+ // Start out with just the significand, and no error
+ ExessSoftFloat input = {significand, 0};
+ uint64_t error = normalize(&input, 0);
+
+ // Get a power of 10 that takes us most of the way without overshooting
+ int cached_expt10 = 0;
+ ExessSoftFloat pow10 = soft_float_pow10_under(expt10, &cached_expt10);
+
+ // Get an exact fixup power if necessary
+ const int d_expt10 = expt10 - cached_expt10;
+ if (d_expt10) {
+ input = soft_float_multiply(input, soft_float_exact_pow10(d_expt10));
+ if (d_expt10 > uint64_digits10 - n_digits) {
+ error += half_ulp; // Product does not fit in an integer
+ }
+ }
+
+ // Multiply the significand by the power, normalize, and update the error
+ input = soft_float_multiply(input, pow10);
+ error = normalize(&input, product_error(error, half_ulp, half_ulp));
+
+ // Get the effective number of significant bits from the order of magnitude
+ const int magnitude = 64 + input.e;
+ const int real_magnitude = magnitude - dbl_subnormal_expt;
+ const unsigned n_significant_bits =
+ (unsigned)MAX(0, MIN(real_magnitude, DBL_MANT_DIG));
+
+ // Calculate the number of "extra" bits of precision we have
+ assert(n_significant_bits <= 64);
+ unsigned n_extra_bits = 64u - n_significant_bits;
+ if (n_extra_bits + lg_denom >= 64u) {
+ // Very small subnormal where extra * denom does not fit in an integer
+ // Shift right (and accumulate some more error) to compensate
+ const unsigned amount = (n_extra_bits + lg_denom) - 63;
+
+ input.f >>= amount;
+ input.e += (int)amount;
+ error = product_error((error >> amount) + 1u, half_ulp, half_ulp);
+ n_extra_bits -= amount;
+ }
+
+ // Calculate boundaries for the extra bits (with the common denominator)
+ assert(n_extra_bits < 64);
+ const uint64_t extra_mask = (1ull << n_extra_bits) - 1u;
+ const uint64_t extra_bits = (input.f & extra_mask) * denom;
+ const uint64_t middle = (1ull << (n_extra_bits - 1u)) * denom;
+ const uint64_t low = middle - error;
+ const uint64_t high = middle + error;
+
+ // Round to nearest representable double
+ guess->f = (input.f >> n_extra_bits) + (extra_bits >= high);
+ guess->e = input.e + (int)n_extra_bits;
+
+ // Too inaccurate if the extra bits are within the error around the middle
+ return extra_bits <= low || extra_bits >= high;
+}
+
+static int
+compare_buffer(const char* buf, const int expt, const ExessSoftFloat upper)
+{
+ ExessBigint buf_bigint;
+ exess_bigint_set_decimal_string(&buf_bigint, buf);
+
+ ExessBigint upper_bigint;
+ exess_bigint_set_u64(&upper_bigint, upper.f);
+
+ if (expt >= 0) {
+ exess_bigint_multiply_pow10(&buf_bigint, (unsigned)expt);
+ } else {
+ exess_bigint_multiply_pow10(&upper_bigint, (unsigned)-expt);
+ }
+
+ if (upper.e > 0) {
+ exess_bigint_shift_left(&upper_bigint, (unsigned)upper.e);
+ } else {
+ exess_bigint_shift_left(&buf_bigint, (unsigned)-upper.e);
+ }
+
+ return exess_bigint_compare(&buf_bigint, &upper_bigint);
+}
+
+double
+parsed_double_to_double(const ExessDecimalDouble in)
+{
+ static const int n_exact_pow10 = sizeof(POW10) / sizeof(POW10[0]);
+ static const unsigned max_exact_int_digits = 15; // Digits that fit exactly
+ static const int max_decimal_power = 309; // Max finite power
+ static const int min_decimal_power = -324; // Min non-zero power
+
+ switch (in.kind) {
+ case EXESS_NEGATIVE:
+ break;
+ case EXESS_NEGATIVE_INFINITY:
+ return (double)-INFINITY;
+ case EXESS_NEGATIVE_ZERO:
+ return -0.0;
+ case EXESS_POSITIVE_ZERO:
+ return 0.0;
+ case EXESS_POSITIVE:
+ break;
+ case EXESS_POSITIVE_INFINITY:
+ return (double)INFINITY;
+ case EXESS_NAN:
+ return (double)NAN;
+ }
+
+ uint64_t frac = 0;
+ for (unsigned i = 0u; i < in.n_digits; ++i) {
+ if (is_digit(in.digits[i])) {
+ frac = (frac * 10) + (unsigned)(in.digits[i] - '0');
+ }
+ }
+
+ const int expt = in.expt;
+ const int result_power = (int)in.n_digits + expt;
+
+ // Return early for simple exact cases
+
+ const int sign = in.kind >= EXESS_POSITIVE_ZERO ? 1 : -1;
+
+ if (result_power > max_decimal_power) {
+ return sign * (double)INFINITY;
+ }
+
+ if (result_power < min_decimal_power) {
+ return sign * 0.0;
+ }
+
+ if (in.n_digits < max_exact_int_digits) {
+ if (expt < 0 && -expt < n_exact_pow10) {
+ return sign * ((double)frac / (double)POW10[-expt]);
+ }
+
+ if (expt >= 0 && expt < n_exact_pow10) {
+ return sign * ((double)frac * (double)POW10[expt]);
+ }
+ }
+
+ // Try to guess the number using only soft floating point (fast path)
+ ExessSoftFloat guess = {0, 0};
+ const bool exact = sftod(frac, expt, (int)in.n_digits, &guess);
+ const double g = soft_float_to_double(guess);
+ if (exact) {
+ return sign * g;
+ }
+
+ // Not sure, guess is either the number or its predecessor (rare slow path)
+ // Compare it with the buffer using bigints to find out which
+ const ExessSoftFloat upper = {guess.f * 2 + 1, guess.e - 1};
+ const int cmp = compare_buffer(in.digits, in.expt, upper);
+ if (cmp < 0) {
+ return sign * g;
+ }
+
+ if (cmp > 0) {
+ return sign * nextafter(g, (double)INFINITY);
+ }
+
+ if ((guess.f & 1u) == 0) {
+ return sign * g; // Round towards even
+ }
+
+ return sign * nextafter(g, (double)INFINITY); // Round odd up
+}
diff --git a/subprojects/exess/src/strtod.h b/subprojects/exess/src/strtod.h
new file mode 100644
index 00000000..7fdf7d6a
--- /dev/null
+++ b/subprojects/exess/src/strtod.h
@@ -0,0 +1,33 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_STRTOD_H
+#define EXESS_STRTOD_H
+
+#include "decimal.h"
+
+#include "exess/exess.h"
+
+ExessResult
+parse_decimal(ExessDecimalDouble* out, const char* str);
+
+ExessResult
+parse_double(ExessDecimalDouble* out, const char* str);
+
+double
+parsed_double_to_double(ExessDecimalDouble in);
+
+#endif // EXESS_STRTOD_H
diff --git a/subprojects/exess/src/time.c b/subprojects/exess/src/time.c
new file mode 100644
index 00000000..8c7bc12c
--- /dev/null
+++ b/subprojects/exess/src/time.c
@@ -0,0 +1,172 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+#include "string_utils.h"
+#include "time_utils.h"
+#include "timezone.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+ExessResult
+read_nanoseconds(uint32_t* const out, const char* const str)
+{
+ size_t i = 0;
+ char frac_digits[10] = {'0', '0', '0', '0', '0', '0', '0', '0', '0', 0};
+ for (unsigned j = 0u; j < 9u && is_digit(str[i]); ++j) {
+ frac_digits[j] = str[i++];
+ }
+
+ return result(exess_read_uint(out, frac_digits).status, i);
+}
+
+ExessResult
+exess_read_time(ExessTime* const out, const char* const str)
+{
+ memset(out, 0, sizeof(*out));
+
+ // Read hour
+ size_t i = skip_whitespace(str);
+ ExessResult r = read_two_digit_number(&out->hour, 0, 24, str + i);
+ if (r.status) {
+ return result(r.status, i + r.count);
+ }
+
+ // Read hour-minute delimiter
+ i += r.count;
+ if (str[i] != ':') {
+ return result(EXESS_EXPECTED_COLON, i);
+ }
+
+ // Read minute
+ ++i;
+ r = read_two_digit_number(&out->minute, 0, 59, str + i);
+ if (r.status) {
+ return result(r.status, i + r.count);
+ }
+
+ // Read minute-second delimiter
+ i += r.count;
+ if (str[i] != ':') {
+ return result(EXESS_EXPECTED_COLON, i);
+ }
+
+ // Read second
+ ++i;
+ r = read_two_digit_number(&out->second, 0, 59, str + i);
+ i += r.count;
+ if (r.status) {
+ return result(r.status, i);
+ }
+
+ // Read nanoseconds if present
+ if (str[i] == '.') {
+ ++i;
+ r = read_nanoseconds(&out->nanosecond, str + i);
+ i += r.count;
+ }
+
+ // Read timezone if present
+ if (!is_end(str[i])) {
+ r = exess_read_timezone(&out->zone, str + i);
+ i += r.count;
+ } else {
+ out->zone.quarter_hours = EXESS_LOCAL;
+ }
+
+ return end_read(r.status, str, i);
+}
+
+size_t
+write_nanoseconds(const uint32_t nanosecond,
+ const size_t buf_size,
+ char* const buf,
+ const size_t i)
+{
+ assert(nanosecond <= 999999999);
+
+ if (nanosecond == 0) {
+ return 0;
+ }
+
+ char frac_digits[10] = {'0', '0', '0', '0', '0', '0', '0', '0', '0', 0};
+
+ // Write digits right to left, but replace trailing zeros with null
+ uint32_t remaining = nanosecond;
+ uint32_t n_trailing = 0;
+ bool wrote_nonzero = false;
+ for (uint32_t j = 0; remaining > 0; ++j) {
+ const char digit = (char)('0' + (remaining % 10));
+ if (!wrote_nonzero && digit == '0') {
+ frac_digits[8 - j] = '\0';
+ ++n_trailing;
+ } else {
+ frac_digits[8 - j] = digit;
+ }
+
+ wrote_nonzero = wrote_nonzero || digit != '0';
+ remaining /= 10;
+ }
+
+ size_t n = write_char('.', buf_size, buf, i);
+
+ n += write_string(9 - n_trailing, frac_digits, buf_size, buf, i + n);
+
+ return n;
+}
+
+ExessResult
+write_time(const ExessTime value,
+ const size_t buf_size,
+ char* const buf,
+ const size_t offset)
+{
+ if (value.hour > 24 || value.minute > 59 || value.second > 59 ||
+ value.nanosecond > 999999999 ||
+ (value.hour == 24 &&
+ (value.minute != 0 || value.second != 0 || value.nanosecond != 0))) {
+ return result(EXESS_BAD_VALUE, 0);
+ }
+
+ size_t o = offset;
+
+ // Write integral hour, minute, and second
+ o += write_two_digit_number(value.hour, buf_size, buf, o);
+ o += write_char(':', buf_size, buf, o);
+ o += write_two_digit_number(value.minute, buf_size, buf, o);
+ o += write_char(':', buf_size, buf, o);
+ o += write_two_digit_number(value.second, buf_size, buf, o);
+ o += write_nanoseconds(value.nanosecond, buf_size, buf, o);
+
+ const ExessResult r = write_timezone(value.zone, buf_size, buf, o);
+
+ return result(r.status, o - offset + r.count);
+}
+
+ExessResult
+exess_write_time(const ExessTime value, const size_t buf_size, char* const buf)
+{
+ const ExessResult r = write_time(value, buf_size, buf, 0);
+
+ return end_write(r.status, buf_size, buf, r.count);
+}
diff --git a/subprojects/exess/src/time_utils.h b/subprojects/exess/src/time_utils.h
new file mode 100644
index 00000000..32e4b6df
--- /dev/null
+++ b/subprojects/exess/src/time_utils.h
@@ -0,0 +1,35 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_TIME_UTILS_H
+#define EXESS_TIME_UTILS_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+/// Read fractional digits as an integer number of nanoseconds
+ExessResult
+read_nanoseconds(uint32_t* out, const char* str);
+
+/// Write nanoseconds as fractional digits
+size_t
+write_nanoseconds(uint32_t nanosecond, size_t buf_size, char* buf, size_t i);
+
+/// Write a complete time with timezone suffix if necessary
+ExessResult
+write_time(ExessTime value, size_t buf_size, char* buf, size_t offset);
+
+#endif // EXESS_TIME_UTILS_H
diff --git a/subprojects/exess/src/timezone.c b/subprojects/exess/src/timezone.c
new file mode 100644
index 00000000..717f0d08
--- /dev/null
+++ b/subprojects/exess/src/timezone.c
@@ -0,0 +1,150 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "timezone.h"
+#include "date_utils.h"
+#include "read_utils.h"
+#include "string_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+exess_read_timezone(ExessTimezone* const out, const char* const str)
+{
+ out->quarter_hours = INT8_MAX;
+
+ // Start at the beginning (no whitespace skipping here)
+ size_t i = 0;
+
+ // Handle UTC special case
+ if (str[i] == 'Z') {
+ out->quarter_hours = 0;
+ return result(EXESS_SUCCESS, i + 1);
+ }
+
+ // Read leading sign (required)
+ int sign = 1;
+ switch (str[i]) {
+ case '+':
+ ++i;
+ break;
+ case '-':
+ sign = -1;
+ ++i;
+ break;
+ default:
+ return result(EXESS_EXPECTED_SIGN, i);
+ }
+
+ const char h0 = str[i];
+ if (!is_digit(h0)) {
+ return result(EXESS_EXPECTED_DIGIT, i);
+ }
+
+ const char h1 = str[++i];
+ if (!is_digit(h1)) {
+ return result(EXESS_EXPECTED_DIGIT, i);
+ }
+
+ ++i;
+
+ const int8_t hour = (int8_t)(sign * (10 * (h0 - '0') + (h1 - '0')));
+ if (hour > 14 || hour < -14) {
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ if (str[i] != ':') {
+ return result(EXESS_EXPECTED_COLON, i);
+ }
+
+ const char m0 = str[++i];
+ if (!is_digit(m0)) {
+ return result(EXESS_EXPECTED_DIGIT, i);
+ }
+
+ const char m1 = str[++i];
+ if (!is_digit(m1)) {
+ return result(EXESS_EXPECTED_DIGIT, i);
+ }
+
+ const int8_t minute = (int8_t)(sign * (10 * (m0 - '0') + (m1 - '0')));
+
+ ++i;
+
+ if (minute % 15) {
+ return result(EXESS_UNSUPPORTED, i);
+ }
+
+ if (minute > 59 || minute < -59) {
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ const int8_t quarters = (int8_t)(4 * hour + minute / 15);
+ if (quarters < -56 || quarters > 56) {
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ out->quarter_hours = quarters;
+
+ return result(EXESS_SUCCESS, i);
+}
+
+size_t
+exess_timezone_string_length(const ExessTimezone value)
+{
+ return value.quarter_hours != EXESS_LOCAL ? (value.quarter_hours == 0) ? 1 : 6
+ : 0;
+}
+
+ExessResult
+write_timezone(const ExessTimezone value,
+ const size_t buf_size,
+ char* const buf,
+ size_t o)
+{
+ if (value.quarter_hours == EXESS_LOCAL) {
+ return result(EXESS_SUCCESS, 0);
+ }
+
+ if (value.quarter_hours < -56 || value.quarter_hours > 56) {
+ return result(EXESS_BAD_VALUE, 0);
+ }
+
+ if (!buf) {
+ return result(EXESS_SUCCESS, exess_timezone_string_length(value));
+ }
+
+ if (value.quarter_hours == 0) {
+ write_char('Z', buf_size, buf, o);
+ return result(EXESS_SUCCESS, 1);
+ }
+
+ const uint8_t abs_quarters = (uint8_t)abs(value.quarter_hours);
+ const uint8_t abs_hour = abs_quarters / 4;
+ const uint8_t abs_minute = (uint8_t)(15u * (abs_quarters % 4u));
+
+ size_t n = 0;
+ n += write_char(value.quarter_hours < 0 ? '-' : '+', buf_size, buf, o + n);
+ n += write_two_digit_number(abs_hour, buf_size, buf, o + n);
+ n += write_char(':', buf_size, buf, o + n);
+ n += write_two_digit_number(abs_minute, buf_size, buf, o + n);
+
+ return result(EXESS_SUCCESS, n);
+}
diff --git a/subprojects/exess/src/timezone.h b/subprojects/exess/src/timezone.h
new file mode 100644
index 00000000..b10c2483
--- /dev/null
+++ b/subprojects/exess/src/timezone.h
@@ -0,0 +1,59 @@
+/*
+ Copyright 2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_TIMEZONE_H
+#define EXESS_TIMEZONE_H
+
+#include "exess/exess.h"
+
+#include <stddef.h>
+
+/// The maximum length of a canonical timezone string, 6
+#define EXESS_MAX_TIMEZONE_LENGTH 6
+
+/**
+ Read a timezone string after any leading whitespace.
+
+ @param out Set to the parsed value, or false on error.
+ @param str String input.
+ @return The `count` of characters read, and a `status` code.
+*/
+EXESS_API
+ExessResult
+exess_read_timezone(ExessTimezone* EXESS_NONNULL out,
+ const char* EXESS_NONNULL str);
+
+/**
+ Write a canonical timezone suffix.
+
+ The output is always in canonical form, either `Z` for UTC or a signed hour
+ and minute offset with leading zeros, like `-05:30` or `+14:00`.
+
+ @param value Value to write.
+ @param buf_size The size of `buf` in bytes.
+ @param buf Output buffer, or null to only measure.
+ @param o The current write offset in `buf`
+
+ @return #EXESS_SUCCESS on success, #EXESS_NO_SPACE if the buffer is too
+ small, or #EXESS_BAD_VALUE if the value is invalid.
+*/
+ExessResult
+write_timezone(ExessTimezone value,
+ size_t buf_size,
+ char* EXESS_NULLABLE buf,
+ size_t o);
+
+#endif // EXESS_TIMEZONE_H
diff --git a/subprojects/exess/src/ubyte.c b/subprojects/exess/src/ubyte.c
new file mode 100644
index 00000000..f520832a
--- /dev/null
+++ b/subprojects/exess/src/ubyte.c
@@ -0,0 +1,45 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+exess_read_ubyte(uint8_t* const out, const char* const str)
+{
+ uint64_t long_out = 0;
+ const ExessResult r = exess_read_ulong(&long_out, str);
+ if (r.status) {
+ return r;
+ }
+
+ if (long_out > UINT8_MAX) {
+ return result(EXESS_OUT_OF_RANGE, r.count);
+ }
+
+ *out = (uint8_t)long_out;
+ return r;
+}
+
+ExessResult
+exess_write_ubyte(const uint8_t value, const size_t buf_size, char* const buf)
+{
+ return exess_write_ulong(value, buf_size, buf);
+}
diff --git a/subprojects/exess/src/uint.c b/subprojects/exess/src/uint.c
new file mode 100644
index 00000000..dad46b48
--- /dev/null
+++ b/subprojects/exess/src/uint.c
@@ -0,0 +1,45 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+exess_read_uint(uint32_t* const out, const char* const str)
+{
+ uint64_t long_out = 0;
+ const ExessResult r = exess_read_ulong(&long_out, str);
+ if (r.status && r.status != EXESS_EXPECTED_END) {
+ return r;
+ }
+
+ if (long_out > UINT32_MAX) {
+ return result(EXESS_OUT_OF_RANGE, r.count);
+ }
+
+ *out = (uint32_t)long_out;
+ return r;
+}
+
+ExessResult
+exess_write_uint(const uint32_t value, const size_t buf_size, char* const buf)
+{
+ return exess_write_ulong(value, buf_size, buf);
+}
diff --git a/subprojects/exess/src/ulong.c b/subprojects/exess/src/ulong.c
new file mode 100644
index 00000000..3d9e00c9
--- /dev/null
+++ b/subprojects/exess/src/ulong.c
@@ -0,0 +1,94 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "exess/exess.h"
+#include "int_math.h"
+#include "read_utils.h"
+#include "string_utils.h"
+#include "write_utils.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+exess_read_ulong(uint64_t* const out, const char* const str)
+{
+ *out = 0;
+
+ // Ensure the first character is a digit
+ size_t i = skip_whitespace(str);
+ if (!is_digit(str[i])) {
+ return result(EXESS_EXPECTED_DIGIT, i);
+ }
+
+ // Skip leading zeros
+ int n_zeroes = 0;
+ while (str[i] == '0') {
+ ++i;
+ ++n_zeroes;
+ }
+
+ // Read digits
+ for (; is_digit(str[i]); ++i) {
+ const uint64_t next = (*out * 10u) + (uint64_t)(str[i] - '0');
+ if (next < *out) {
+ *out = 0;
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ *out = next;
+ }
+
+ return end_read(EXESS_SUCCESS, str, i);
+}
+
+ExessResult
+write_digits(const uint64_t value,
+ const size_t buf_size,
+ char* const buf,
+ const size_t i)
+{
+ const uint8_t n_digits = exess_num_digits(value);
+ if (!buf) {
+ return result(EXESS_SUCCESS, n_digits);
+ }
+
+ if (i + n_digits >= buf_size) {
+ return end_write(EXESS_NO_SPACE, buf_size, buf, 0);
+ }
+
+ // Point s to the end
+ char* s = buf + i + n_digits - 1u;
+
+ // Write integer part (right to left)
+ uint64_t remaining = value;
+ do {
+ *s-- = (char)('0' + (remaining % 10));
+ } while ((remaining /= 10) > 0);
+
+ return result(EXESS_SUCCESS, n_digits);
+}
+
+ExessResult
+exess_write_ulong(const uint64_t value, const size_t buf_size, char* const buf)
+{
+ if (!buf) {
+ return result(EXESS_SUCCESS, exess_num_digits(value));
+ }
+
+ const ExessResult r = write_digits(value, buf_size, buf, 0);
+ return end_write(r.status, buf_size, buf, r.count);
+}
diff --git a/subprojects/exess/src/ushort.c b/subprojects/exess/src/ushort.c
new file mode 100644
index 00000000..fc4d2c7e
--- /dev/null
+++ b/subprojects/exess/src/ushort.c
@@ -0,0 +1,45 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+exess_read_ushort(uint16_t* const out, const char* const str)
+{
+ uint64_t long_out = 0;
+ const ExessResult r = exess_read_ulong(&long_out, str);
+ if (r.status) {
+ return r;
+ }
+
+ if (long_out > UINT16_MAX) {
+ return result(EXESS_OUT_OF_RANGE, r.count);
+ }
+
+ *out = (uint16_t)long_out;
+ return r;
+}
+
+ExessResult
+exess_write_ushort(const uint16_t value, const size_t buf_size, char* const buf)
+{
+ return exess_write_ulong(value, buf_size, buf);
+}
diff --git a/subprojects/exess/src/variant.c b/subprojects/exess/src/variant.c
new file mode 100644
index 00000000..d8977abd
--- /dev/null
+++ b/subprojects/exess/src/variant.c
@@ -0,0 +1,431 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "read_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+// Constructors
+
+ExessVariant
+exess_make_nothing(const ExessStatus status)
+{
+ ExessVariant v = {EXESS_NOTHING, .value.as_status = status};
+ return v;
+}
+
+ExessVariant
+exess_make_boolean(const bool value)
+{
+ ExessVariant v = {EXESS_BOOLEAN, .value.as_bool = value};
+ return v;
+}
+
+ExessVariant
+exess_make_decimal(const double value)
+{
+ ExessVariant v = {EXESS_DECIMAL, .value.as_double = value};
+ return v;
+}
+
+ExessVariant
+exess_make_double(const double value)
+{
+ ExessVariant v = {EXESS_DOUBLE, .value.as_double = value};
+ return v;
+}
+
+ExessVariant
+exess_make_float(const float value)
+{
+ ExessVariant v = {EXESS_FLOAT, .value.as_float = value};
+ return v;
+}
+
+ExessVariant
+exess_make_long(const int64_t value)
+{
+ ExessVariant v = {EXESS_LONG, .value.as_long = value};
+ return v;
+}
+
+ExessVariant
+exess_make_int(const int32_t value)
+{
+ ExessVariant v = {EXESS_INT, .value.as_int = value};
+ return v;
+}
+
+ExessVariant
+exess_make_short(const int16_t value)
+{
+ ExessVariant v = {EXESS_SHORT, .value.as_short = value};
+ return v;
+}
+
+ExessVariant
+exess_make_byte(const int8_t value)
+{
+ ExessVariant v = {EXESS_BYTE, .value.as_byte = value};
+ return v;
+}
+
+ExessVariant
+exess_make_ulong(const uint64_t value)
+{
+ ExessVariant v = {EXESS_ULONG, .value.as_ulong = value};
+ return v;
+}
+
+ExessVariant
+exess_make_uint(const uint32_t value)
+{
+ ExessVariant v = {EXESS_UINT, .value.as_uint = value};
+ return v;
+}
+
+ExessVariant
+exess_make_ushort(const uint16_t value)
+{
+ ExessVariant v = {EXESS_USHORT, .value.as_ushort = value};
+ return v;
+}
+
+ExessVariant
+exess_make_ubyte(const uint8_t value)
+{
+ ExessVariant v = {EXESS_UBYTE, .value.as_ubyte = value};
+ return v;
+}
+
+ExessVariant
+exess_make_duration(const ExessDuration value)
+{
+ ExessVariant v = {EXESS_DURATION, .value.as_duration = value};
+ return v;
+}
+
+ExessVariant
+exess_make_datetime(const ExessDateTime value)
+{
+ ExessVariant v = {EXESS_DATETIME, .value.as_datetime = value};
+ return v;
+}
+
+ExessVariant
+exess_make_time(const ExessTime value)
+{
+ ExessVariant v = {EXESS_TIME, .value.as_time = value};
+ return v;
+}
+
+ExessVariant
+exess_make_date(const ExessDate value)
+{
+ ExessVariant v = {EXESS_DATE, .value.as_date = value};
+ return v;
+}
+
+ExessVariant
+exess_make_hex(const ExessBlob blob)
+{
+ ExessVariant v = {EXESS_HEX, .value.as_blob = blob};
+ return v;
+}
+
+ExessVariant
+exess_make_base64(const ExessBlob blob)
+{
+ ExessVariant v = {EXESS_BASE64, .value.as_blob = blob};
+ return v;
+}
+
+// Accessors
+
+ExessStatus
+exess_get_status(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_NOTHING ? variant->value.as_status
+ : EXESS_SUCCESS;
+}
+const bool*
+exess_get_boolean(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_BOOLEAN ? &variant->value.as_bool : NULL;
+}
+
+const double*
+exess_get_double(const ExessVariant* const variant)
+{
+ return (variant->datatype == EXESS_DECIMAL ||
+ variant->datatype == EXESS_DOUBLE)
+ ? &variant->value.as_double
+ : NULL;
+}
+
+const float*
+exess_get_float(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_FLOAT ? &variant->value.as_float : NULL;
+}
+
+const int64_t*
+exess_get_long(const ExessVariant* const variant)
+{
+ return (variant->datatype >= EXESS_INTEGER && variant->datatype <= EXESS_LONG)
+ ? &variant->value.as_long
+ : NULL;
+}
+
+const int32_t*
+exess_get_int(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_INT ? &variant->value.as_int : NULL;
+}
+
+const int16_t*
+exess_get_short(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_SHORT ? &variant->value.as_short : NULL;
+}
+
+const int8_t*
+exess_get_byte(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_BYTE ? &variant->value.as_byte : NULL;
+}
+
+const uint64_t*
+exess_get_ulong(const ExessVariant* const variant)
+{
+ return (variant->datatype == EXESS_NON_NEGATIVE_INTEGER ||
+ variant->datatype == EXESS_ULONG ||
+ variant->datatype == EXESS_POSITIVE_INTEGER)
+ ? &variant->value.as_ulong
+ : NULL;
+}
+
+const uint32_t*
+exess_get_uint(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_UINT ? &variant->value.as_uint : NULL;
+}
+
+const uint16_t*
+exess_get_ushort(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_USHORT ? &variant->value.as_ushort : NULL;
+}
+
+const uint8_t*
+exess_get_ubyte(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_UBYTE ? &variant->value.as_ubyte : NULL;
+}
+
+const ExessBlob*
+exess_get_blob(const ExessVariant* const variant)
+{
+ return (variant->datatype == EXESS_HEX || variant->datatype == EXESS_BASE64)
+ ? &variant->value.as_blob
+ : NULL;
+}
+
+const ExessDuration*
+exess_get_duration(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_DURATION ? &variant->value.as_duration
+ : NULL;
+}
+
+const ExessDateTime*
+exess_get_datetime(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_DATETIME ? &variant->value.as_datetime
+ : NULL;
+}
+
+const ExessTime*
+exess_get_time(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_TIME ? &variant->value.as_time : NULL;
+}
+
+const ExessDate*
+exess_get_date(const ExessVariant* const variant)
+{
+ return variant->datatype == EXESS_DATE ? &variant->value.as_date : NULL;
+}
+
+// Reading and Writing
+
+ExessResult
+exess_read_variant(ExessVariant* const out,
+ ExessDatatype datatype,
+ const char* const str)
+{
+ ExessResult r = {EXESS_UNSUPPORTED, 0};
+
+ out->datatype = datatype;
+
+ switch (datatype) {
+ case EXESS_NOTHING:
+ break;
+ case EXESS_BOOLEAN:
+ return exess_read_boolean(&out->value.as_bool, str);
+ case EXESS_DECIMAL:
+ return exess_read_decimal(&out->value.as_double, str);
+ case EXESS_DOUBLE:
+ return exess_read_double(&out->value.as_double, str);
+ case EXESS_FLOAT:
+ return exess_read_float(&out->value.as_float, str);
+ case EXESS_INTEGER:
+ return exess_read_long(&out->value.as_long, str);
+
+ case EXESS_NON_POSITIVE_INTEGER:
+ if (!(r = exess_read_long(&out->value.as_long, str)).status) {
+ if (out->value.as_long > 0) {
+ return result(EXESS_OUT_OF_RANGE, r.count);
+ }
+ }
+ break;
+
+ case EXESS_NEGATIVE_INTEGER:
+ if (!(r = exess_read_long(&out->value.as_long, str)).status) {
+ if (out->value.as_long >= 0) {
+ return result(EXESS_OUT_OF_RANGE, r.count);
+ }
+ }
+ break;
+
+ case EXESS_LONG:
+ return exess_read_long(&out->value.as_long, str);
+ case EXESS_INT:
+ return exess_read_int(&out->value.as_int, str);
+ case EXESS_SHORT:
+ return exess_read_short(&out->value.as_short, str);
+ case EXESS_BYTE:
+ return exess_read_byte(&out->value.as_byte, str);
+ case EXESS_NON_NEGATIVE_INTEGER:
+ case EXESS_ULONG:
+ return exess_read_ulong(&out->value.as_ulong, str);
+ case EXESS_UINT:
+ return exess_read_uint(&out->value.as_uint, str);
+ case EXESS_USHORT:
+ return exess_read_ushort(&out->value.as_ushort, str);
+ case EXESS_UBYTE:
+ return exess_read_ubyte(&out->value.as_ubyte, str);
+
+ case EXESS_POSITIVE_INTEGER:
+ if (!(r = exess_read_ulong(&out->value.as_ulong, str)).status) {
+ if (out->value.as_ulong <= 0) {
+ return result(EXESS_OUT_OF_RANGE, r.count);
+ }
+ }
+ break;
+
+ case EXESS_DURATION:
+ return exess_read_duration(&out->value.as_duration, str);
+ case EXESS_DATETIME:
+ return exess_read_datetime(&out->value.as_datetime, str);
+ case EXESS_TIME:
+ return exess_read_time(&out->value.as_time, str);
+ case EXESS_DATE:
+ return exess_read_date(&out->value.as_date, str);
+ case EXESS_HEX:
+ return exess_read_hex(&out->value.as_blob, str);
+ case EXESS_BASE64:
+ return exess_read_base64(&out->value.as_blob, str);
+ }
+
+ return r;
+}
+
+ExessResult
+exess_write_variant(const ExessVariant variant,
+ const size_t buf_size,
+ char* const buf)
+{
+ if (buf_size > 0) {
+ buf[0] = '\0';
+ }
+
+ switch (variant.datatype) {
+ case EXESS_NOTHING:
+ break;
+ case EXESS_BOOLEAN:
+ return exess_write_boolean(variant.value.as_bool, buf_size, buf);
+ case EXESS_DECIMAL:
+ return exess_write_decimal(variant.value.as_double, buf_size, buf);
+ case EXESS_DOUBLE:
+ return exess_write_double(variant.value.as_double, buf_size, buf);
+ case EXESS_FLOAT:
+ return exess_write_float(variant.value.as_float, buf_size, buf);
+ case EXESS_INTEGER:
+ case EXESS_NON_POSITIVE_INTEGER:
+ case EXESS_NEGATIVE_INTEGER:
+ case EXESS_LONG:
+ return exess_write_long(variant.value.as_long, buf_size, buf);
+ case EXESS_INT:
+ return exess_write_int(variant.value.as_int, buf_size, buf);
+ case EXESS_SHORT:
+ return exess_write_short(variant.value.as_short, buf_size, buf);
+ case EXESS_BYTE:
+ return exess_write_byte(variant.value.as_byte, buf_size, buf);
+ case EXESS_NON_NEGATIVE_INTEGER:
+ case EXESS_ULONG:
+ return exess_write_ulong(variant.value.as_ulong, buf_size, buf);
+ case EXESS_UINT:
+ return exess_write_uint(variant.value.as_uint, buf_size, buf);
+ case EXESS_USHORT:
+ return exess_write_ushort(variant.value.as_ushort, buf_size, buf);
+ case EXESS_UBYTE:
+ return exess_write_ubyte(variant.value.as_ubyte, buf_size, buf);
+ case EXESS_POSITIVE_INTEGER:
+ return exess_write_ulong(variant.value.as_ulong, buf_size, buf);
+ case EXESS_DURATION:
+ return exess_write_duration(variant.value.as_duration, buf_size, buf);
+ case EXESS_DATETIME:
+ return exess_write_datetime(variant.value.as_datetime, buf_size, buf);
+ case EXESS_TIME:
+ return exess_write_time(variant.value.as_time, buf_size, buf);
+ case EXESS_DATE:
+ return exess_write_date(variant.value.as_date, buf_size, buf);
+ case EXESS_HEX:
+ if (variant.value.as_blob.data) {
+ return exess_write_hex(variant.value.as_blob.size,
+ (void*)variant.value.as_blob.data,
+ buf_size,
+ buf);
+ }
+ break;
+ case EXESS_BASE64:
+ if (variant.value.as_blob.data) {
+ return exess_write_base64(variant.value.as_blob.size,
+ (void*)variant.value.as_blob.data,
+ buf_size,
+ buf);
+ }
+ break;
+ }
+
+ return end_write(EXESS_BAD_VALUE, buf_size, buf, 0);
+}
diff --git a/subprojects/exess/src/warnings.h b/subprojects/exess/src/warnings.h
new file mode 100644
index 00000000..2b1f6587
--- /dev/null
+++ b/subprojects/exess/src/warnings.h
@@ -0,0 +1,46 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_WARNINGS_H
+#define EXESS_WARNINGS_H
+
+#if defined(__clang__)
+
+# define EXESS_DISABLE_CONVERSION_WARNINGS \
+ _Pragma("clang diagnostic push") \
+ _Pragma("clang diagnostic ignored \"-Wconversion\"") \
+ _Pragma("clang diagnostic ignored \"-Wdouble-promotion\"")
+
+# define EXESS_RESTORE_WARNINGS _Pragma("clang diagnostic pop")
+
+#elif defined(__GNUC__)
+
+# define EXESS_DISABLE_CONVERSION_WARNINGS \
+ _Pragma("GCC diagnostic push") \
+ _Pragma("GCC diagnostic ignored \"-Wconversion\"") \
+ _Pragma("GCC diagnostic ignored \"-Wfloat-conversion\"") \
+ _Pragma("GCC diagnostic ignored \"-Wdouble-promotion\"")
+
+# define EXESS_RESTORE_WARNINGS _Pragma("GCC diagnostic pop")
+
+#else
+
+# define EXESS_DISABLE_CONVERSION_WARNINGS
+# define EXESS_RESTORE_WARNINGS
+
+#endif
+
+#endif // EXESS_WARNINGS_H
diff --git a/subprojects/exess/src/write_utils.c b/subprojects/exess/src/write_utils.c
new file mode 100644
index 00000000..fdac0754
--- /dev/null
+++ b/subprojects/exess/src/write_utils.c
@@ -0,0 +1,50 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "write_utils.h"
+#include "read_utils.h"
+
+#include "exess/exess.h"
+
+#include <string.h>
+
+size_t
+write_two_digit_number(const uint8_t value,
+ const size_t buf_size,
+ char* const buf,
+ const size_t i)
+{
+ if (buf_size >= i + 1) {
+ buf[i] = (char)((value >= 10) ? ('0' + value / 10) : '0');
+ buf[i + 1] = (char)('0' + (value % 10));
+ }
+
+ return 2;
+}
+
+ExessResult
+write_special(const size_t string_length,
+ const char* const string,
+ const size_t buf_size,
+ char* const buf)
+{
+ if (buf_size < string_length + 1) {
+ return end_write(EXESS_NO_SPACE, buf_size, buf, 0);
+ }
+
+ memcpy(buf, string, string_length + 1);
+ return result(EXESS_SUCCESS, string_length);
+}
diff --git a/subprojects/exess/src/write_utils.h b/subprojects/exess/src/write_utils.h
new file mode 100644
index 00000000..ea941743
--- /dev/null
+++ b/subprojects/exess/src/write_utils.h
@@ -0,0 +1,85 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef EXESS_WRITE_UTILS_H
+#define EXESS_WRITE_UTILS_H
+
+#include "exess/exess.h"
+
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+static inline size_t
+write_char(const char c, size_t buf_size, char* const buf, const size_t i)
+{
+ if (buf && buf_size >= i + 1) {
+ buf[i] = c;
+ }
+
+ return 1;
+}
+
+static inline size_t
+write_string(const size_t len,
+ const char* str,
+ const size_t buf_size,
+ char* const buf,
+ const size_t i)
+{
+ if (buf && buf_size >= i + len + 1) {
+ memcpy(buf + i, str, len);
+ buf[i + len] = 0;
+ }
+
+ return len;
+}
+
+static inline ExessResult
+end_write(const ExessStatus status,
+ const size_t buf_size,
+ char* const buf,
+ const size_t i)
+{
+ ExessResult r = {status, status > EXESS_EXPECTED_END ? 0 : i};
+
+ if (buf) {
+ if (!status && i >= buf_size) {
+ r.status = EXESS_NO_SPACE;
+ r.count = 0;
+ }
+
+ if (r.count < buf_size) {
+ buf[r.count] = '\0';
+ }
+ }
+
+ return r;
+}
+
+ExessResult
+write_digits(uint64_t value, size_t buf_size, char* buf, size_t i);
+
+size_t
+write_two_digit_number(uint8_t value, size_t buf_size, char* buf, size_t i);
+
+ExessResult
+write_special(size_t string_length,
+ const char* string,
+ size_t buf_size,
+ char* buf);
+
+#endif // EXESS_WRITE_UTILS_H
diff --git a/subprojects/exess/src/year.c b/subprojects/exess/src/year.c
new file mode 100644
index 00000000..e268ff80
--- /dev/null
+++ b/subprojects/exess/src/year.c
@@ -0,0 +1,98 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "date_utils.h"
+#include "int_math.h"
+#include "macros.h"
+#include "read_utils.h"
+#include "write_utils.h"
+
+#include "exess/exess.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+ExessResult
+read_year_number(int16_t* const out, const char* const str)
+{
+ *out = 0;
+
+ // Read leading sign if present
+ size_t i = 0;
+ int sign = 1;
+ if (str[i] == '-') {
+ sign = -1;
+ ++i;
+ }
+
+ // Read digits
+ uint64_t magnitude = 0;
+ ExessResult r = exess_read_ulong(&magnitude, str + i);
+ if (r.status > EXESS_EXPECTED_END) {
+ return result(r.status, i + r.count);
+ }
+
+ i += r.count;
+
+ if (sign > 0) {
+ if (magnitude > (uint16_t)INT16_MAX) {
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ *out = (int16_t)magnitude;
+ } else {
+ const uint16_t min_magnitude = (uint16_t)(-(INT16_MIN + 1)) + 1;
+ if (magnitude > min_magnitude) {
+ return result(EXESS_OUT_OF_RANGE, i);
+ }
+
+ if (magnitude == min_magnitude) {
+ *out = INT16_MIN;
+ } else {
+ *out = (int16_t)-magnitude;
+ }
+ }
+
+ return result(r.count >= 4 ? EXESS_SUCCESS : EXESS_EXPECTED_DIGIT, i);
+}
+
+ExessResult
+write_year_number(const int16_t value, const size_t buf_size, char* const buf)
+{
+ const uint32_t abs_year = (uint32_t)abs(value);
+ const uint8_t n_digits = exess_num_digits(abs_year);
+ const bool is_negative = value < 0;
+
+ if (!buf) {
+ return result(EXESS_SUCCESS, is_negative + MAX(4u, n_digits));
+ }
+
+ // Write sign
+ size_t i = 0;
+ if (is_negative) {
+ i += write_char('-', buf_size, buf, i);
+ }
+
+ // Write leading zeros to ensure we have at least 4 year digits
+ for (size_t j = n_digits; j < 4; ++j) {
+ i += write_char('0', buf_size, buf, i);
+ }
+
+ const ExessResult yr = exess_write_uint(abs_year, buf_size - i, buf + i);
+
+ return end_write(yr.status, buf_size, buf, i + yr.count);
+}
diff --git a/subprojects/exess/test/.clang-tidy b/subprojects/exess/test/.clang-tidy
new file mode 100644
index 00000000..98645dc0
--- /dev/null
+++ b/subprojects/exess/test/.clang-tidy
@@ -0,0 +1,11 @@
+Checks: >
+ *,
+ -*-magic-numbers,
+ -*-uppercase-literal-suffix,
+ -clang-analyzer-nullability.NullableDereferenced,
+ -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
+ -llvm-header-guard,
+ -llvmlibc-*,
+WarningsAsErrors: '*'
+HeaderFilterRegex: '.*'
+FormatStyle: file
diff --git a/subprojects/exess/test/float_test_data.h b/subprojects/exess/test/float_test_data.h
new file mode 100644
index 00000000..eaea65c4
--- /dev/null
+++ b/subprojects/exess/test/float_test_data.h
@@ -0,0 +1,130 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "attributes.h"
+#include "ieee_float.h"
+#include "warnings.h"
+
+#include <assert.h>
+#include <math.h>
+#include <stdint.h>
+#include <string.h>
+
+/// Return the float with representation `rep`
+static inline float
+float_from_rep(const uint32_t rep)
+{
+ float f = 0.0f;
+ memcpy(&f, &rep, sizeof(f));
+ return f;
+}
+
+/// Return the double with representation `rep`
+static inline double
+double_from_rep(const uint64_t rep)
+{
+ double d = 0.0;
+ memcpy(&d, &rep, sizeof(d));
+ return d;
+}
+
+/// Return the distance between two doubles in ULPs
+static EXESS_I_PURE_FUNC uint64_t
+double_ulp_distance(const double a, const double b)
+{
+ assert(a >= 0.0);
+ assert(b >= 0.0);
+
+ const uint64_t ia = double_to_rep(a);
+ const uint64_t ib = double_to_rep(b);
+ if (ia == ib) {
+ return 0;
+ }
+
+ EXESS_DISABLE_CONVERSION_WARNINGS
+ if (isnan(a) || isnan(b) || isinf(a) || isinf(b)) {
+ return UINT64_MAX;
+ }
+ EXESS_RESTORE_WARNINGS
+
+ return ia > ib ? ia - ib : ib - ia;
+}
+
+/// Return the distance between two floats in ULPs
+static EXESS_I_PURE_FUNC uint32_t
+float_ulp_distance(const float a, const float b)
+{
+ assert(a >= 0.0f);
+ assert(b >= 0.0f);
+
+ const uint32_t ia = float_to_rep(a);
+ const uint32_t ib = float_to_rep(b);
+ if (ia == ib) {
+ return 0;
+ }
+
+ EXESS_DISABLE_CONVERSION_WARNINGS
+ if (isnan(a) || isnan(b) || isinf(a) || isinf(b)) {
+ return UINT32_MAX;
+ }
+ EXESS_RESTORE_WARNINGS
+
+ return ia > ib ? ia - ib : ib - ia;
+}
+
+static inline bool
+float_matches(const float a, const float b)
+{
+ EXESS_DISABLE_CONVERSION_WARNINGS
+ const bool a_is_nan = isnan(a);
+ const bool a_is_negative = signbit(a);
+ const bool b_is_nan = isnan(b);
+ const bool b_is_negative = signbit(b);
+ EXESS_RESTORE_WARNINGS
+
+ if (a_is_nan && b_is_nan) {
+ return true;
+ }
+
+ if (a_is_nan || b_is_nan || a_is_negative != b_is_negative) {
+ return false;
+ }
+
+ return a_is_negative ? float_ulp_distance(-a, -b) == 0
+ : float_ulp_distance(a, b) == 0;
+}
+
+static inline bool
+double_matches(const double a, const double b)
+{
+ EXESS_DISABLE_CONVERSION_WARNINGS
+ const bool a_is_nan = isnan(a);
+ const bool a_is_negative = signbit(a);
+ const bool b_is_nan = isnan(b);
+ const bool b_is_negative = signbit(b);
+ EXESS_RESTORE_WARNINGS
+
+ if (a_is_nan && b_is_nan) {
+ return true;
+ }
+
+ if (a_is_nan || b_is_nan || a_is_negative != b_is_negative) {
+ return false;
+ }
+
+ return a_is_negative ? double_ulp_distance(-a, -b) == 0
+ : double_ulp_distance(a, b) == 0;
+}
diff --git a/subprojects/exess/test/int_test_data.h b/subprojects/exess/test/int_test_data.h
new file mode 100644
index 00000000..978f863b
--- /dev/null
+++ b/subprojects/exess/test/int_test_data.h
@@ -0,0 +1,42 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#ifndef INT_TEST_DATA_H
+#define INT_TEST_DATA_H
+
+#include <stdint.h>
+
+/// Linear Congruential Generator for making random 32-bit integers
+static inline uint32_t
+lcg32(const uint32_t i)
+{
+ static const uint32_t a = 134775813u;
+ static const uint32_t c = 1u;
+
+ return (a * i) + c;
+}
+
+/// Linear Congruential Generator for making random 64-bit integers
+static inline uint64_t
+lcg64(const uint64_t i)
+{
+ static const uint64_t a = 6364136223846793005ull;
+ static const uint64_t c = 1ull;
+
+ return (a * i) + c;
+}
+
+#endif // INT_TEST_DATA_H
diff --git a/subprojects/exess/test/meson.build b/subprojects/exess/test/meson.build
new file mode 100644
index 00000000..972793f0
--- /dev/null
+++ b/subprojects/exess/test/meson.build
@@ -0,0 +1,76 @@
+autoship = find_program('autoship', required: false)
+
+test_args = []
+
+if get_option('strict')
+ if cc.get_id() == 'clang'
+ test_args = [
+ '-Wno-float-equal',
+ ]
+ elif cc.get_id() == 'gcc'
+ test_args = [
+ '-Wno-float-equal',
+ '-Wno-suggest-attribute=pure',
+ ]
+ elif cc.get_id() == 'msvc'
+ test_args = [
+ '/wd4996', # POSIX name is deprecated
+ ]
+ endif
+endif
+
+private_tests = [
+ 'bigint',
+ 'int_math',
+]
+
+foreach unit : private_tests
+ test(unit,
+ executable('test_@0@'.format(unit),
+ 'test_@0@.c'.format(unit),
+ c_args: exess_c_args + prog_args + test_args,
+ dependencies: exess_static_dep,
+ include_directories: include_directories('../src')),
+ suite: 'private')
+endforeach
+
+public_tests = [
+ 'base64',
+ 'boolean',
+ 'byte',
+ 'canonical',
+ 'coerce',
+ 'datatype',
+ 'date',
+ 'datetime',
+ 'decimal',
+ 'double',
+ 'duration',
+ 'float',
+ 'hex',
+ 'int',
+ 'long',
+ 'short',
+ 'strerror',
+ 'time',
+ 'timezone',
+ 'ubyte',
+ 'uint',
+ 'ulong',
+ 'ushort',
+ 'variant',
+]
+
+foreach unit : public_tests
+ test(unit,
+ executable('test_@0@'.format(unit),
+ 'test_@0@.c'.format(unit),
+ c_args: exess_c_args + prog_args + test_args,
+ dependencies: [m_dep, exess_dep],
+ include_directories: include_directories('../src')),
+ suite: 'public')
+endforeach
+
+if autoship.found()
+ test('autoship', autoship, args: ['test', exess_src_root], suite: 'data')
+endif
diff --git a/subprojects/exess/test/num_test_utils.h b/subprojects/exess/test/num_test_utils.h
new file mode 100644
index 00000000..ca29a479
--- /dev/null
+++ b/subprojects/exess/test/num_test_utils.h
@@ -0,0 +1,85 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <time.h>
+
+#ifdef _WIN32
+# include <process.h>
+#else
+# include <unistd.h>
+#endif
+
+typedef struct {
+ size_t n_tests;
+ uint32_t seed;
+ bool exhaustive;
+ bool error;
+} ExessNumTestOptions;
+
+static bool
+print_num_test_usage(const char* const name)
+{
+ fprintf(stderr, "Usage: %s [OPTION]...\n", name);
+ fprintf(stderr, " -n NUM_TESTS Number of random tests to run.\n");
+ fprintf(stderr, " -s SEED Use random seed.\n");
+ fprintf(stderr, " -x Exhaustively test numbers.\n");
+ return true;
+}
+
+static ExessNumTestOptions
+parse_num_test_options(const int argc, char* const* const argv)
+{
+ ExessNumTestOptions opts = {
+ 16384u, (uint32_t)time(NULL) + (uint32_t)getpid(), false, false};
+
+ int a = 1;
+ for (; a < argc && argv[a][0] == '-'; ++a) {
+ if (argv[a][1] == 'x') {
+ opts.exhaustive = true;
+ } else if (argv[a][1] == 's') {
+ if (++a == argc) {
+ opts.error = print_num_test_usage(argv[0]);
+ break;
+ }
+
+ opts.seed = (uint32_t)strtol(argv[a], NULL, 10);
+ } else if (argv[a][1] == 'n') {
+ if (++a == argc) {
+ opts.error = print_num_test_usage(argv[0]);
+ break;
+ }
+
+ opts.n_tests = (uint32_t)strtol(argv[a], NULL, 10);
+ } else {
+ opts.error = print_num_test_usage(argv[0]);
+ break;
+ }
+ }
+
+ return opts;
+}
+
+static void
+print_num_test_progress(const uint64_t i, const uint64_t n_tests)
+{
+ if (i % (n_tests / 20) == 1) {
+ fprintf(stderr, "%f%%\n", (double)i / (double)n_tests * 100.0);
+ }
+}
diff --git a/subprojects/exess/test/test_base64.c b/subprojects/exess/test/test_base64.c
new file mode 100644
index 00000000..760f44fe
--- /dev/null
+++ b/subprojects/exess/test/test_base64.c
@@ -0,0 +1,196 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const size_t expected_value_length,
+ const char* const expected_value,
+ const size_t expected_value_size,
+ const size_t expected_count)
+{
+ char buf[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
+ ExessBlob blob = {9, buf};
+
+ ExessResult r = exess_read_base64(&blob, string);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(r.status || blob.size == expected_value_size);
+ if (expected_value_length > 0) {
+ assert(!strncmp(buf, expected_value, expected_value_length));
+ assert(blob.size <= exess_base64_decoded_size(strlen(string)));
+ }
+}
+
+static void
+test_rfc4648_cases(void)
+{
+ char buf[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+ assert(!exess_write_base64(6, "foobar", sizeof(buf), buf).status);
+ assert(!strcmp(buf, "Zm9vYmFy"));
+
+ assert(!exess_write_base64(5, "fooba", sizeof(buf), buf).status);
+ assert(!strcmp(buf, "Zm9vYmE="));
+
+ assert(!exess_write_base64(4, "foob", sizeof(buf), buf).status);
+ assert(!strcmp(buf, "Zm9vYg=="));
+
+ assert(!exess_write_base64(3, "foo", sizeof(buf), buf).status);
+ assert(!strcmp(buf, "Zm9v"));
+
+ assert(!exess_write_base64(2, "fo", sizeof(buf), buf).status);
+ assert(!strcmp(buf, "Zm8="));
+
+ assert(!exess_write_base64(1, "f", sizeof(buf), buf).status);
+ assert(!strcmp(buf, "Zg=="));
+}
+
+static void
+test_whitespace(void)
+{
+ check_read("Zm9vYmFy", EXESS_SUCCESS, 6, "foobar", 6, 8);
+ check_read(" Zm9vYmFy", EXESS_SUCCESS, 6, "foobar", 6, 9);
+ check_read("Z\fm9vYmFy", EXESS_SUCCESS, 6, "foobar", 6, 9);
+ check_read("Zm\n9vYmFy", EXESS_SUCCESS, 6, "foobar", 6, 9);
+ check_read("Zm9\rvYmFy", EXESS_SUCCESS, 6, "foobar", 6, 9);
+ check_read("Zm9v\tYmFy", EXESS_SUCCESS, 6, "foobar", 6, 9);
+ check_read("Zm9vY\vmFy", EXESS_SUCCESS, 6, "foobar", 6, 9);
+ check_read(" \f\n\r\t\vZm9vYmFy", EXESS_SUCCESS, 6, "foobar", 6, 14);
+ check_read("Zm9vYmFy \f\n\r\t\v", EXESS_SUCCESS, 6, "foobar", 6, 14);
+}
+
+static void
+test_syntax_errors(void)
+{
+ check_read("Z", EXESS_EXPECTED_BASE64, 0, NULL, 0, 1);
+ check_read("ZZ", EXESS_EXPECTED_BASE64, 0, NULL, 0, 2);
+ check_read("ZZZ", EXESS_EXPECTED_BASE64, 0, NULL, 0, 3);
+
+ check_read("=ZZZ", EXESS_BAD_VALUE, 0, NULL, 0, 4);
+ check_read("Z=ZZ", EXESS_BAD_VALUE, 0, NULL, 0, 4);
+ check_read("ZZ=Z", EXESS_BAD_VALUE, 0, NULL, 0, 4);
+
+ check_read("!m9vYmFy", EXESS_EXPECTED_BASE64, 0, NULL, 0, 0);
+ check_read("Z!9vYmFy", EXESS_EXPECTED_BASE64, 0, NULL, 0, 1);
+ check_read("Zm!vYmFy", EXESS_EXPECTED_BASE64, 0, NULL, 0, 2);
+ check_read("Zm9!YmFy", EXESS_EXPECTED_BASE64, 0, NULL, 0, 3);
+ check_read("Zm9v!mFy", EXESS_EXPECTED_BASE64, 0, NULL, 3, 4);
+ check_read("Zm9vY!Fy", EXESS_EXPECTED_BASE64, 0, NULL, 3, 5);
+ check_read("Zm9vYm!y", EXESS_EXPECTED_BASE64, 0, NULL, 3, 6);
+ check_read("Zm9vYmF!", EXESS_EXPECTED_BASE64, 0, NULL, 3, 7);
+}
+
+static void
+test_read_overflow(void)
+{
+ char buf[3] = {0, 0, 0};
+ ExessBlob blob0 = {0, buf};
+ ExessBlob blob1 = {1, buf};
+ ExessBlob blob2 = {2, buf};
+ ExessBlob blob3 = {3, buf};
+
+ ExessResult r = exess_read_base64(&blob0, "Zm9v");
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 4);
+ assert(blob0.size == 0);
+
+ r = exess_read_base64(&blob1, "Zm9v");
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 4);
+ assert(!buf[0]);
+
+ r = exess_read_base64(&blob2, "Zm9v");
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 4);
+ assert(!buf[0]);
+
+ r = exess_read_base64(&blob3, "Zm9v");
+ assert(r.status == EXESS_SUCCESS);
+ assert(r.count == 4);
+ assert(blob3.size == 3);
+ assert(!strncmp(buf, "foo", 3));
+}
+
+static void
+test_write_overflow(void)
+{
+ char buf[5] = {1, 2, 3, 4, 5};
+
+ assert(exess_write_base64(3, "foo", 0, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_base64(3, "foo", 1, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_base64(3, "foo", 2, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_base64(3, "foo", 3, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_base64(3, "foo", 4, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_base64(3, "foo", 5, buf).status == EXESS_SUCCESS);
+}
+
+static void
+test_round_trip(void)
+{
+ for (size_t size = 1; size < 256; ++size) {
+ // Allocate and generate data
+ uint8_t* const data = (uint8_t*)malloc(size);
+ for (size_t i = 0; i < size; ++i) {
+ data[i] = (uint8_t)((size + i) % 256);
+ }
+
+ // Allocate buffer for encoding with minimum required size
+ const size_t str_len = exess_write_base64(size, data, 0, NULL).count;
+ char* const str = (char*)malloc(str_len + 1);
+
+ // Encode data to string buffer
+ assert(!exess_write_base64(size, data, str_len + 1, str).status);
+ assert(strlen(str) == str_len);
+ assert(str_len % 4 == 0);
+
+ // Allocate buffer for decoded data with the same size as the input
+ uint8_t* const decoded = (uint8_t*)malloc(size);
+ ExessBlob decoded_blob = {size, decoded};
+
+ // Decode and check that data matches the original input
+ assert(!exess_read_base64(&decoded_blob, str).status);
+ assert(decoded_blob.size == size);
+ assert(!memcmp(decoded, data, size));
+
+ free(decoded);
+ free(str);
+ free(data);
+ }
+}
+
+int
+main(void)
+{
+ test_rfc4648_cases();
+ test_whitespace();
+ test_syntax_errors();
+ test_read_overflow();
+ test_write_overflow();
+ test_round_trip();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_bigint.c b/subprojects/exess/test/test_bigint.c
new file mode 100644
index 00000000..3a74bb92
--- /dev/null
+++ b/subprojects/exess/test/test_bigint.c
@@ -0,0 +1,841 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "bigint.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+/* Some test data borrowed from http://github.com/google/double-conversion
+ which uses a completely different bigint representation (so if these agree,
+ everything is probably fine). Others cases are either made up to hit the
+ edges of the implementation, or interesting cases collected from testing the
+ decimal implementation. Almost everything here uses the hex representation
+ so it is easy to dump into Python as a sanity check. */
+
+static ExessBigint
+bigint_from_hex(const char* str)
+{
+ ExessBigint num;
+ exess_bigint_set_hex_string(&num, str);
+ return num;
+}
+
+static bool
+check_hex_equals(const char* const str, const ExessBigint* const num)
+{
+ const ExessBigint expected = bigint_from_hex(str);
+
+ return exess_bigint_compare(&expected, num) == 0;
+}
+
+#define CHECK_HEXEQ(str, num) assert(check_hex_equals(str, num))
+
+#define CHECK_SET(setter, value, expected) \
+ do { \
+ ExessBigint num; \
+ exess_bigint_set_##setter(&num, value); \
+ CHECK_HEXEQ(expected, &num); \
+ } while (0)
+
+static void
+test_set(void)
+{
+ CHECK_SET(u32, 0, "0");
+ CHECK_SET(u32, 0xA, "A");
+ CHECK_SET(u32, 0x20, "20");
+ CHECK_SET(u32, 0x12345678, "12345678");
+
+ CHECK_SET(u64, 0, "0");
+ CHECK_SET(u64, 0xA, "A");
+ CHECK_SET(u64, 0x20, "20");
+ CHECK_SET(u64, 0x12345678, "12345678");
+ CHECK_SET(u64, 0xFFFFFFFFFFFFFFFFull, "FFFFFFFFFFFFFFFF");
+ CHECK_SET(u64, 0x123456789ABCDEF0ull, "123456789ABCDEF0");
+ CHECK_SET(u64, 0x123456789ABCDEF0ull, "123456789ABCDEF0");
+
+ CHECK_SET(decimal_string, "0", "0");
+ CHECK_SET(decimal_string, "1", "1");
+ CHECK_SET(decimal_string, "01234567890", "499602D2");
+ CHECK_SET(decimal_string, "12345.67890", "499602D2");
+ CHECK_SET(decimal_string, "12345.67890EOF", "499602D2");
+ CHECK_SET(decimal_string, "012345678901", "2DFDC1C35");
+ CHECK_SET(decimal_string, "12345.678901", "2DFDC1C35");
+ CHECK_SET(decimal_string,
+ "340282366920938463463374607431768211456",
+ "100000000000000000000000000000000");
+
+ CHECK_SET(hex_string, "0", "0");
+ CHECK_SET(hex_string, "123456789ABCDEF0", "123456789ABCDEF0");
+
+ const ExessBigint orig = bigint_from_hex("123456789ABCDEF01");
+ ExessBigint copy;
+ exess_bigint_set(&copy, &orig);
+ CHECK_HEXEQ("123456789ABCDEF01", &copy);
+}
+
+static void
+check_left_shifted_bigit(const char* value,
+ const unsigned amount,
+ const unsigned index,
+ const Bigit expected)
+{
+ const ExessBigint num = bigint_from_hex(value);
+ const Bigit actual = exess_bigint_left_shifted_bigit(&num, amount, index);
+
+ assert(expected == actual);
+}
+
+static void
+test_left_shifted_bigit(void)
+{
+ check_left_shifted_bigit("0", 100, 1, 0x0);
+ check_left_shifted_bigit("1", 0, 0, 0x1);
+ check_left_shifted_bigit("1", 1, 0, 0x2);
+ check_left_shifted_bigit("1", 4, 0, 0x10);
+ check_left_shifted_bigit("1", 32, 0, 0x0);
+ check_left_shifted_bigit("1", 32, 1, 0x1);
+ check_left_shifted_bigit("1", 64, 0, 0x0);
+ check_left_shifted_bigit("1", 64, 1, 0x0);
+ check_left_shifted_bigit("1", 64, 2, 0x1);
+ check_left_shifted_bigit("123456789ABCDEF", 64, 0, 0x0);
+ check_left_shifted_bigit("123456789ABCDEF", 64, 1, 0x0);
+ check_left_shifted_bigit("123456789ABCDEF", 64, 2, 0x89ABCDEF);
+ check_left_shifted_bigit("123456789ABCDEF", 64, 3, 0x1234567);
+ check_left_shifted_bigit("123456789ABCDEF", 64, 4, 0x0);
+ check_left_shifted_bigit("123456789ABCDEF", 65, 0, 0x0);
+ check_left_shifted_bigit("123456789ABCDEF", 65, 1, 0x0);
+ check_left_shifted_bigit("123456789ABCDEF", 65, 2, 0x13579BDE);
+ check_left_shifted_bigit("123456789ABCDEF", 65, 3, 0x2468ACF);
+ check_left_shifted_bigit("123456789ABCDEF", 65, 4, 0x0);
+}
+
+static void
+check_shift_left(const char* value, const unsigned amount, const char* expected)
+{
+ ExessBigint num = bigint_from_hex(value);
+ exess_bigint_shift_left(&num, amount);
+ CHECK_HEXEQ(expected, &num);
+}
+
+static void
+test_shift_left(void)
+{
+ check_shift_left("0", 100, "0");
+ check_shift_left("1", 1, "2");
+ check_shift_left("1", 4, "10");
+ check_shift_left("1", 32, "100000000");
+ check_shift_left("1", 64, "10000000000000000");
+ check_shift_left("123456789ABCDEF", 0, "123456789ABCDEF");
+ check_shift_left("123456789ABCDEF", 64, "123456789ABCDEF0000000000000000");
+ check_shift_left("123456789ABCDEF", 65, "2468ACF13579BDE0000000000000000");
+ check_shift_left("16B8B5E06EDC79", 23, "B5C5AF0376E3C800000");
+}
+
+static void
+check_add_u32(const char* value, const uint32_t rhs, const char* expected)
+{
+ ExessBigint num = bigint_from_hex(value);
+ exess_bigint_add_u32(&num, rhs);
+ CHECK_HEXEQ(expected, &num);
+}
+
+static void
+test_add_u32(void)
+{
+ check_add_u32("0", 1, "1");
+ check_add_u32("1", 1, "2");
+ check_add_u32("FFFFFFF", 1, "10000000");
+ check_add_u32("FFFFFFFFFFFFFF", 1, "100000000000000");
+
+ check_add_u32("10000000000000000000000000000000000080000000",
+ 0x80000000,
+ "10000000000000000000000000000000000100000000");
+
+ check_add_u32("10000000000000000000000000000000000000000000",
+ 0x1,
+ "10000000000000000000000000000000000000000001");
+}
+
+static void
+check_add(const char* lhs_hex, const char* rhs_hex, const char* expected)
+{
+ ExessBigint lhs = bigint_from_hex(lhs_hex);
+ const ExessBigint rhs = bigint_from_hex(rhs_hex);
+
+ exess_bigint_add(&lhs, &rhs);
+ CHECK_HEXEQ(expected, &lhs);
+}
+
+static void
+test_add(void)
+{
+ check_add("1", "0", "1");
+ check_add("1", "1", "2");
+ check_add("FFFFFFF", "1", "10000000");
+ check_add("FFFFFFFFFFFFFF", "1", "100000000000000");
+ check_add("1", "1000000000000", "1000000000001");
+ check_add("FFFFFFF", "1000000000000", "100000FFFFFFF");
+
+ check_add("10000000000000000000000000000000000000000000",
+ "1",
+ "10000000000000000000000000000000000000000001");
+
+ check_add("10000000000000000000000000000000000000000000",
+ "1000000000000",
+ "10000000000000000000000000000001000000000000");
+
+ check_add("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
+ "1000000000000",
+ "1000000000000000000000000000000FFFFFFFFFFFF");
+
+ check_add("10000000000000000000000000",
+ "1000000000000",
+ "10000000000001000000000000");
+
+ check_add(
+ "1", "10000000000000000000000000000", "10000000000000000000000000001");
+
+ check_add("FFFFFFF",
+ "10000000000000000000000000000",
+ "1000000000000000000000FFFFFFF");
+
+ check_add("10000000000000000000000000000000000000000000",
+ "10000000000000000000000000000",
+ "10000000000000010000000000000000000000000000");
+
+ check_add("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
+ "10000000000000000000000000000",
+ "100000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFF");
+
+ check_add("10000000000000000000000000",
+ "10000000000000000000000000000",
+ "10010000000000000000000000000");
+}
+
+static void
+check_subtract(const char* lhs_hex, const char* rhs_hex, const char* expected)
+{
+ ExessBigint lhs = bigint_from_hex(lhs_hex);
+ const ExessBigint rhs = bigint_from_hex(rhs_hex);
+
+ exess_bigint_subtract(&lhs, &rhs);
+ CHECK_HEXEQ(expected, &lhs);
+}
+
+static void
+test_subtract(void)
+{
+ check_subtract("1", "0", "1");
+ check_subtract("2", "0", "2");
+ check_subtract("10000000", "1", "FFFFFFF");
+ check_subtract("1FFFFFFFF00000000", "FFFFFFFF", "1FFFFFFFE00000001");
+ check_subtract("100000000000000", "1", "FFFFFFFFFFFFFF");
+ check_subtract("1000000000001", "1000000000000", "1");
+ check_subtract("100000FFFFFFF", "1000000000000", "FFFFFFF");
+
+ check_subtract(
+ "11F2678326EA00000000", "0878678326EAC9000000", "979FFFFFFFF37000000");
+
+ check_subtract("10000000000000000000000000000000000000000001",
+ "00000000000000000000000000000000000000000001",
+ "10000000000000000000000000000000000000000000");
+
+ check_subtract("10000000000000000000000000000001000000000000",
+ "00000000000000000000000000000001000000000000",
+ "10000000000000000000000000000000000000000000");
+
+ check_subtract("1000000000000000000000000000000FFFFFFFFFFFF",
+ "0000000000000000000000000000001000000000000",
+ " FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
+
+ check_subtract("10000000000000000000000000",
+ "00000000000001000000000000",
+ " FFFFFFFFFFFFF000000000000");
+
+ check_subtract("10000000000000000000000000",
+ "1000000000000000000000000",
+ "F000000000000000000000000");
+
+ check_subtract("FFFFFFF000000000000000",
+ "0000000000000800000000",
+ "FFFFFFEFFFFFF800000000");
+
+ check_subtract("10000000000000000000000000000000000000000000",
+ "00000000000000000000000000000000000800000000",
+ "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF800000000");
+
+ check_subtract("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
+ "000000000000000000000000000000000800000000",
+ "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFF");
+}
+
+static void
+check_subtract_left_shifted(const char* lhs_hex,
+ const char* rhs_hex,
+ const unsigned amount,
+ const char* expected)
+{
+ ExessBigint lhs = bigint_from_hex(lhs_hex);
+ const ExessBigint rhs = bigint_from_hex(rhs_hex);
+
+ exess_bigint_subtract_left_shifted(&lhs, &rhs, amount);
+ CHECK_HEXEQ(expected, &lhs);
+}
+
+static void
+test_subtract_left_shifted(void)
+{
+ check_subtract_left_shifted("1", "0", 1, "1");
+ check_subtract_left_shifted("10000000", "1", 1, "FFFFFFE");
+ check_subtract_left_shifted("100000000", "40000000", 2, "0");
+ check_subtract_left_shifted("1000000000000000", "400000000000000", 2, "0");
+ check_subtract_left_shifted("1000000000000000", "800000000000000", 1, "0");
+ check_subtract_left_shifted("1000000000000000", "F", 16, "FFFFFFFFFF10000");
+ check_subtract_left_shifted("1000000000000000", "F", 24, "FFFFFFFF1000000");
+ check_subtract_left_shifted("100000000000000", "1", 0, "FFFFFFFFFFFFFF");
+ check_subtract_left_shifted("100000000000000", "1", 56, "0");
+
+ check_subtract_left_shifted(
+ "11F2678326EA00000000", "43C33C1937564800000", 1, "979FFFFFFFF37000000");
+}
+
+static void
+check_multiply_u32(const char* value, const uint32_t rhs, const char* expected)
+{
+ ExessBigint num = bigint_from_hex(value);
+ exess_bigint_multiply_u32(&num, rhs);
+ CHECK_HEXEQ(expected, &num);
+}
+
+static void
+test_multiply_u32(void)
+{
+ check_multiply_u32("0", 0x25, "0");
+ check_multiply_u32("123456789ABCDEF", 0, "0");
+ check_multiply_u32("2", 0x5, "A");
+ check_multiply_u32("10000000", 0x9, "90000000");
+ check_multiply_u32("100000000000000", 0xFFFF, "FFFF00000000000000");
+ check_multiply_u32("100000000000000", 0xFFFFFFFF, "FFFFFFFF00000000000000");
+ check_multiply_u32("1234567ABCD", 0xFFF, "12333335552433");
+ check_multiply_u32("1234567ABCD", 0xFFFFFFF, "12345679998A985433");
+ check_multiply_u32("FFFFFFFFFFFFFFFF", 0x2, "1FFFFFFFFFFFFFFFE");
+ check_multiply_u32("FFFFFFFFFFFFFFFF", 0x4, "3FFFFFFFFFFFFFFFC");
+ check_multiply_u32("FFFFFFFFFFFFFFFF", 0xF, "EFFFFFFFFFFFFFFF1");
+ check_multiply_u32("FFFFFFFFFFFFFFFF", 0xFFFFFF, "FFFFFEFFFFFFFFFF000001");
+ check_multiply_u32("377654D193A171", 10000000, "210EDD6D4CDD2580EE80");
+ check_multiply_u32("2E3F36D108373C00000", 10, "1CE78242A52285800000");
+
+ check_multiply_u32(
+ "10000000000000000000000000", 0x00000002, "20000000000000000000000000");
+
+ check_multiply_u32(
+ "10000000000000000000000000", 0x0000000F, "F0000000000000000000000000");
+
+ check_multiply_u32("FFFF0000000000000000000000000",
+ 0xFFFF,
+ "FFFE00010000000000000000000000000");
+
+ check_multiply_u32("FFFF0000000000000000000000000",
+ 0xFFFFFFFF,
+ "FFFEFFFF00010000000000000000000000000");
+
+ check_multiply_u32("FFFF0000000000000000000000000",
+ 0xFFFFFFFF,
+ "FFFEFFFF00010000000000000000000000000");
+}
+
+static void
+check_multiply_u64(const char* value, const uint64_t rhs, const char* expected)
+{
+ ExessBigint num = bigint_from_hex(value);
+ exess_bigint_multiply_u64(&num, rhs);
+ CHECK_HEXEQ(expected, &num);
+}
+
+static void
+test_multiply_u64(void)
+{
+ check_multiply_u64("0", 0x25, "0");
+ check_multiply_u64("123456789ABCDEF", 0, "0");
+ check_multiply_u64("123456789ABCDEF", 1, "123456789ABCDEF");
+ check_multiply_u64("2", 0x5, "A");
+ check_multiply_u64("10000000", 0x9, "90000000");
+ check_multiply_u64("100000000000000", 0xFFFF, "FFFF00000000000000");
+ check_multiply_u64("1234567ABCD", 0xFFF, "12333335552433");
+ check_multiply_u64("1234567ABCD", 0xFFFFFFFFFFull, "1234567ABCBDCBA985433");
+ check_multiply_u64("FFFFFFFFFFFFFFFF", 0x2, "1FFFFFFFFFFFFFFFE");
+ check_multiply_u64("FFFFFFFFFFFFFFFF", 0x4, "3FFFFFFFFFFFFFFFC");
+ check_multiply_u64("FFFFFFFFFFFFFFFF", 0xF, "EFFFFFFFFFFFFFFF1");
+
+ check_multiply_u64(
+ "100000000000000", 0xFFFFFFFFFFFFFFFFull, "FFFFFFFFFFFFFFFF00000000000000");
+
+ check_multiply_u64("FFFFFFFFFFFFFFFF",
+ 0xFFFFFFFFFFFFFFFFull,
+ "FFFFFFFFFFFFFFFE0000000000000001");
+
+ check_multiply_u64(
+ "10000000000000000000000000", 0x00000002, "20000000000000000000000000");
+
+ check_multiply_u64(
+ "10000000000000000000000000", 0x0000000F, "F0000000000000000000000000");
+
+ check_multiply_u64("FFFF0000000000000000000000000",
+ 0xFFFF,
+ "FFFE00010000000000000000000000000");
+
+ check_multiply_u64("FFFF0000000000000000000000000",
+ 0xFFFFFFFF,
+ "FFFEFFFF00010000000000000000000000000");
+
+ check_multiply_u64("FFFF0000000000000000000000000",
+ 0xFFFFFFFFFFFFFFFFull,
+ "FFFEFFFFFFFFFFFF00010000000000000000000000000");
+
+ check_multiply_u64(
+ "377654D193A171", 0x8AC7230489E80000ull, "1E10EE4B11D15A7F3DE7F3C7680000");
+}
+
+static void
+check_multiply_pow10(const char* value,
+ const unsigned exponent,
+ const char* expected)
+{
+ ExessBigint num = bigint_from_hex(value);
+ exess_bigint_multiply_pow10(&num, exponent);
+ CHECK_HEXEQ(expected, &num);
+}
+
+static void
+test_multiply_pow10(void)
+{
+ check_multiply_pow10("0", 10, "0");
+ check_multiply_pow10("1234", 0, "1234");
+ check_multiply_pow10("4D2", 1, "3034");
+ check_multiply_pow10("4D2", 2, "1E208");
+ check_multiply_pow10("4D2", 3, "12D450");
+ check_multiply_pow10("4D2", 4, "BC4B20");
+ check_multiply_pow10("4D2", 5, "75AEF40");
+ check_multiply_pow10("4D2", 6, "498D5880");
+ check_multiply_pow10("4D2", 7, "2DF857500");
+ check_multiply_pow10("4D2", 8, "1CBB369200");
+ check_multiply_pow10("4D2", 9, "11F5021B400");
+ check_multiply_pow10("4D2", 10, "B3921510800");
+ check_multiply_pow10("4D2", 11, "703B4D2A5000");
+ check_multiply_pow10("4D2", 12, "4625103A72000");
+ check_multiply_pow10("4D2", 13, "2BD72A24874000");
+ check_multiply_pow10("4D2", 14, "1B667A56D488000");
+ check_multiply_pow10("4D2", 15, "11200C7644D50000");
+ check_multiply_pow10("4D2", 16, "AB407C9EB0520000");
+ check_multiply_pow10("4D2", 17, "6B084DE32E3340000");
+ check_multiply_pow10("4D2", 18, "42E530ADFCE0080000");
+ check_multiply_pow10("4D2", 19, "29CF3E6CBE0C0500000");
+ check_multiply_pow10("4D2", 20, "1A218703F6C783200000");
+ check_multiply_pow10("4D2", 21, "1054F4627A3CB1F400000");
+ check_multiply_pow10("4D2", 22, "A3518BD8C65EF38800000");
+ check_multiply_pow10("4D2", 23, "6612F7677BFB5835000000");
+ check_multiply_pow10("4D2", 24, "3FCBDAA0AD7D17212000000");
+ check_multiply_pow10("4D2", 25, "27DF68A46C6E2E74B4000000");
+ check_multiply_pow10("4D2", 26, "18EBA166C3C4DD08F08000000");
+ check_multiply_pow10("4D2", 27, "F9344E03A5B0A259650000000");
+ check_multiply_pow10("4D2", 28, "9BC0B0C2478E6577DF20000000");
+ check_multiply_pow10("4D2", 29, "61586E796CB8FF6AEB740000000");
+ check_multiply_pow10("4D2", 30, "3CD7450BE3F39FA2D32880000000");
+ check_multiply_pow10("4D2", 31, "26068B276E7843C5C3F9500000000");
+
+ check_multiply_pow10("4D2",
+ 50,
+ "149D1B4CFED03B23AB5F4E1196EF45C0"
+ "8000000000000");
+
+ check_multiply_pow10("4D2",
+ 100,
+ "5827249F27165024FBC47DFCA9359BF3"
+ "16332D1B91ACEECF471FBAB06D9B2000"
+ "0000000000000000000000");
+
+ check_multiply_pow10("4D2",
+ 305,
+ "AFBA390D657B0829339F5B98DC852A89"
+ "682758E01829EADFD016D1528D4D548B"
+ "80894B9ED9C2EC6A9CABB4881302A637"
+ "9FF3058908FEAC310C52FCA009799718"
+ "8260B0B2E2EC96E471B7892AD9B4F9F9"
+ "A448CBF150D2E87F3934000000000000"
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000000");
+
+ check_multiply_pow10("123456789ABCDEF0", 0, "123456789ABCDEF0");
+ check_multiply_pow10("123456789ABCDEF0",
+ 44,
+ "51A1AD66ACE4E5C79209330F58F52DE3"
+ "7CEFFF1F000000000000");
+ check_multiply_pow10("123456789ABCDEF0",
+ 88,
+ "16E0C6D18F4BFA7D0289B88382F56151"
+ "EB9DA5DB09D56C9BA5D8305619CEE057"
+ "4F00000000000000000000000");
+ check_multiply_pow10("123456789ABCDEF0",
+ 132,
+ "6696B1DA27BEA173B5EFCAABBB8492A9"
+ "2AE3D97F7EE3C7314FB7E2FF8AEFD329"
+ "F5F8202C22650BB79A7D9F3867F00000"
+ "00000000000000000000000000000");
+ check_multiply_pow10("123456789ABCDEF0",
+ 176,
+ "1CC05FF0499D8BC7D8EBE0C6DC2FDC09"
+ "E93765F3448235FB16AD09D98BBB3A0A"
+ "843372D33A318EE63DAE6998DA59EF34"
+ "B15C40A65B9B65ABF3CAF00000000000"
+ "00000000000000000000000000000000"
+ "00");
+ check_multiply_pow10("123456789ABCDEF0",
+ 220,
+ "80ED0FD9A6C0F56A495F466320D34E22"
+ "507FAA83F0519E7FF909FDDBDA184682"
+ "BB70D38D43284C828A3681540722E550"
+ "960567BAB1C25389C1BE7705228BE8CC"
+ "AF3EBD382829DF000000000000000000"
+ "00000000000000000000000000000000"
+ "000000");
+ check_multiply_pow10("123456789ABCDEF0",
+ 264,
+ "2421FD0F55C486D05211339D45EC2DC4"
+ "12AE7A64DDFE619DA81B73C069088D3E"
+ "83D7AA9F99B571815DE939A5275FB4A6"
+ "9D8930798C01FB96781B9D633BB59AD5"
+ "A7F322A7EC14154D1B8B5DF1718779A5"
+ "2291FE0F000000000000000000000000"
+ "00000000000000000000000000000000"
+ "00000000000");
+ check_multiply_pow10("123456789ABCDEF0",
+ 308,
+ "A206620F35C83E9E780ECC07DCAF13BB"
+ "0A7EE2E213747914340BC172D783BA56"
+ "661E8DCFFD03C398BD66F5570F445AC6"
+ "737126283C64AE1A289B9D8BB4531033"
+ "8C3E34DE2D534187092ABA1F4706100E"
+ "ECF66D14059461A05A9BEBBCCBA0F693"
+ "F0000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "000000000000000");
+}
+
+static void
+check_divmod(const char* lhs_hex,
+ const char* rhs_hex,
+ const uint32_t expected_divisor,
+ const char* expected_mod_hex)
+{
+ ExessBigint lhs = bigint_from_hex(lhs_hex);
+ const ExessBigint rhs = bigint_from_hex(rhs_hex);
+ const uint32_t divisor = exess_bigint_divmod(&lhs, &rhs);
+
+ assert(divisor == expected_divisor);
+ CHECK_HEXEQ(expected_mod_hex, &lhs);
+}
+
+static void
+test_divmod(void)
+{
+ check_divmod("A", "2", 5, "0");
+ check_divmod("B", "2", 5, "1");
+ check_divmod("C", "2", 6, "0");
+ check_divmod("A", "1234567890", 0, "A");
+ check_divmod("FFFFFFFF", "3", 0x55555555, "0");
+ check_divmod("12345678", "3789012", 5, "D9861E");
+ check_divmod("70000001", "1FFFFFFF", 3, "10000004");
+ check_divmod("28000000", "12A05F20", 2, "2BF41C0");
+ check_divmod("FFFFFFFFF", "FFFFFFFF", 16, "F");
+ check_divmod("100000000000001", "FFFFFFF", 0x10000001, "2");
+ check_divmod("40000000000002", "2FAF0800000000", 1, "1050F800000002");
+ check_divmod("40000000000000", "40000000000000", 1, "0");
+
+ check_divmod("43DE72C3DF858FC278A361EEB5A000000",
+ "80000000000000000000000000000000",
+ 8,
+ "3DE72C3DF858FC278A361EEB5A000000");
+
+ check_divmod(
+ "B5C5AF0376E3C800000", "43C33C1937564800000", 2, "2E3F36D108373800000");
+
+ check_divmod("A0000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "000000000000000000000000000000",
+ "20000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "000000000000000000000000000000",
+ 5,
+ "0");
+
+ check_divmod("A0000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "000000000000000000000000000001",
+ "20000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "000000000000000000000000000000",
+ 5,
+ "1");
+
+ check_divmod("B6080000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "000000000000000000000000000000FF"
+ "F",
+ "A0000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "000000000000000000000000000000",
+ 0x1234,
+ "FFF");
+
+ check_divmod("B6080000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "00000000000000000000000000000000"
+ "0",
+ "9FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+ "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+ "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+ "FFFFFFFFFFFFFFFFFFFFFFFFFFF001",
+ 0x1234,
+ "1232DCC");
+}
+
+static void
+check_compare(const char* lhs_hex, const char* rhs_hex, const int expected_cmp)
+{
+ const ExessBigint lhs = bigint_from_hex(lhs_hex);
+ const ExessBigint rhs = bigint_from_hex(rhs_hex);
+ const int cmp = exess_bigint_compare(&lhs, &rhs);
+
+ assert(cmp == expected_cmp);
+
+ if (cmp) {
+ const int rcmp = exess_bigint_compare(&rhs, &lhs);
+ assert(rcmp == -cmp);
+ }
+}
+
+static void
+test_compare(void)
+{
+ check_compare("1", "1", 0);
+ check_compare("0", "1", -1);
+ check_compare("F", "FF", -1);
+ check_compare("F", "FFFFFFFFF", -1);
+ check_compare("10000000000000000", "10000000000000000", 0);
+ check_compare("10000000000000000", "10000000000000001", -1);
+ check_compare("FFFFFFFFFFFFFFFFF", "100000000000000000", -1);
+ check_compare("10000000000000000", "10000000000000001", -1);
+ check_compare("1234567890ABCDEF12345", "1234567890ABCDEF12345", 0);
+ check_compare("1234567890ABCDEF12345", "1234567890ABCDEF12346", -1);
+
+ const char* const huge = "123456789ABCDEF0123456789ABCDEF0"
+ "123456789ABCDEF0123456789ABCDEF0"
+ "123456789ABCDEF0123456789ABCDEF0"
+ "123456789ABCDEF0123456789ABCDEF0";
+
+ const char* const huger = "123456789ABCDEF0123456789ABCDEF0"
+ "123456789ABCDEF0123456789ABCDEF0"
+ "123456789ABCDEF0123456789ABCDEF0"
+ "123456789ABCDEF0123456789ABCDEF1";
+
+ check_compare(huge, huge, 0);
+ check_compare(huger, huger, 0);
+ check_compare(huge, huger, -1);
+}
+
+static void
+check_plus_compare(const char* l_hex,
+ const char* p_hex,
+ const char* c_hex,
+ const int expected_cmp)
+{
+ const ExessBigint l = bigint_from_hex(l_hex);
+ const ExessBigint p = bigint_from_hex(p_hex);
+ const ExessBigint c = bigint_from_hex(c_hex);
+ const int cmp = exess_bigint_plus_compare(&l, &p, &c);
+ const int rcmp = exess_bigint_plus_compare(&p, &l, &c);
+
+ assert(cmp == expected_cmp);
+ assert(rcmp == expected_cmp);
+}
+
+static void
+test_plus_compare(void)
+{
+ check_plus_compare("1", "0", "1", 0);
+ check_plus_compare("0", "0", "1", -1);
+ check_plus_compare("FFFFFFFFF", "F", "F", 1);
+ check_plus_compare("F", "F", "800000000", -1);
+ check_plus_compare("F", "F", "80000000000000000", -1);
+ check_plus_compare("800000000", "F", "80000000000000000", -1);
+ check_plus_compare("2D79883D20000", "2D79883D20000", "5AF3107A40000", 0);
+ check_plus_compare("20000000000000", "1", "20000000000000", +1);
+
+ check_plus_compare(
+ "0588A503282FE00000", "0588A503282FE00000", "0AD78EBC5AC6200000", +1);
+
+ check_plus_compare("2F06018572BEADD1280000000",
+ "0204FCE5E3E25026110000000",
+ "4000000000000000000000000",
+ -1);
+
+ check_plus_compare("1234567890ABCDEF12345",
+ "000000000000000000001",
+ "1234567890ABCDEF12345",
+ +1);
+
+ check_plus_compare("1234567890ABCDEF12344",
+ "000000000000000000001",
+ "1234567890ABCDEF12345",
+ 0);
+
+ check_plus_compare("123456789000000000000",
+ "0000000000ABCDEF12345",
+ "1234567890ABCDEF12345",
+ 0);
+
+ check_plus_compare("123456789000000000000",
+ "0000000000ABCDEF12344",
+ "1234567890ABCDEF12345",
+ -1);
+
+ check_plus_compare("123456789000000000000",
+ "0000000000ABCDEF12346",
+ "1234567890ABCDEF12345",
+ 1);
+
+ check_plus_compare("123456789100000000000",
+ "0000000000ABCDEF12345",
+ "1234567890ABCDEF12345",
+ 1);
+
+ check_plus_compare("123456788900000000000",
+ "0000000000ABCDEF12345",
+ "1234567890ABCDEF12345",
+ -1);
+
+ check_plus_compare("12345678900000000000000000000",
+ "0000000000ABCDEF1234500000000",
+ "1234567890ABCDEF1234500000000",
+ 0);
+
+ check_plus_compare("12345678900000000000000000000",
+ "0000000000ABCDEF1234400000000",
+ "1234567890ABCDEF1234500000000",
+ -1);
+
+ check_plus_compare("12345678900000000000000000000",
+ "0000000000ABCDEF1234600000000",
+ "1234567890ABCDEF1234500000000",
+ 1);
+
+ check_plus_compare("12345678910000000000000000000",
+ "0000000000ABCDEF1234500000000",
+ "1234567890ABCDEF1234500000000",
+ 1);
+ check_plus_compare("12345678890000000000000000000",
+ "0000000000ABCDEF1234500000000",
+ "1234567890ABCDEF1234500000000",
+ -1);
+
+ check_plus_compare("12345678900000000000000000000",
+ "000000000000000000ABCDEF12345",
+ "123456789000000000ABCDEF12345",
+ 0);
+
+ check_plus_compare("12345678900000000000000000000",
+ "000000000000000000ABCDEF12346",
+ "123456789000000000ABCDEF12345",
+ 1);
+
+ check_plus_compare("12345678900000000000000000000",
+ "000000000000000000ABCDEF12344",
+ "123456789000000000ABCDEF12345",
+ -1);
+
+ check_plus_compare("12345678900000000000000000000",
+ "000000000000000000ABCDEF12345",
+ "12345678900000ABCDEF123450000",
+ -1);
+
+ check_plus_compare("12345678900000000000000000000",
+ "000000000000000000ABCDEF12344",
+ "12345678900000ABCDEF123450000",
+ -1);
+
+ check_plus_compare("12345678900000000000000000000",
+ "000000000000000000ABCDEF12345",
+ "12345678900000ABCDEF123450001",
+ -1);
+
+ check_plus_compare("12345678900000000000000000000",
+ "00000000000000ABCDEF123460000",
+ "12345678900000ABCDEF123450000",
+ 1);
+}
+
+static void
+check_pow10(const unsigned exponent, const char* expected)
+{
+ ExessBigint num;
+ exess_bigint_set_pow10(&num, exponent);
+ CHECK_HEXEQ(expected, &num);
+}
+
+static void
+test_set_pow10(void)
+{
+ check_pow10(0, "1");
+ check_pow10(1, "A");
+ check_pow10(2, "64");
+ check_pow10(5, "186A0");
+ check_pow10(8, "5F5E100");
+ check_pow10(16, "2386F26FC10000");
+ check_pow10(30, "C9F2C9CD04674EDEA40000000");
+ check_pow10(31, "7E37BE2022C0914B2680000000");
+}
+
+int
+main(void)
+{
+ test_set();
+ test_left_shifted_bigit();
+ test_shift_left();
+ test_add_u32();
+ test_add();
+ test_subtract();
+ test_subtract_left_shifted();
+ test_multiply_u32();
+ test_multiply_u64();
+ test_multiply_pow10();
+ test_divmod();
+ test_compare();
+ test_plus_compare();
+ test_set_pow10();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_boolean.c b/subprojects/exess/test/test_boolean.c
new file mode 100644
index 00000000..65eb9b24
--- /dev/null
+++ b/subprojects/exess/test/test_boolean.c
@@ -0,0 +1,114 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const bool expected_value,
+ const size_t expected_count)
+{
+ bool value = false;
+
+ const ExessResult r = exess_read_boolean(&value, string);
+ assert(value == expected_value);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+}
+
+static void
+test_read_boolean(void)
+{
+ // No input
+ check_read("", EXESS_EXPECTED_BOOLEAN, false, 0);
+ check_read(" \f\n\r\t\v", EXESS_EXPECTED_BOOLEAN, false, 6);
+
+ // Canonical form
+ check_read("false", EXESS_SUCCESS, false, 5);
+ check_read("true", EXESS_SUCCESS, true, 4);
+
+ // Non-canonical form
+ check_read("0", EXESS_SUCCESS, false, 1);
+ check_read("1", EXESS_SUCCESS, true, 1);
+ check_read(" \f\n\r\t\vfalse ", EXESS_SUCCESS, false, 11);
+ check_read(" \f\n\r\t\vtrue ", EXESS_SUCCESS, true, 10);
+ check_read(" \f\n\r\t\v0 ", EXESS_SUCCESS, false, 7);
+ check_read(" \f\n\r\t\v1 ", EXESS_SUCCESS, true, 7);
+
+ // Trailing garbage
+ check_read("falsely", EXESS_EXPECTED_END, false, 5);
+ check_read("truely", EXESS_EXPECTED_END, true, 4);
+ check_read("0no", EXESS_EXPECTED_END, false, 1);
+ check_read("1yes", EXESS_EXPECTED_END, true, 1);
+
+ // Garbage
+ check_read("twue", EXESS_EXPECTED_BOOLEAN, false, 0);
+ check_read("fawse", EXESS_EXPECTED_BOOLEAN, false, 0);
+ check_read("tr", EXESS_EXPECTED_BOOLEAN, false, 0);
+ check_read("fa", EXESS_EXPECTED_BOOLEAN, false, 0);
+ check_read("yes", EXESS_EXPECTED_BOOLEAN, false, 0);
+ check_read("no", EXESS_EXPECTED_BOOLEAN, false, 0);
+}
+
+static void
+check_write(const bool value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_BOOLEAN_LENGTH + 1] = {1, 2, 3, 4, 5, 6};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_boolean(value, buf_size, buf);
+ assert(!strcmp(buf, expected_string));
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(r.status || exess_write_boolean(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_boolean(void)
+{
+ check_write(true, EXESS_SUCCESS, 5, "true");
+ check_write(false, EXESS_SUCCESS, 6, "false");
+
+ check_write(true, EXESS_NO_SPACE, 4, "");
+ check_write(false, EXESS_NO_SPACE, 5, "");
+
+ // Check that nothing is written when there isn't enough space
+ char c = 42;
+ const ExessResult r = exess_write_boolean(false, 0, &c);
+ assert(c == 42);
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 0);
+}
+
+int
+main(void)
+{
+ test_read_boolean();
+ test_write_boolean();
+ return 0;
+}
diff --git a/subprojects/exess/test/test_byte.c b/subprojects/exess/test/test_byte.c
new file mode 100644
index 00000000..a67fd779
--- /dev/null
+++ b/subprojects/exess/test/test_byte.c
@@ -0,0 +1,98 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const int8_t expected_value,
+ const size_t expected_count)
+{
+ int8_t value = 0;
+ const ExessResult r = exess_read_byte(&value, string);
+
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(value == expected_value);
+}
+
+static void
+test_read_byte(void)
+{
+ // Limits
+ check_read("-128", EXESS_SUCCESS, INT8_MIN, EXESS_MAX_BYTE_LENGTH);
+ check_read("127", EXESS_SUCCESS, INT8_MAX, 3);
+
+ // Out of range
+ check_read("-129", EXESS_OUT_OF_RANGE, 0, 4);
+ check_read("128", EXESS_OUT_OF_RANGE, 0, 3);
+
+ // Garbage
+ check_read("+", EXESS_EXPECTED_DIGIT, 0, 1);
+}
+
+static void
+check_write(const int8_t value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_BYTE_LENGTH + 1] = {1, 2, 3, 4, 5};
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_byte(value, buf_size, buf);
+ assert(r.status == expected_status);
+ assert(!strcmp(buf, expected_string));
+ assert(r.count == strlen(buf));
+ assert(r.status || exess_write_byte(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_byte(void)
+{
+ check_write(INT8_MIN, EXESS_SUCCESS, 5, "-128");
+ check_write(INT8_MAX, EXESS_SUCCESS, 4, "127");
+}
+
+static void
+test_round_trip(void)
+{
+ int8_t value = 0;
+ char buf[EXESS_MAX_BYTE_LENGTH + 1] = {1, 2, 3, 4, 5};
+
+ for (int16_t i = INT8_MIN; i <= INT8_MAX; ++i) {
+ assert(!exess_write_byte((int8_t)i, sizeof(buf), buf).status);
+ assert(!exess_read_byte(&value, buf).status);
+ assert(value == i);
+ }
+}
+
+int
+main(void)
+{
+ test_read_byte();
+ test_write_byte();
+ test_round_trip();
+ return 0;
+}
diff --git a/subprojects/exess/test/test_canonical.c b/subprojects/exess/test/test_canonical.c
new file mode 100644
index 00000000..77167fcf
--- /dev/null
+++ b/subprojects/exess/test/test_canonical.c
@@ -0,0 +1,412 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <string.h>
+
+static void
+check_write(const ExessDatatype datatype,
+ const char* const value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[328] = {42};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_canonical(value, datatype, buf_size, buf);
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert((r.status && r.status != EXESS_EXPECTED_END) ||
+ exess_write_canonical(value, datatype, 0, NULL).count == r.count);
+}
+
+static void
+test_decimal(void)
+{
+ check_write(EXESS_DECIMAL, "", EXESS_EXPECTED_DIGIT, 1, "");
+ check_write(EXESS_DECIMAL, " \f\n\r\t\v", EXESS_EXPECTED_DIGIT, 1, "");
+
+ check_write(EXESS_DECIMAL, " -001 ", EXESS_SUCCESS, 5, "-1.0");
+ check_write(EXESS_DECIMAL, " -000 ", EXESS_SUCCESS, 5, "-0.0");
+ check_write(EXESS_DECIMAL, " 000 ", EXESS_SUCCESS, 4, "0.0");
+ check_write(EXESS_DECIMAL, " 001 ", EXESS_SUCCESS, 4, "1.0");
+ check_write(EXESS_DECIMAL, " +001 ", EXESS_SUCCESS, 4, "1.0");
+
+ check_write(EXESS_DECIMAL, " -.123", EXESS_SUCCESS, 7, "-0.123");
+ check_write(EXESS_DECIMAL, " +.123", EXESS_SUCCESS, 6, "0.123");
+ check_write(EXESS_DECIMAL, " -0 ", EXESS_SUCCESS, 5, "-0.0");
+ check_write(EXESS_DECIMAL, " +0 ", EXESS_SUCCESS, 4, "0.0");
+
+ check_write(EXESS_DECIMAL, " +00.10 ", EXESS_SUCCESS, 4, "0.1");
+ check_write(EXESS_DECIMAL, " +01 ", EXESS_SUCCESS, 4, "1.0");
+
+ check_write(EXESS_DECIMAL,
+ " 36893488147419103232 ",
+ EXESS_SUCCESS,
+ 23,
+ "36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " 0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 23,
+ "36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " +36893488147419103232 ",
+ EXESS_SUCCESS,
+ 23,
+ "36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " -36893488147419103232 ",
+ EXESS_SUCCESS,
+ 24,
+ "-36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " +0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 23,
+ "36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " +0036893488147419103232. ",
+ EXESS_SUCCESS,
+ 23,
+ "36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " +0036893488147419103232.00 ",
+ EXESS_SUCCESS,
+ 23,
+ "36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " +0036893488147419103232.12300 ",
+ EXESS_SUCCESS,
+ 25,
+ "36893488147419103232.123");
+
+ check_write(EXESS_DECIMAL,
+ " -0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 24,
+ "-36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " -0036893488147419103232. ",
+ EXESS_SUCCESS,
+ 24,
+ "-36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " -0036893488147419103232.00 ",
+ EXESS_SUCCESS,
+ 24,
+ "-36893488147419103232.0");
+
+ check_write(EXESS_DECIMAL,
+ " -0036893488147419103232.12300 ",
+ EXESS_SUCCESS,
+ 26,
+ "-36893488147419103232.123");
+
+ check_write(EXESS_DECIMAL, " -1234extra", EXESS_EXPECTED_END, 8, "-1234.0");
+ check_write(EXESS_DECIMAL, " 1234extra", EXESS_EXPECTED_END, 7, "1234.0");
+
+ check_write(EXESS_DECIMAL, " f", EXESS_EXPECTED_DIGIT, 2, "");
+ check_write(EXESS_DECIMAL, "", EXESS_EXPECTED_DIGIT, 1, "");
+}
+
+static void
+test_integer(void)
+{
+ check_write(EXESS_INTEGER, "", EXESS_EXPECTED_DIGIT, 1, "");
+ check_write(EXESS_INTEGER, " \f\n\r\t\v", EXESS_EXPECTED_DIGIT, 1, "");
+
+ // Integer
+
+ check_write(EXESS_INTEGER, " -001 ", EXESS_SUCCESS, 3, "-1");
+ check_write(EXESS_INTEGER, " 000 ", EXESS_SUCCESS, 2, "0");
+ check_write(EXESS_INTEGER, " 001 ", EXESS_SUCCESS, 2, "1");
+ check_write(EXESS_INTEGER, " +001 ", EXESS_SUCCESS, 2, "1");
+
+ check_write(EXESS_INTEGER, " junk 987654321 ", EXESS_EXPECTED_DIGIT, 2, "");
+ check_write(
+ EXESS_INTEGER, " 987654321 junk ", EXESS_EXPECTED_END, 10, "987654321");
+
+ check_write(EXESS_INTEGER,
+ " 36893488147419103232 ",
+ EXESS_SUCCESS,
+ 21,
+ "36893488147419103232");
+
+ check_write(EXESS_INTEGER,
+ " 0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 21,
+ "36893488147419103232");
+
+ check_write(EXESS_INTEGER,
+ " +36893488147419103232 ",
+ EXESS_SUCCESS,
+ 21,
+ "36893488147419103232");
+
+ check_write(EXESS_INTEGER,
+ " +0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 21,
+ "36893488147419103232");
+
+ check_write(EXESS_INTEGER,
+ " -36893488147419103232 ",
+ EXESS_SUCCESS,
+ 22,
+ "-36893488147419103232");
+
+ check_write(EXESS_INTEGER,
+ " -0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 22,
+ "-36893488147419103232");
+
+ // NonPositiveInteger
+
+ check_write(EXESS_NON_POSITIVE_INTEGER, " -001 ", EXESS_SUCCESS, 3, "-1");
+ check_write(EXESS_NON_POSITIVE_INTEGER, " 000 ", EXESS_SUCCESS, 2, "0");
+ check_write(EXESS_NON_POSITIVE_INTEGER, " 001 ", EXESS_BAD_VALUE, 3, "");
+ check_write(EXESS_NON_POSITIVE_INTEGER, " +001 ", EXESS_BAD_VALUE, 3, "");
+
+ check_write(EXESS_NON_POSITIVE_INTEGER,
+ " -36893488147419103232 ",
+ EXESS_SUCCESS,
+ 22,
+ "-36893488147419103232");
+
+ check_write(EXESS_NON_POSITIVE_INTEGER,
+ " -0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 22,
+ "-36893488147419103232");
+
+ // NegativeInteger
+
+ check_write(EXESS_NEGATIVE_INTEGER, " -001 ", EXESS_SUCCESS, 3, "-1");
+ check_write(EXESS_NEGATIVE_INTEGER, " 000 ", EXESS_BAD_VALUE, 3, "");
+ check_write(EXESS_NEGATIVE_INTEGER, " 001 ", EXESS_BAD_VALUE, 3, "");
+ check_write(EXESS_NEGATIVE_INTEGER, " +001 ", EXESS_BAD_VALUE, 3, "");
+
+ check_write(EXESS_NEGATIVE_INTEGER,
+ " -36893488147419103232 ",
+ EXESS_SUCCESS,
+ 22,
+ "-36893488147419103232");
+
+ check_write(EXESS_NEGATIVE_INTEGER,
+ " -0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 22,
+ "-36893488147419103232");
+
+ // NonNegativeInteger
+
+ check_write(EXESS_NON_NEGATIVE_INTEGER, " -001 ", EXESS_BAD_VALUE, 3, "");
+ check_write(EXESS_NON_NEGATIVE_INTEGER, " 000 ", EXESS_SUCCESS, 2, "0");
+ check_write(EXESS_NON_NEGATIVE_INTEGER, " 001 ", EXESS_SUCCESS, 2, "1");
+ check_write(EXESS_NON_NEGATIVE_INTEGER, " +001 ", EXESS_SUCCESS, 2, "1");
+
+ check_write(EXESS_NON_NEGATIVE_INTEGER,
+ " 36893488147419103232 ",
+ EXESS_SUCCESS,
+ 21,
+ "36893488147419103232");
+
+ check_write(EXESS_NON_NEGATIVE_INTEGER,
+ " 0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 21,
+ "36893488147419103232");
+
+ // PositiveInteger
+
+ check_write(EXESS_POSITIVE_INTEGER, " -001 ", EXESS_BAD_VALUE, 3, "");
+ check_write(EXESS_POSITIVE_INTEGER, " 000 ", EXESS_BAD_VALUE, 3, "");
+ check_write(EXESS_POSITIVE_INTEGER, " 001 ", EXESS_SUCCESS, 2, "1");
+ check_write(EXESS_POSITIVE_INTEGER, " +001 ", EXESS_SUCCESS, 2, "1");
+
+ check_write(EXESS_POSITIVE_INTEGER,
+ " 36893488147419103232 ",
+ EXESS_SUCCESS,
+ 21,
+ "36893488147419103232");
+
+ check_write(EXESS_POSITIVE_INTEGER,
+ " 0036893488147419103232 ",
+ EXESS_SUCCESS,
+ 21,
+ "36893488147419103232");
+}
+
+static void
+test_fixed_numbers(void)
+{
+ check_write(EXESS_DOUBLE, " +00.10 ", EXESS_SUCCESS, 7, "1.0E-1");
+ check_write(EXESS_FLOAT, " +00.10 ", EXESS_SUCCESS, 14, "1.00000001E-1");
+ check_write(EXESS_BOOLEAN, " 0 ", EXESS_SUCCESS, 6, "false");
+ check_write(EXESS_BOOLEAN, " 1 ", EXESS_SUCCESS, 5, "true");
+ check_write(EXESS_LONG, " +012 ", EXESS_SUCCESS, 3, "12");
+ check_write(EXESS_INT, " +012 ", EXESS_SUCCESS, 3, "12");
+ check_write(EXESS_SHORT, " +012 ", EXESS_SUCCESS, 3, "12");
+ check_write(EXESS_BYTE, " +012 ", EXESS_SUCCESS, 3, "12");
+ check_write(EXESS_ULONG, " 012 ", EXESS_SUCCESS, 3, "12");
+ check_write(EXESS_UINT, " 012 ", EXESS_SUCCESS, 3, "12");
+ check_write(EXESS_USHORT, " 012 ", EXESS_SUCCESS, 3, "12");
+ check_write(EXESS_UBYTE, " 012 ", EXESS_SUCCESS, 3, "12");
+}
+
+static void
+test_time(void)
+{
+ check_write(EXESS_DURATION, " P0Y6M ", EXESS_SUCCESS, 4, "P6M");
+ check_write(EXESS_DURATION, " P1Y6M0D ", EXESS_SUCCESS, 6, "P1Y6M");
+ check_write(EXESS_TIME, " 12:15:01+00:00 ", EXESS_SUCCESS, 14, "12:15:01Z");
+ check_write(
+ EXESS_DATE, " 02004-04-12+00:00 ", EXESS_SUCCESS, 12, "2004-04-12Z");
+}
+
+static void
+test_datetime(void)
+{
+ // Local
+ check_write(EXESS_DATETIME,
+ " 02001-02-03T04:05:06.007 ",
+ EXESS_SUCCESS,
+ 26,
+ "2001-02-03T04:05:06.007");
+
+ // Positive carry: minute => hour
+ check_write(EXESS_DATETIME,
+ " 02001-02-03T04:46:59-00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2001-02-03T05:01:59Z");
+
+ // Positive carry: minute => hour => day
+ check_write(EXESS_DATETIME,
+ " 02001-02-03T23:46:59-00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2001-02-04T00:01:59Z");
+
+ // Positive carry: minute => hour => day => month (common year)
+ check_write(EXESS_DATETIME,
+ " 02001-02-28T23:46:59-00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2001-03-01T00:01:59Z");
+
+ // Positive carry: minute => hour => day => month (leap year)
+ check_write(EXESS_DATETIME,
+ " 02000-02-29T23:46:59-00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2000-03-01T00:01:59Z");
+
+ // Positive carry: minute => hour => day => month => year
+ check_write(EXESS_DATETIME,
+ " 02001-12-31T23:46:59-00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2002-01-01T00:01:59Z");
+
+ // Negative carry: minute => hour
+ check_write(EXESS_DATETIME,
+ " 02001-02-03T04:14:59+00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2001-02-03T03:59:59Z");
+
+ // Negative carry: minute => hour => day
+ check_write(EXESS_DATETIME,
+ " 02001-02-02T00:14:59+00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2001-02-01T23:59:59Z");
+
+ // Negative carry: minute => hour => day => month (common year)
+ check_write(EXESS_DATETIME,
+ " 02001-03-01T00:14:59+00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2001-02-28T23:59:59Z");
+
+ // Negative carry: minute => hour => day => month (leap year)
+ check_write(EXESS_DATETIME,
+ " 02000-03-01T00:14:59+00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2000-02-29T23:59:59Z");
+
+ // Negative carry: minute => hour => day => month => year
+ check_write(EXESS_DATETIME,
+ " 02001-01-01T00:14:59+00:15 ",
+ EXESS_SUCCESS,
+ 21,
+ "2000-12-31T23:59:59Z");
+}
+
+static void
+test_binary(void)
+{
+ check_write(EXESS_HEX, " D EA D B3 3F", EXESS_SUCCESS, 9, "DEADB33F");
+ check_write(EXESS_HEX, "invalid", EXESS_EXPECTED_HEX, 1, "");
+ check_write(EXESS_HEX, "1A2B3", EXESS_EXPECTED_HEX, 5, "");
+ check_write(EXESS_HEX, "1", EXESS_EXPECTED_HEX, 1, "");
+ check_write(EXESS_HEX, "", EXESS_EXPECTED_HEX, 5, "");
+
+ check_write(
+ EXESS_BASE64, " Z\fm\n9\rv\tY\vmFy", EXESS_SUCCESS, 9, "Zm9vYmFy");
+ check_write(EXESS_BASE64, "!nvalid", EXESS_EXPECTED_BASE64, 1, "");
+ check_write(EXESS_BASE64, "Z", EXESS_EXPECTED_BASE64, 1, "");
+ check_write(EXESS_BASE64, "Zm", EXESS_EXPECTED_BASE64, 2, "");
+ check_write(EXESS_BASE64, "Zm9", EXESS_EXPECTED_BASE64, 3, "");
+ check_write(EXESS_BASE64, "", EXESS_EXPECTED_BASE64, 5, "");
+}
+
+int
+main(void)
+{
+ check_write(EXESS_NOTHING, "?", EXESS_UNSUPPORTED, 1, "");
+
+ test_decimal();
+ test_integer();
+ test_fixed_numbers();
+ test_time();
+ test_datetime();
+ test_binary();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_coerce.c b/subprojects/exess/test/test_coerce.c
new file mode 100644
index 00000000..dc5ad060
--- /dev/null
+++ b/subprojects/exess/test/test_coerce.c
@@ -0,0 +1,519 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <string.h>
+
+static const char* const min_long_str = "-9223372036854775808";
+static const char* const max_long_str = "9223372036854775807";
+static const char* const max_ulong_str = "18446744073709551615";
+
+static void
+check_from_to(const ExessDatatype from_datatype,
+ const char* const from_string,
+ const ExessDatatype to_datatype,
+ const char* const to_string)
+{
+ char value_buf[4] = {0, 0, 0, 0};
+ char buf[328] = {42};
+
+ // Read original value
+ ExessVariant value = {EXESS_HEX, {.as_blob = {sizeof(value_buf), value_buf}}};
+ assert(!exess_read_variant(&value, from_datatype, from_string).status);
+
+ // Coerce to target datatype
+ const ExessVariant coerced = exess_coerce(value, to_datatype, EXESS_LOSSLESS);
+ assert(coerced.datatype == to_datatype);
+
+ // Write coerced value and check string against expectation
+ assert(!exess_write_variant(coerced, sizeof(buf), buf).status);
+ assert(!strcmp(buf, to_string));
+
+ // Coerce the value back to the original type
+ const ExessVariant tripped =
+ exess_coerce(coerced, from_datatype, EXESS_LOSSLESS);
+ assert(tripped.datatype == from_datatype);
+
+ // Write round-tripped value and check string against the original
+ assert(!exess_write_variant(tripped, sizeof(buf), buf).status);
+ assert(!strcmp(buf, from_string));
+}
+
+static void
+check_one_way(const ExessDatatype from_datatype,
+ const char* const from_string,
+ const ExessCoercionFlags coercions,
+ const ExessDatatype to_datatype,
+ const char* const to_string)
+{
+ char buf[328] = {42};
+
+ // Read original value
+ ExessVariant value = {EXESS_NOTHING, {EXESS_SUCCESS}};
+ assert(!exess_read_variant(&value, from_datatype, from_string).status);
+
+ // Coerce to target datatype
+ ExessVariant coerced = exess_coerce(value, to_datatype, coercions);
+ assert(coerced.datatype == to_datatype);
+
+ // Write coerced value and check string against expectation
+ assert(!exess_write_variant(coerced, sizeof(buf), buf).status);
+ assert(!strcmp(buf, to_string));
+}
+
+static void
+check_failure(const ExessDatatype from_datatype,
+ const char* const from_string,
+ const ExessDatatype to_datatype,
+ const ExessStatus expected_status)
+{
+ // Read original value
+ ExessVariant value = {EXESS_NOTHING, {EXESS_SUCCESS}};
+ assert(!exess_read_variant(&value, from_datatype, from_string).status);
+
+ // Try to coerce to target datatype
+ const ExessVariant coerced = exess_coerce(value, to_datatype, EXESS_LOSSLESS);
+ assert(coerced.datatype == EXESS_NOTHING);
+ assert(exess_get_status(&coerced) == expected_status);
+}
+
+static void
+test_unknown(void)
+{
+ ExessVariant long_value = exess_make_long(1);
+ ExessVariant ulong_value = exess_make_ulong(1u);
+ ExessVariant unknown_value = exess_make_nothing(EXESS_SUCCESS);
+
+ assert(exess_coerce(unknown_value, EXESS_LONG, EXESS_LOSSLESS).datatype ==
+ EXESS_NOTHING);
+
+ assert(exess_coerce(unknown_value, EXESS_ULONG, EXESS_LOSSLESS).datatype ==
+ EXESS_NOTHING);
+
+ assert(exess_coerce(long_value, EXESS_NOTHING, EXESS_LOSSLESS).datatype ==
+ EXESS_NOTHING);
+
+ assert(exess_coerce(ulong_value, EXESS_NOTHING, EXESS_LOSSLESS).datatype ==
+ EXESS_NOTHING);
+}
+
+static void
+test_boolean(void)
+{
+ // Exactly from 0 or 1, lossy from 0 or non-zero
+ check_from_to(EXESS_BOOLEAN, "false", EXESS_FLOAT, "0.0E0");
+ check_from_to(EXESS_BOOLEAN, "true", EXESS_FLOAT, "1.0E0");
+ check_from_to(EXESS_BOOLEAN, "false", EXESS_DOUBLE, "0.0E0");
+ check_from_to(EXESS_BOOLEAN, "true", EXESS_DOUBLE, "1.0E0");
+ check_from_to(EXESS_BOOLEAN, "false", EXESS_LONG, "0");
+ check_from_to(EXESS_BOOLEAN, "true", EXESS_LONG, "1");
+ check_failure(EXESS_LONG, "-1", EXESS_BOOLEAN, EXESS_WOULD_TRUNCATE);
+ check_failure(EXESS_LONG, "2", EXESS_BOOLEAN, EXESS_WOULD_TRUNCATE);
+ check_one_way(EXESS_LONG, "42", EXESS_TRUNCATE, EXESS_BOOLEAN, "true");
+ check_one_way(EXESS_LONG, "-1", EXESS_TRUNCATE, EXESS_BOOLEAN, "true");
+ check_from_to(EXESS_BOOLEAN, "false", EXESS_ULONG, "0");
+ check_from_to(EXESS_BOOLEAN, "true", EXESS_ULONG, "1");
+ check_failure(EXESS_LONG, "2", EXESS_BOOLEAN, EXESS_WOULD_TRUNCATE);
+ check_one_way(EXESS_ULONG, "42", EXESS_TRUNCATE, EXESS_BOOLEAN, "true");
+
+ // Not convertible to any time types
+ check_failure(EXESS_BOOLEAN, "true", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_BOOLEAN, "true", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_BOOLEAN, "true", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_BOOLEAN, "true", EXESS_DATE, EXESS_UNSUPPORTED);
+}
+
+static void
+test_long(void)
+{
+ // Truncating boolean conversion
+ check_one_way(EXESS_LONG, "42", EXESS_TRUNCATE, EXESS_BOOLEAN, "true");
+ check_one_way(EXESS_LONG, "-1", EXESS_TRUNCATE, EXESS_BOOLEAN, "true");
+
+ // All smaller integer types
+ check_from_to(EXESS_LONG, "-2147483648", EXESS_INT, "-2147483648");
+ check_failure(EXESS_LONG, "-2147483649", EXESS_INT, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_LONG, "-32768", EXESS_SHORT, "-32768");
+ check_failure(EXESS_LONG, "-32769", EXESS_SHORT, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_LONG, "-128", EXESS_BYTE, "-128");
+ check_failure(EXESS_LONG, "-129", EXESS_BYTE, EXESS_OUT_OF_RANGE);
+
+ // Positive values to/from all unsigned types
+ check_from_to(EXESS_LONG, max_long_str, EXESS_ULONG, max_long_str);
+ check_failure(EXESS_LONG, "-1", EXESS_ULONG, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_LONG, "4294967295", EXESS_UINT, "4294967295");
+ check_failure(EXESS_LONG, "-1", EXESS_UINT, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_LONG, "65535", EXESS_USHORT, "65535");
+ check_failure(EXESS_LONG, "-1", EXESS_USHORT, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_LONG, "255", EXESS_UBYTE, "255");
+ check_failure(EXESS_LONG, "-1", EXESS_UBYTE, EXESS_OUT_OF_RANGE);
+
+ // Any value to/from integer
+ check_from_to(EXESS_LONG, min_long_str, EXESS_INTEGER, min_long_str);
+ check_from_to(EXESS_LONG, max_long_str, EXESS_INTEGER, max_long_str);
+
+ // Non-positive values to/from nonPositiveInteger
+ check_from_to(
+ EXESS_LONG, min_long_str, EXESS_NON_POSITIVE_INTEGER, min_long_str);
+ check_from_to(EXESS_LONG, "0", EXESS_NON_POSITIVE_INTEGER, "0");
+ check_failure(
+ EXESS_LONG, "1", EXESS_NON_POSITIVE_INTEGER, EXESS_OUT_OF_RANGE);
+
+ // Negative values to/from negativeInteger
+ check_from_to(EXESS_LONG, min_long_str, EXESS_NEGATIVE_INTEGER, min_long_str);
+ check_from_to(EXESS_LONG, "-1", EXESS_NEGATIVE_INTEGER, "-1");
+ check_failure(EXESS_LONG, "0", EXESS_NEGATIVE_INTEGER, EXESS_OUT_OF_RANGE);
+
+ // Non-negative values to/from nonNegativeInteger
+ check_failure(
+ EXESS_LONG, "-1", EXESS_NON_NEGATIVE_INTEGER, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_LONG, "0", EXESS_NON_NEGATIVE_INTEGER, "0");
+ check_from_to(
+ EXESS_LONG, max_long_str, EXESS_NON_NEGATIVE_INTEGER, max_long_str);
+
+ // Positive values to/from positiveInteger
+ check_failure(EXESS_LONG, "-1", EXESS_POSITIVE_INTEGER, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_LONG, "0", EXESS_POSITIVE_INTEGER, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_LONG, max_long_str, EXESS_POSITIVE_INTEGER, max_long_str);
+ check_failure(EXESS_POSITIVE_INTEGER,
+ "9223372036854775808",
+ EXESS_LONG,
+ EXESS_OUT_OF_RANGE);
+
+ // Float
+ check_failure(EXESS_FLOAT, "1.5", EXESS_LONG, EXESS_WOULD_ROUND);
+ check_from_to(EXESS_LONG, "-16777215", EXESS_FLOAT, "-1.6777215E7");
+ check_failure(EXESS_LONG, "-16777216", EXESS_FLOAT, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_FLOAT, "-16777216", EXESS_LONG, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_LONG, "16777215", EXESS_FLOAT, "1.6777215E7");
+ check_failure(EXESS_LONG, "16777216", EXESS_FLOAT, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_FLOAT, "16777216", EXESS_LONG, EXESS_OUT_OF_RANGE);
+ check_one_way(EXESS_FLOAT, "1.0", EXESS_LOSSLESS, EXESS_LONG, "1");
+ check_one_way(EXESS_FLOAT, "1.5", EXESS_ROUND, EXESS_LONG, "2");
+ check_one_way(EXESS_FLOAT, "2.5", EXESS_ROUND, EXESS_LONG, "2");
+ check_one_way(EXESS_FLOAT, "3.5", EXESS_ROUND, EXESS_LONG, "4");
+
+ // Double
+ check_failure(EXESS_DOUBLE, "1.5", EXESS_LONG, EXESS_WOULD_ROUND);
+ check_from_to(
+ EXESS_LONG, "-9007199254740991", EXESS_DOUBLE, "-9.007199254740991E15");
+ check_failure(
+ EXESS_LONG, "-9007199254740992", EXESS_DOUBLE, EXESS_OUT_OF_RANGE);
+ check_failure(
+ EXESS_DOUBLE, "-9007199254740992", EXESS_LONG, EXESS_OUT_OF_RANGE);
+ check_from_to(
+ EXESS_LONG, "9007199254740991", EXESS_DOUBLE, "9.007199254740991E15");
+ check_failure(
+ EXESS_LONG, "9007199254740992", EXESS_DOUBLE, EXESS_OUT_OF_RANGE);
+ check_failure(
+ EXESS_DOUBLE, "9007199254740992", EXESS_LONG, EXESS_OUT_OF_RANGE);
+ check_one_way(EXESS_DOUBLE, "1.0", EXESS_LOSSLESS, EXESS_LONG, "1");
+ check_one_way(EXESS_DOUBLE, "1.5", EXESS_ROUND, EXESS_LONG, "2");
+ check_one_way(EXESS_DOUBLE, "2.5", EXESS_ROUND, EXESS_LONG, "2");
+ check_one_way(EXESS_DOUBLE, "3.5", EXESS_ROUND, EXESS_LONG, "4");
+}
+
+static void
+test_ulong(void)
+{
+ ExessVariant unknown = {EXESS_NOTHING, {EXESS_SUCCESS}};
+
+ assert(exess_coerce(unknown, EXESS_ULONG, EXESS_LOSSLESS).datatype ==
+ EXESS_NOTHING);
+
+ // Truncating boolean conversion
+ check_one_way(EXESS_ULONG, "42", EXESS_TRUNCATE, EXESS_BOOLEAN, "true");
+
+ // All integer types
+ check_from_to(EXESS_ULONG, max_long_str, EXESS_LONG, max_long_str);
+ check_failure(EXESS_ULONG, max_ulong_str, EXESS_LONG, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_ULONG, "2147483647", EXESS_INT, "2147483647");
+ check_failure(EXESS_ULONG, "2147483648", EXESS_INT, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_INT, "-1", EXESS_ULONG, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_ULONG, "32767", EXESS_SHORT, "32767");
+ check_failure(EXESS_ULONG, "32768", EXESS_SHORT, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_SHORT, "-1", EXESS_ULONG, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_ULONG, "127", EXESS_BYTE, "127");
+ check_failure(EXESS_ULONG, "128", EXESS_BYTE, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_BYTE, "-1", EXESS_ULONG, EXESS_OUT_OF_RANGE);
+
+ // All unsigned types
+ check_from_to(EXESS_ULONG, "4294967295", EXESS_UINT, "4294967295");
+ check_failure(EXESS_ULONG, "4294967296", EXESS_UINT, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_ULONG, "65535", EXESS_USHORT, "65535");
+ check_failure(EXESS_ULONG, "65536", EXESS_USHORT, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_ULONG, "255", EXESS_UBYTE, "255");
+ check_failure(EXESS_ULONG, "256", EXESS_UBYTE, EXESS_OUT_OF_RANGE);
+
+ // Small enough value Any value to/from integer
+ check_from_to(EXESS_ULONG, "0", EXESS_INTEGER, "0");
+ check_failure(
+ EXESS_ULONG, "9223372036854775808", EXESS_INTEGER, EXESS_OUT_OF_RANGE);
+
+ // Only zero to/from nonPositiveInteger
+ check_from_to(EXESS_ULONG, "0", EXESS_NON_POSITIVE_INTEGER, "0");
+ check_failure(
+ EXESS_ULONG, "1", EXESS_NON_POSITIVE_INTEGER, EXESS_OUT_OF_RANGE);
+
+ // Not convertible to/from negativeInteger
+ check_failure(EXESS_ULONG, "0", EXESS_NEGATIVE_INTEGER, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_ULONG, "1", EXESS_NEGATIVE_INTEGER, EXESS_OUT_OF_RANGE);
+
+ // Any value to/from nonNegativeInteger
+ check_from_to(EXESS_ULONG, "0", EXESS_NON_NEGATIVE_INTEGER, "0");
+ check_from_to(
+ EXESS_ULONG, max_ulong_str, EXESS_NON_NEGATIVE_INTEGER, max_ulong_str);
+
+ // Positive values to/from positiveInteger
+ check_failure(EXESS_ULONG, "0", EXESS_POSITIVE_INTEGER, EXESS_OUT_OF_RANGE);
+ check_from_to(EXESS_ULONG, "1", EXESS_POSITIVE_INTEGER, "1");
+
+ // Float
+ check_failure(EXESS_FLOAT, "-1", EXESS_ULONG, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_FLOAT, "1.5", EXESS_ULONG, EXESS_WOULD_ROUND);
+ check_from_to(EXESS_ULONG, "0", EXESS_FLOAT, "0.0E0");
+ check_from_to(EXESS_ULONG, "16777215", EXESS_FLOAT, "1.6777215E7");
+ check_failure(EXESS_ULONG, "16777216", EXESS_FLOAT, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_FLOAT, "16777216", EXESS_ULONG, EXESS_OUT_OF_RANGE);
+ check_one_way(EXESS_FLOAT, "1.0", EXESS_LOSSLESS, EXESS_ULONG, "1");
+ check_one_way(EXESS_FLOAT, "1.5", EXESS_ROUND, EXESS_ULONG, "2");
+ check_one_way(EXESS_FLOAT, "2.5", EXESS_ROUND, EXESS_ULONG, "2");
+ check_one_way(EXESS_FLOAT, "3.5", EXESS_ROUND, EXESS_ULONG, "4");
+
+ // Double
+ check_failure(EXESS_DOUBLE, "-1", EXESS_ULONG, EXESS_OUT_OF_RANGE);
+ check_failure(EXESS_DOUBLE, "1.5", EXESS_ULONG, EXESS_WOULD_ROUND);
+ check_from_to(EXESS_ULONG, "0", EXESS_DOUBLE, "0.0E0");
+ check_from_to(
+ EXESS_ULONG, "9007199254740991", EXESS_DOUBLE, "9.007199254740991E15");
+ check_failure(
+ EXESS_ULONG, "9007199254740992", EXESS_DOUBLE, EXESS_OUT_OF_RANGE);
+ check_failure(
+ EXESS_DOUBLE, "9007199254740992", EXESS_ULONG, EXESS_OUT_OF_RANGE);
+ check_one_way(EXESS_DOUBLE, "1.0", EXESS_LOSSLESS, EXESS_ULONG, "1");
+ check_one_way(EXESS_DOUBLE, "1.5", EXESS_ROUND, EXESS_ULONG, "2");
+ check_one_way(EXESS_DOUBLE, "2.5", EXESS_ROUND, EXESS_ULONG, "2");
+ check_one_way(EXESS_DOUBLE, "3.5", EXESS_ROUND, EXESS_ULONG, "4");
+}
+
+static void
+test_large_integers(void)
+{
+ check_failure(EXESS_TIME, "00:00:00", EXESS_INTEGER, EXESS_UNSUPPORTED);
+ check_failure(
+ EXESS_TIME, "00:00:00", EXESS_NON_POSITIVE_INTEGER, EXESS_UNSUPPORTED);
+ check_failure(
+ EXESS_TIME, "00:00:00", EXESS_NEGATIVE_INTEGER, EXESS_UNSUPPORTED);
+ check_failure(
+ EXESS_TIME, "00:00:00", EXESS_NON_NEGATIVE_INTEGER, EXESS_UNSUPPORTED);
+ check_failure(
+ EXESS_TIME, "00:00:00", EXESS_POSITIVE_INTEGER, EXESS_UNSUPPORTED);
+}
+
+static void
+test_coerce(void)
+{
+ check_one_way(EXESS_DOUBLE,
+ "1.0000000000001",
+ EXESS_REDUCE_PRECISION,
+ EXESS_FLOAT,
+ "1.0E0");
+
+ check_failure(
+ EXESS_DOUBLE, "1.0000000000001", EXESS_FLOAT, EXESS_WOULD_REDUCE_PRECISION);
+
+ check_one_way(EXESS_FLOAT, "1.5", EXESS_LOSSLESS, EXESS_DOUBLE, "1.5E0");
+
+ /* check_failure( */
+ /* EXESS_LONG, "9007199254740993", EXESS_DOUBLE, EXESS_OUT_OF_RANGE); */
+
+ /* check_failure(EXESS_FLOAT, "1.0", EXESS_LONG, EXESS_SUCCESS); */
+ /* check_from_to(EXESS_BYTE, "-128", EXESS_LONG, "-128"); */
+
+ // DateTime
+}
+
+static void
+test_date_time(void)
+{
+ check_failure(
+ EXESS_DATETIME, "2001-02-03T04:05:06", EXESS_TIME, EXESS_WOULD_TRUNCATE);
+
+ check_one_way(EXESS_DATETIME,
+ "2001-02-03T04:05:06",
+ EXESS_TRUNCATE,
+ EXESS_TIME,
+ "04:05:06");
+
+ check_one_way(EXESS_DATETIME,
+ "2001-02-03T04:05:06Z",
+ EXESS_TRUNCATE,
+ EXESS_TIME,
+ "04:05:06Z");
+
+ check_failure(
+ EXESS_DATETIME, "2001-02-03T04:05:06", EXESS_DATE, EXESS_WOULD_TRUNCATE);
+
+ check_one_way(EXESS_DATETIME,
+ "2001-02-03T04:05:06",
+ EXESS_TRUNCATE,
+ EXESS_DATE,
+ "2001-02-03");
+
+ check_one_way(EXESS_DATETIME,
+ "2001-02-03T04:05:06Z",
+ EXESS_TRUNCATE,
+ EXESS_DATE,
+ "2001-02-03Z");
+}
+
+static void
+test_number_to_time(void)
+{
+ check_failure(EXESS_BOOLEAN, "true", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_BOOLEAN, "true", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_BOOLEAN, "true", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_BOOLEAN, "true", EXESS_DATE, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_LONG, "1", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_LONG, "1", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_LONG, "1", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_LONG, "1", EXESS_DATE, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_INT, "1", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_INT, "1", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_INT, "1", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_INT, "1", EXESS_DATE, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_SHORT, "1", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_SHORT, "1", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_SHORT, "1", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_SHORT, "1", EXESS_DATE, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_BYTE, "1", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_BYTE, "1", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_BYTE, "1", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_BYTE, "1", EXESS_DATE, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_ULONG, "1", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_ULONG, "1", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_ULONG, "1", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_ULONG, "1", EXESS_DATE, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_UINT, "1", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_UINT, "1", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_UINT, "1", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_UINT, "1", EXESS_DATE, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_USHORT, "1", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_USHORT, "1", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_USHORT, "1", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_USHORT, "1", EXESS_DATE, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_UBYTE, "1", EXESS_DURATION, EXESS_UNSUPPORTED);
+ check_failure(EXESS_UBYTE, "1", EXESS_DATETIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_UBYTE, "1", EXESS_TIME, EXESS_UNSUPPORTED);
+ check_failure(EXESS_UBYTE, "1", EXESS_DATE, EXESS_UNSUPPORTED);
+}
+
+static void
+test_time_to_number(void)
+{
+ static const char* const duration_str = "P1Y";
+ static const char* const datetime_str = "2001-02-03T04:05:06";
+ static const char* const time_str = "04:05:06";
+ static const char* const date_str = "2001-02-03";
+
+ check_failure(EXESS_DURATION, duration_str, EXESS_BOOLEAN, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATETIME, datetime_str, EXESS_BOOLEAN, EXESS_UNSUPPORTED);
+ check_failure(EXESS_TIME, time_str, EXESS_BOOLEAN, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATE, date_str, EXESS_BOOLEAN, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_DURATION, duration_str, EXESS_INT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATETIME, datetime_str, EXESS_INT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_TIME, time_str, EXESS_INT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATE, date_str, EXESS_INT, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_DURATION, duration_str, EXESS_SHORT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATETIME, datetime_str, EXESS_SHORT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_TIME, time_str, EXESS_SHORT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATE, date_str, EXESS_SHORT, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_DURATION, duration_str, EXESS_BYTE, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATETIME, datetime_str, EXESS_BYTE, EXESS_UNSUPPORTED);
+ check_failure(EXESS_TIME, time_str, EXESS_BYTE, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATE, date_str, EXESS_BYTE, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_DURATION, duration_str, EXESS_ULONG, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATETIME, datetime_str, EXESS_ULONG, EXESS_UNSUPPORTED);
+ check_failure(EXESS_TIME, time_str, EXESS_ULONG, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATE, date_str, EXESS_ULONG, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_DURATION, duration_str, EXESS_UINT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATETIME, datetime_str, EXESS_UINT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_TIME, time_str, EXESS_UINT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATE, date_str, EXESS_UINT, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_DURATION, duration_str, EXESS_USHORT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATETIME, datetime_str, EXESS_USHORT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_TIME, time_str, EXESS_USHORT, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATE, date_str, EXESS_USHORT, EXESS_UNSUPPORTED);
+
+ check_failure(EXESS_DURATION, duration_str, EXESS_UBYTE, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATETIME, datetime_str, EXESS_UBYTE, EXESS_UNSUPPORTED);
+ check_failure(EXESS_TIME, time_str, EXESS_UBYTE, EXESS_UNSUPPORTED);
+ check_failure(EXESS_DATE, date_str, EXESS_UBYTE, EXESS_UNSUPPORTED);
+}
+
+static void
+test_binary(void)
+{
+ check_from_to(EXESS_HEX, "666F6F", EXESS_BASE64, "Zm9v");
+
+ check_failure(EXESS_LONG, "-2147483649", EXESS_HEX, EXESS_UNSUPPORTED);
+ check_failure(EXESS_LONG, "-2147483649", EXESS_BASE64, EXESS_UNSUPPORTED);
+
+ /* check_from_to(EXESS_BASE64, "Zm9v", EXESS_HEX, "666F6F"); */
+
+ /* ////////// */
+
+ /* check_one_way(EXESS_LONG, "-1", EXESS_TRUNCATE, EXESS_BOOLEAN, "true"); */
+
+ /* // All smaller integer types */
+ /* check_from_to(EXESS_LONG, "-2147483648", EXESS_INT, "-2147483648"); */
+ /* check_failure(EXESS_LONG, "-2147483649", EXESS_INT, EXESS_OUT_OF_RANGE); */
+}
+
+int
+main(void)
+{
+ test_unknown();
+ test_boolean();
+ test_long();
+ test_ulong();
+ test_large_integers();
+ test_coerce();
+ test_date_time();
+ test_number_to_time();
+ test_time_to_number();
+ test_binary();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_datatype.c b/subprojects/exess/test/test_datatype.c
new file mode 100644
index 00000000..7ad3fb5e
--- /dev/null
+++ b/subprojects/exess/test/test_datatype.c
@@ -0,0 +1,81 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+
+static void
+test_datatype_uris(void)
+{
+ assert(!exess_datatype_uri(EXESS_NOTHING));
+ assert(!exess_datatype_uri((ExessDatatype)(EXESS_BASE64 + 1)));
+
+ for (unsigned i = 1; i <= EXESS_BASE64; ++i) {
+ const char* const uri = exess_datatype_uri((ExessDatatype)i);
+
+ assert(uri);
+ assert(exess_datatype_from_uri(uri) == i);
+ }
+
+ assert(exess_datatype_from_uri(EXESS_XSD_URI) == EXESS_NOTHING);
+ assert(exess_datatype_from_uri(EXESS_XSD_URI "unknown") == EXESS_NOTHING);
+ assert(exess_datatype_from_uri("garbage") == EXESS_NOTHING);
+ assert(exess_datatype_from_uri("http://example.org/very/long/unknown/uri") ==
+ EXESS_NOTHING);
+}
+
+static void
+test_datatype_is_bounded(void)
+{
+ assert(!exess_datatype_is_bounded(EXESS_NOTHING));
+ assert(exess_datatype_is_bounded(EXESS_BOOLEAN));
+ assert(!exess_datatype_is_bounded(EXESS_DECIMAL));
+ assert(exess_datatype_is_bounded(EXESS_DOUBLE));
+ assert(exess_datatype_is_bounded(EXESS_FLOAT));
+ assert(!exess_datatype_is_bounded(EXESS_INTEGER));
+ assert(!exess_datatype_is_bounded(EXESS_NON_POSITIVE_INTEGER));
+ assert(!exess_datatype_is_bounded(EXESS_NEGATIVE_INTEGER));
+ assert(exess_datatype_is_bounded(EXESS_LONG));
+ assert(exess_datatype_is_bounded(EXESS_INT));
+ assert(exess_datatype_is_bounded(EXESS_SHORT));
+ assert(exess_datatype_is_bounded(EXESS_BYTE));
+ assert(!exess_datatype_is_bounded(EXESS_NON_NEGATIVE_INTEGER));
+ assert(exess_datatype_is_bounded(EXESS_ULONG));
+ assert(exess_datatype_is_bounded(EXESS_UINT));
+ assert(exess_datatype_is_bounded(EXESS_USHORT));
+ assert(exess_datatype_is_bounded(EXESS_UBYTE));
+ assert(!exess_datatype_is_bounded(EXESS_POSITIVE_INTEGER));
+ assert(exess_datatype_is_bounded(EXESS_DURATION));
+ assert(exess_datatype_is_bounded(EXESS_DATETIME));
+ assert(exess_datatype_is_bounded(EXESS_TIME));
+ assert(exess_datatype_is_bounded(EXESS_DATE));
+ assert(!exess_datatype_is_bounded(EXESS_HEX));
+ assert(!exess_datatype_is_bounded(EXESS_BASE64));
+}
+
+int
+main(int argc, char** argv)
+{
+ (void)argv;
+
+ test_datatype_uris();
+ test_datatype_is_bounded();
+
+ return argc == 1 ? 0 : 1;
+}
diff --git a/subprojects/exess/test/test_date.c b/subprojects/exess/test/test_date.c
new file mode 100644
index 00000000..0ac8129a
--- /dev/null
+++ b/subprojects/exess/test/test_date.c
@@ -0,0 +1,257 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "date_utils.h"
+#include "int_test_data.h"
+#include "macros.h"
+#include "num_test_utils.h"
+#include "time_test_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+static const ExessDate nozone = {2001, 1, 2, {EXESS_LOCAL}};
+static const ExessDate utc = {2002, 2, 3, {0}};
+static const ExessDate zoned = {2003, 3, 4, INIT_ZONE(11, 30)};
+static const ExessDate early = {99, 3, 4, INIT_ZONE(11, 30)};
+static const ExessDate future = {12345, 3, 4, INIT_ZONE(11, 30)};
+static const ExessDate lowest = {INT16_MIN, 1, 1, INIT_ZONE(-14, 0)};
+static const ExessDate highest = {INT16_MAX, 1, 1, INIT_ZONE(14, 0)};
+static const ExessDate garbage1 = {2004, 0, 1, INIT_ZONE(11, 30)};
+static const ExessDate garbage2 = {2005, 13, 1, INIT_ZONE(11, 30)};
+static const ExessDate garbage3 = {2006, 1, 0, INIT_ZONE(11, 30)};
+static const ExessDate garbage4 = {2006, 1, 32, INIT_ZONE(11, 30)};
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const int64_t expected_year,
+ const uint8_t expected_month,
+ const uint8_t expected_day,
+ const int8_t expected_tz_hour,
+ const int8_t expected_tz_minute,
+ const bool expected_tz_is_present,
+ const size_t expected_count)
+{
+ ExessDate value = {0, 0, 0, {EXESS_LOCAL}};
+
+ const ExessResult r = exess_read_date(&value, string);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(value.year == expected_year);
+ assert(value.month == expected_month);
+ assert(value.day == expected_day);
+ assert((!expected_tz_is_present && value.zone.quarter_hours == EXESS_LOCAL) ||
+ value.zone.quarter_hours ==
+ 4 * expected_tz_hour + expected_tz_minute / 15);
+}
+
+static void
+test_read_date(void)
+{
+ // No input
+ check_read("", EXESS_EXPECTED_DIGIT, 0, 0, 0, 0, 0, false, 0);
+ check_read(" \f\n\r\t\v", EXESS_EXPECTED_DIGIT, 0, 0, 0, 0, 0, false, 6);
+
+ // Good values
+ check_read("2004-04-12", EXESS_SUCCESS, 2004, 4, 12, 0, 0, false, 10);
+ check_read("-0045-01-01", EXESS_SUCCESS, -45, 1, 1, 0, 0, false, 11);
+ check_read("12004-04-12", EXESS_SUCCESS, 12004, 4, 12, 0, 0, false, 11);
+ check_read("2004-04-12-05:00", EXESS_SUCCESS, 2004, 4, 12, -5, 0, true, 16);
+ check_read("2004-04-12Z", EXESS_SUCCESS, 2004, 4, 12, 0, 0, true, 11);
+ check_read("2001-10-26", EXESS_SUCCESS, 2001, 10, 26, 0, 0, false, 10);
+ check_read("2001-10-26+02:00", EXESS_SUCCESS, 2001, 10, 26, 2, 0, true, 16);
+ check_read("2001-10-26Z", EXESS_SUCCESS, 2001, 10, 26, 0, 0, true, 11);
+ check_read("2001-10-26+00:00", EXESS_SUCCESS, 2001, 10, 26, 0, 0, true, 16);
+ check_read("-2001-10-26", EXESS_SUCCESS, -2001, 10, 26, 0, 0, false, 11);
+ check_read("-20000-04-01", EXESS_SUCCESS, -20000, 04, 01, 0, 0, false, 12);
+
+ // Non-canonical
+ check_read("02004-04-12", EXESS_SUCCESS, 2004, 4, 12, 0, 0, false, 11);
+ check_read(" 02004-04-12 ", EXESS_SUCCESS, 2004, 4, 12, 0, 0, false, 12);
+
+ // Good common year values
+ check_read("1900-02-28", EXESS_SUCCESS, 1900, 2, 28, 0, 0, false, 10);
+
+ // Good leap year values
+ check_read("2000-02-29", EXESS_SUCCESS, 2000, 2, 29, 0, 0, false, 10);
+ check_read("2004-02-29", EXESS_SUCCESS, 2004, 2, 29, 0, 0, false, 10);
+
+ // Longest possible string
+ check_read("-32768-01-01-14:00",
+ EXESS_SUCCESS,
+ -32768,
+ 1,
+ 1,
+ -14,
+ 0,
+ true,
+ EXESS_MAX_DATE_LENGTH);
+
+ // Limits
+ check_read("-32768-01-01", EXESS_SUCCESS, -32768, 1, 1, 0, 0, false, 12);
+ check_read("32767-01-01", EXESS_SUCCESS, 32767, 1, 1, 0, 0, false, 11);
+
+ // Out of range years
+ check_read("-32769-01-01", EXESS_OUT_OF_RANGE, 0, 0, 0, 0, 0, false, 6);
+ check_read("32768-01-01", EXESS_OUT_OF_RANGE, 0, 0, 0, 0, 0, false, 5);
+
+ // Out of range months
+ check_read("2001-00-26", EXESS_OUT_OF_RANGE, 2001, 0, 0, 0, 0, false, 7);
+ check_read("2001-13-26", EXESS_OUT_OF_RANGE, 2001, 13, 0, 0, 0, false, 7);
+ check_read("2001-10-00", EXESS_OUT_OF_RANGE, 2001, 10, 0, 0, 0, false, 10);
+ check_read("2001-10-32", EXESS_OUT_OF_RANGE, 2001, 10, 32, 0, 0, false, 10);
+
+ // Out of range days
+ check_read("2001-01-32", EXESS_OUT_OF_RANGE, 2001, 1, 32, 0, 0, false, 10);
+ check_read("2001-02-29", EXESS_OUT_OF_RANGE, 2001, 2, 29, 0, 0, false, 10);
+ check_read("2001-03-32", EXESS_OUT_OF_RANGE, 2001, 3, 32, 0, 0, false, 10);
+ check_read("2001-04-31", EXESS_OUT_OF_RANGE, 2001, 4, 31, 0, 0, false, 10);
+ check_read("2001-05-32", EXESS_OUT_OF_RANGE, 2001, 5, 32, 0, 0, false, 10);
+ check_read("2001-06-31", EXESS_OUT_OF_RANGE, 2001, 6, 31, 0, 0, false, 10);
+ check_read("2001-07-32", EXESS_OUT_OF_RANGE, 2001, 7, 32, 0, 0, false, 10);
+ check_read("2001-08-32", EXESS_OUT_OF_RANGE, 2001, 8, 32, 0, 0, false, 10);
+ check_read("2001-09-31", EXESS_OUT_OF_RANGE, 2001, 9, 31, 0, 0, false, 10);
+ check_read("2001-10-32", EXESS_OUT_OF_RANGE, 2001, 10, 32, 0, 0, false, 10);
+ check_read("2001-11-31", EXESS_OUT_OF_RANGE, 2001, 11, 31, 0, 0, false, 10);
+ check_read("2001-12-32", EXESS_OUT_OF_RANGE, 2001, 12, 32, 0, 0, false, 10);
+
+ // Garbage
+ check_read("f", EXESS_EXPECTED_DIGIT, 0, 0, 0, 0, 0, false, 0);
+ check_read("99-04-12", EXESS_EXPECTED_DIGIT, 99, 0, 0, 0, 0, false, 2);
+ check_read("2004-4-2", EXESS_EXPECTED_DIGIT, 2004, 4, 0, 0, 0, false, 6);
+ check_read("2004/04/02", EXESS_EXPECTED_DASH, 2004, 0, 0, 0, 0, false, 4);
+ check_read("04-12-2004", EXESS_EXPECTED_DIGIT, 4, 0, 0, 0, 0, false, 2);
+ check_read("2001-10", EXESS_EXPECTED_DASH, 2001, 10, 0, 0, 0, false, 7);
+ check_read("01-10-26", EXESS_EXPECTED_DIGIT, 1, 0, 0, 0, 0, false, 2);
+ check_read("2004-04-12A", EXESS_EXPECTED_SIGN, 2004, 4, 12, 0, 0, false, 10);
+}
+
+static void
+check_write(const ExessDate value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_DATE_LENGTH + 1] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_date(value, buf_size, buf);
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert(r.status || exess_write_date(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_date(void)
+{
+ check_write(nozone, EXESS_SUCCESS, 11, "2001-01-02");
+ check_write(utc, EXESS_SUCCESS, 12, "2002-02-03Z");
+ check_write(zoned, EXESS_SUCCESS, 17, "2003-03-04+11:30");
+ check_write(early, EXESS_SUCCESS, 17, "0099-03-04+11:30");
+ check_write(future, EXESS_SUCCESS, 18, "12345-03-04+11:30");
+ check_write(lowest, EXESS_SUCCESS, 19, "-32768-01-01-14:00");
+ check_write(highest, EXESS_SUCCESS, 18, "32767-01-01+14:00");
+
+ check_write(garbage1, EXESS_BAD_VALUE, 14, "");
+ check_write(garbage2, EXESS_BAD_VALUE, 14, "");
+ check_write(garbage3, EXESS_BAD_VALUE, 14, "");
+ check_write(garbage4, EXESS_BAD_VALUE, 14, "");
+
+ check_write(nozone, EXESS_NO_SPACE, 10, "");
+ check_write(future, EXESS_NO_SPACE, 5, "");
+ check_write(lowest, EXESS_NO_SPACE, 5, "");
+ check_write(highest, EXESS_NO_SPACE, 5, "");
+ check_write(utc, EXESS_NO_SPACE, 11, "");
+ check_write(zoned, EXESS_NO_SPACE, 16, "");
+
+ // Check that nothing is written when there isn't enough space
+ char c = 42;
+ const ExessResult r = exess_write_date(nozone, 0, &c);
+ assert(c == 42);
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 0);
+}
+
+static void
+check_round_trip(const ExessDate value)
+{
+ ExessDate parsed_value = {0, 0, 0, {0}};
+ char buf[EXESS_MAX_DATE_LENGTH + 1] = {0};
+
+ assert(!exess_write_date(value, sizeof(buf), buf).status);
+ assert(!exess_read_date(&parsed_value, buf).status);
+ assert(parsed_value.year == value.year);
+ assert(parsed_value.month == value.month);
+ assert(parsed_value.day == value.day);
+ assert(parsed_value.zone.quarter_hours == value.zone.quarter_hours);
+}
+
+static void
+test_round_trip(const ExessNumTestOptions opts)
+{
+ fprintf(stderr, "Testing xsd:gDate randomly with seed %u\n", opts.seed);
+
+ const uint64_t n_tests = MAX(256, opts.n_tests / 16);
+
+ uint32_t rng = opts.seed;
+ for (uint64_t i = 0; i < n_tests; ++i) {
+ rng = lcg32(rng);
+
+ const int16_t year = (int16_t)(rng % UINT16_MAX);
+ for (uint8_t month = 1; month < 13; ++month) {
+ for (uint8_t day = 1; day <= days_in_month(year, month); ++day) {
+ const ExessDate no_zone = {year, month, day, {EXESS_LOCAL}};
+ const ExessDate lowest_zone = {year, month, day, {4 * -14 + 0}};
+ const ExessDate highest_zone = {year, month, day, {4 * 14}};
+
+ check_round_trip(no_zone);
+ check_round_trip(lowest_zone);
+ check_round_trip(highest_zone);
+
+ const ExessDate value = {year, month, day, random_timezone(&rng)};
+ check_round_trip(value);
+ }
+ }
+
+ print_num_test_progress(i, n_tests);
+ }
+}
+
+int
+main(int argc, char** argv)
+{
+ const ExessNumTestOptions opts = parse_num_test_options(argc, argv);
+ if (opts.error) {
+ return 1;
+ }
+
+ test_read_date();
+ test_write_date();
+ test_round_trip(opts);
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_datetime.c b/subprojects/exess/test/test_datetime.c
new file mode 100644
index 00000000..d6d2b378
--- /dev/null
+++ b/subprojects/exess/test/test_datetime.c
@@ -0,0 +1,459 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+static const ExessDateTime local = {2001, 2, 3, false, 4, 5, 6, 0};
+static const ExessDateTime utc = {2001, 2, 3, true, 4, 5, 6, 0};
+static const ExessDateTime lowest = {INT16_MIN, 1, 1, false, 0, 0, 0, 0};
+static const ExessDateTime highest = {INT16_MAX, 12, 31, false, 24, 0, 0, 0};
+static const ExessDateTime utc_min = {INT16_MIN, 1, 1, true, 0, 0, 0, 0};
+static const ExessDateTime utc_max = {INT16_MAX, 12, 31, true, 24, 0, 0, 0};
+static const ExessDateTime nano = {2001, 1, 1, false, 0, 0, 0, 1};
+static const ExessDateTime garbage1 = {2004, 0, 1, false, 12, 0, 0, 0};
+static const ExessDateTime garbage2 = {2005, 13, 1, false, 12, 0, 0, 0};
+static const ExessDateTime garbage3 = {2006, 1, 0, false, 12, 0, 0, 0};
+static const ExessDateTime garbage4 = {2006, 1, 32, false, 12, 0, 0, 0};
+static const ExessDateTime garbage5 = {2001, 2, 3, false, 0, 0, 0, 1000000000};
+static const ExessDateTime garbage6 = {2001, 2, 3, false, 0, 0, 60, 0};
+static const ExessDateTime garbage7 = {2001, 2, 3, false, 0, 60, 0, 0};
+static const ExessDateTime garbage8 = {2001, 2, 3, false, 24, 0, 0, 1};
+static const ExessDateTime garbage9 = {2001, 2, 3, false, 24, 0, 1, 0};
+static const ExessDateTime garbage10 = {2001, 2, 3, false, 24, 1, 0, 0};
+static const ExessDateTime garbage11 = {2001, 2, 3, false, 25, 0, 0, 0};
+
+static void
+check_add(const char* const datetime_string,
+ const char* const duration_string,
+ const char* const result_string)
+{
+ ExessDateTime datetime = {0, 0u, 0u, false, 0u, 0u, 0u, 0u};
+ ExessDuration duration = {0u, 0u, 0u};
+
+ ExessResult r = exess_read_datetime(&datetime, datetime_string);
+ assert(!r.status);
+
+ r = exess_read_duration(&duration, duration_string);
+ assert(!r.status);
+
+ const ExessDateTime result = exess_add_datetime_duration(datetime, duration);
+ char buf[28] = {0};
+
+ r = exess_write_datetime(result, sizeof(buf), buf);
+ assert(!r.status);
+ assert(!strcmp(buf, result_string));
+}
+
+static void
+check_is_underflow(const ExessDateTime datetime, const bool is_utc)
+{
+ assert(datetime.year == INT16_MIN);
+ assert(datetime.month == 0);
+ assert(datetime.day == 0);
+ assert(datetime.is_utc == is_utc);
+ assert(datetime.hour == 0);
+ assert(datetime.minute == 0);
+ assert(datetime.second == 0);
+ assert(datetime.nanosecond == 0);
+}
+
+static void
+check_is_overflow(const ExessDateTime datetime, const bool is_utc)
+{
+ assert(datetime.year == INT16_MAX);
+ assert(datetime.month == UINT8_MAX);
+ assert(datetime.day == UINT8_MAX);
+ assert(datetime.is_utc == is_utc);
+ assert(datetime.hour == UINT8_MAX);
+ assert(datetime.minute == UINT8_MAX);
+ assert(datetime.second == UINT8_MAX);
+ assert(datetime.nanosecond == UINT32_MAX);
+}
+
+static void
+test_add(void)
+{
+ // Simple cases
+ check_add("2001-01-01T00:00:00", "PT1.5S", "2001-01-01T00:00:01.5");
+ check_add("2001-01-01T00:00:00", "PT1M", "2001-01-01T00:01:00");
+ check_add("2001-01-01T00:00:00", "PT1H", "2001-01-01T01:00:00");
+ check_add("2001-01-01T00:00:00", "P1D", "2001-01-02T00:00:00");
+ check_add("2001-01-01T00:00:00", "P1M", "2001-02-01T00:00:00");
+ check_add("2001-01-01T00:00:00", "P1Y", "2002-01-01T00:00:00");
+ check_add("2001-02-02T02:02:02", "-PT1.5S", "2001-02-02T02:02:00.5");
+ check_add("2001-02-02T02:02:02", "-PT1M", "2001-02-02T02:01:02");
+ check_add("2001-02-02T02:02:02", "-PT1H", "2001-02-02T01:02:02");
+ check_add("2001-02-02T02:02:02", "-P1D", "2001-02-01T02:02:02");
+ check_add("2001-02-02T02:02:02", "-P1M", "2001-01-02T02:02:02");
+ check_add("2001-02-02T02:02:02", "-P1Y", "2000-02-02T02:02:02");
+
+ // Positive carrying
+ check_add("2001-01-01T00:00:59", "PT1S", "2001-01-01T00:01:00");
+ check_add("2001-01-01T00:59:00", "PT1M", "2001-01-01T01:00:00");
+ check_add("2001-01-01T23:00:00", "PT1H", "2001-01-02T00:00:00");
+ check_add("2001-01-31T00:00:00", "P1D", "2001-02-01T00:00:00");
+ check_add("2001-12-01T00:00:00", "P1M", "2002-01-01T00:00:00");
+
+ // Negative carrying
+ check_add("2001-01-01T00:01:00", "-PT1S", "2001-01-01T00:00:59");
+ check_add("2001-02-01T01:00:00", "-PT1M", "2001-02-01T00:59:00");
+ check_add("2001-02-02T00:00:00", "-PT1H", "2001-02-01T23:00:00");
+ check_add("2001-02-01T00:00:00", "-P1D", "2001-01-31T00:00:00");
+ check_add("2001-01-01T00:00:00", "-P1M", "2000-12-01T00:00:00");
+
+ // Underflow and overflow
+
+ static const ExessDuration minus_month = {-1, 0, 0};
+ static const ExessDuration minus_second = {0, -1, 0};
+ static const ExessDuration minus_nanosecond = {0, 0, -1};
+ static const ExessDuration plus_month = {1, 0, 0};
+ static const ExessDuration plus_second = {0, 1, 0};
+ static const ExessDuration plus_nanosecond = {0, 0, 1};
+
+ check_is_underflow(exess_add_datetime_duration(lowest, minus_month), false);
+ check_is_underflow(exess_add_datetime_duration(lowest, minus_second), false);
+ check_is_underflow(exess_add_datetime_duration(lowest, minus_nanosecond),
+ false);
+
+ check_is_underflow(exess_add_datetime_duration(utc_min, minus_month), true);
+ check_is_underflow(exess_add_datetime_duration(utc_min, minus_second), true);
+ check_is_underflow(exess_add_datetime_duration(utc_min, minus_nanosecond),
+ true);
+
+ check_is_overflow(exess_add_datetime_duration(highest, plus_month), false);
+ check_is_overflow(exess_add_datetime_duration(highest, plus_second), false);
+ check_is_overflow(exess_add_datetime_duration(highest, plus_nanosecond),
+ false);
+
+ check_is_overflow(exess_add_datetime_duration(utc_max, plus_month), true);
+ check_is_overflow(exess_add_datetime_duration(utc_max, plus_second), true);
+ check_is_overflow(exess_add_datetime_duration(utc_max, plus_nanosecond),
+ true);
+}
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const size_t expected_count,
+ const int64_t expected_year,
+ const uint8_t expected_month,
+ const uint8_t expected_day,
+ const uint8_t expected_hour,
+ const uint8_t expected_minute,
+ const uint8_t expected_second,
+ const uint32_t expected_nanosecond,
+ const bool expected_is_utc)
+{
+ ExessDateTime value = {0, 0, 0, false, 0, 0, 0, 0};
+
+ const ExessResult r = exess_read_datetime(&value, string);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(value.year == expected_year);
+ assert(value.month == expected_month);
+ assert(value.day == expected_day);
+ assert(value.hour == expected_hour);
+ assert(value.minute == expected_minute);
+ assert(value.second == expected_second);
+ assert(value.nanosecond == expected_nanosecond);
+ assert(value.is_utc == expected_is_utc);
+}
+
+static void
+test_read_datetime(void)
+{
+ // Simple values
+
+ check_read(
+ "2001-02-03T04:05:06", EXESS_SUCCESS, 19, 2001, 2, 3, 4, 5, 6, 0, false);
+
+ check_read(
+ "2001-02-03T04:05:06Z", EXESS_SUCCESS, 20, 2001, 2, 3, 4, 5, 6, 0, true);
+
+ check_read("2004-04-12T13:20:15.5",
+ EXESS_SUCCESS,
+ 21,
+ 2004,
+ 4,
+ 12,
+ 13,
+ 20,
+ 15,
+ 500000000,
+ false);
+
+ check_read("-32768-01-01T00:00:00.000000001Z",
+ EXESS_SUCCESS,
+ EXESS_MAX_DATETIME_LENGTH,
+ -32768,
+ 1,
+ 1,
+ 0,
+ 0,
+ 0,
+ 1,
+ true);
+
+ // Simple timezones
+
+ check_read("2001-02-03T04:05:06-00:30",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 2,
+ 3,
+ 4,
+ 35,
+ 6,
+ 0,
+ true);
+
+ check_read("2001-02-03T04:05:06-01:00",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 2,
+ 3,
+ 5,
+ 5,
+ 6,
+ 0,
+ true);
+
+ check_read("2001-02-03T04:05:06+00:30",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 2,
+ 3,
+ 3,
+ 35,
+ 6,
+ 0,
+ true);
+
+ check_read("2001-02-03T04:05:06+01:00",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 2,
+ 3,
+ 3,
+ 5,
+ 6,
+ 0,
+ true);
+
+ // Positive timezone carry
+
+ // Minute => hour
+ check_read("2001-02-03T04:46:00-00:15",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 2,
+ 3,
+ 5,
+ 1,
+ 0,
+ 0,
+ true);
+
+ // Minute => hour => day
+ check_read("2001-02-03T23:46:00-00:15",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 2,
+ 4,
+ 0,
+ 1,
+ 0,
+ 0,
+ true);
+
+ // Minute => hour => day => month
+ check_read("2001-02-28T23:46:00-00:15",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 3,
+ 1,
+ 0,
+ 1,
+ 0,
+ 0,
+ true);
+
+ // Minute => hour => day => month => year
+ check_read("2001-12-31T23:46:00-00:15",
+ EXESS_SUCCESS,
+ 25,
+ 2002,
+ 1,
+ 1,
+ 0,
+ 1,
+ 0,
+ 0,
+ true);
+
+ // Negative timezone carry
+
+ // Minute => hour
+ check_read("2001-02-03T04:14:00+00:15",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 2,
+ 3,
+ 3,
+ 59,
+ 0,
+ 0,
+ true);
+
+ // Minute => hour => day
+ check_read("2001-02-03T00:14:00+00:15",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 2,
+ 2,
+ 23,
+ 59,
+ 0,
+ 0,
+ true);
+
+ // Minute => hour => day => month
+ check_read("2001-02-01T00:14:00+00:15",
+ EXESS_SUCCESS,
+ 25,
+ 2001,
+ 1,
+ 31,
+ 23,
+ 59,
+ 0,
+ 0,
+ true);
+
+ // Garbage
+
+ check_read(
+ "2004-04-12T13:00", EXESS_EXPECTED_COLON, 16, 0, 0, 0, 0, 0, 0, 0, false);
+
+ check_read("2004-04-1213:20:00",
+ EXESS_EXPECTED_TIME_SEP,
+ 10,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ false);
+
+ check_read(
+ "99-04-12T13:00", EXESS_EXPECTED_DIGIT, 2, 0, 0, 0, 0, 0, 0, 0, false);
+
+ check_read(
+ "2004-04-12", EXESS_EXPECTED_TIME_SEP, 10, 0, 0, 0, 0, 0, 0, 0, false);
+
+ check_read("2004-04-12-05:00",
+ EXESS_EXPECTED_TIME_SEP,
+ 10,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ false);
+}
+
+static void
+check_write(const ExessDateTime value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_DATETIME_LENGTH + 1] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+ 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_datetime(value, buf_size, buf);
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert(r.status || exess_write_datetime(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_datetime(void)
+{
+ check_write(local, EXESS_SUCCESS, 20, "2001-02-03T04:05:06");
+ check_write(utc, EXESS_SUCCESS, 21, "2001-02-03T04:05:06Z");
+ check_write(lowest, EXESS_SUCCESS, 22, "-32768-01-01T00:00:00");
+ check_write(highest, EXESS_SUCCESS, 21, "32767-12-31T24:00:00");
+ check_write(nano, EXESS_SUCCESS, 30, "2001-01-01T00:00:00.000000001");
+
+ check_write(garbage1, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage2, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage3, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage4, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage5, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage6, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage7, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage8, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage9, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage10, EXESS_BAD_VALUE, 20, "");
+ check_write(garbage11, EXESS_BAD_VALUE, 20, "");
+
+ check_write(lowest, EXESS_NO_SPACE, 12, "");
+ check_write(lowest, EXESS_NO_SPACE, 17, "");
+ check_write(lowest, EXESS_NO_SPACE, 18, "");
+ check_write(lowest, EXESS_NO_SPACE, 21, "");
+
+ // Check that nothing is written when there isn't enough space
+ char c = 42;
+ const ExessResult r = exess_write_datetime(highest, 0, &c);
+ assert(c == 42);
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 0);
+}
+
+int
+main(void)
+{
+ test_add();
+ test_read_datetime();
+ test_write_datetime();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_decimal.c b/subprojects/exess/test/test_decimal.c
new file mode 100644
index 00000000..2a49565d
--- /dev/null
+++ b/subprojects/exess/test/test_decimal.c
@@ -0,0 +1,260 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "float_test_data.h"
+#include "int_test_data.h"
+#include "num_test_utils.h"
+#include "string_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <float.h>
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const double expected_value,
+ const size_t expected_count)
+{
+ double value = 0;
+ const ExessResult r = exess_read_decimal(&value, string);
+
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(double_matches(value, expected_value));
+}
+
+static void
+test_read_decimal(void)
+{
+ // No value
+ check_read("", EXESS_EXPECTED_DIGIT, (double)NAN, 0);
+ check_read(" \f\n\r\t\v", EXESS_EXPECTED_DIGIT, (double)NAN, 6);
+
+ // Basic values
+ check_read("1.2", EXESS_SUCCESS, 1.2, 3);
+ check_read("0.01", EXESS_SUCCESS, 0.01, 4);
+ check_read("10.0", EXESS_SUCCESS, 10.0, 4);
+
+ // Non-canonical form
+ check_read(" \f\n\r\t\v42.24 ", EXESS_SUCCESS, 42.24, 11);
+ check_read("12.", EXESS_SUCCESS, 12., 3);
+ check_read(".34", EXESS_SUCCESS, 0.34, 3);
+ check_read("+.56", EXESS_SUCCESS, 0.56, 4);
+ check_read("-.78", EXESS_SUCCESS, -0.78, 4);
+
+ // Limits
+ check_read(
+ "0."
+ "00000000000000000000000000000000000000000000000000000000000000000000000000"
+ "00000000000000000000000000000000000000000000000000000000000000000000000000"
+ "00000000000000000000000000000000000000000000000000000000000000000000000000"
+ "00000000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000022250738585072014",
+ EXESS_SUCCESS,
+ DBL_MIN,
+ 326);
+
+ check_read("1797693134862315700000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ "00000000000000000000000000000000000000000000000000000.0",
+ EXESS_SUCCESS,
+ DBL_MAX,
+ 311);
+
+ // Superfluous digits
+ check_read("12345678901234567890", EXESS_SUCCESS, 12345678901234568000.0, 20);
+ check_read("1.2345678901234567890", EXESS_SUCCESS, 1.2345678901234568, 21);
+
+ // Special values
+ check_read("-0.0E0", EXESS_EXPECTED_END, -0.0, 4);
+ check_read("0.0E0", EXESS_EXPECTED_END, 0.0, 3);
+ check_read("+0.0E0", EXESS_EXPECTED_END, 0.0, 4);
+
+ // No exponent
+ check_read("1", EXESS_SUCCESS, 1.0, 1);
+ check_read("2.3", EXESS_SUCCESS, 2.3, 3);
+ check_read("-4.5", EXESS_SUCCESS, -4.5, 4);
+
+ // Garbage
+ check_read("NaN", EXESS_EXPECTED_DIGIT, (double)NAN, 0);
+ check_read("INF", EXESS_EXPECTED_DIGIT, (double)NAN, 0);
+ check_read("-INF", EXESS_EXPECTED_DIGIT, (double)NAN, 1);
+ check_read("true", EXESS_EXPECTED_DIGIT, (double)NAN, 0);
+ check_read("+true", EXESS_EXPECTED_DIGIT, (double)NAN, 1);
+ check_read("-false", EXESS_EXPECTED_DIGIT, (double)NAN, 1);
+}
+
+static void
+test_decimal_string_length(void)
+{
+ // Basic values
+ assert(exess_write_decimal(-1.0, 0, NULL).count == 4);
+ assert(exess_write_decimal(-0.0, 0, NULL).count == 4);
+ assert(exess_write_decimal(0.0, 0, NULL).count == 3);
+ assert(exess_write_decimal(1.0, 0, NULL).count == 3);
+
+ // Limits
+ assert(exess_write_decimal(DBL_MIN, 0, NULL).count == 326);
+ assert(exess_write_decimal(DBL_MAX, 0, NULL).count == 311);
+
+ // Special values
+ assert(exess_write_decimal((double)NAN, 0, NULL).count == 0);
+ assert(exess_write_decimal(-0.0, 0, NULL).count == 4);
+ assert(exess_write_decimal(0.0, 0, NULL).count == 3);
+ assert(exess_write_decimal((double)INFINITY, 0, NULL).count == 0);
+ assert(exess_write_decimal((double)-INFINITY, 0, NULL).count == 0);
+}
+
+/// Check that `str` is a canonical xsd:double string
+static void
+check_canonical(const char* const str)
+{
+ assert(strlen(str) > 2); // Shortest possible is something like 1.2
+ assert(str[0] == '-' || is_digit(str[0]));
+
+ const int first_digit = str[0] == '-' ? 1 : 0;
+
+ for (const char* s = str + first_digit; *s; ++s) {
+ assert(*s == '.' || is_digit(*s));
+ }
+}
+
+static void
+check_write(const double value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_DECIMAL_LENGTH + 1] = {42};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_decimal(value, buf_size, buf);
+ assert(r.status == expected_status);
+ if (expected_string) {
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert(r.status || exess_write_decimal(value, 0, NULL).count == r.count);
+
+ if (expected_string[0]) {
+ check_canonical(buf);
+ }
+ }
+}
+
+static void
+test_write_decimal(void)
+{
+ check_write((double)NAN, EXESS_BAD_VALUE, 4, "");
+ check_write((double)-INFINITY, EXESS_BAD_VALUE, 5, "");
+
+ check_write(
+ DBL_MIN,
+ EXESS_SUCCESS,
+ 327,
+ "0."
+ "00000000000000000000000000000000000000000000000000000000000000000000000000"
+ "00000000000000000000000000000000000000000000000000000000000000000000000000"
+ "00000000000000000000000000000000000000000000000000000000000000000000000000"
+ "00000000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000022250738585072014");
+
+ check_write(-1.2, EXESS_SUCCESS, 5, "-1.2");
+ check_write(-0.0, EXESS_SUCCESS, 5, "-0.0");
+ check_write(0.0, EXESS_SUCCESS, 4, "0.0");
+ check_write(1.2, EXESS_SUCCESS, 4, "1.2");
+
+ check_write(DBL_MAX,
+ EXESS_SUCCESS,
+ 312,
+ "1797693134862315700000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ "00000000000000000000000000000000000000000000000000000.0");
+
+ check_write((double)INFINITY, EXESS_BAD_VALUE, 4, "");
+
+ check_write(DBL_MIN, EXESS_NO_SPACE, 326, "");
+ check_write(-1.2, EXESS_NO_SPACE, 4, "");
+ check_write(-0.0, EXESS_NO_SPACE, 4, "");
+ check_write(0.0, EXESS_NO_SPACE, 3, "");
+ check_write(1.2, EXESS_NO_SPACE, 3, "");
+ check_write(DBL_MAX, EXESS_NO_SPACE, 311, "");
+
+ check_write(-1.0, EXESS_NO_SPACE, 1, "");
+ check_write(-1.0, EXESS_NO_SPACE, 0, NULL);
+}
+
+static void
+check_round_trip(const double value)
+{
+ double parsed_value = 0.0;
+ char buf[EXESS_MAX_DECIMAL_LENGTH + 1] = {42};
+
+ /* fprintf(stderr, "%f\n", value); */
+ assert(!exess_write_decimal(value, sizeof(buf), buf).status);
+ /* fprintf(stderr, "Buf: %s\n", buf); */
+ assert(!exess_read_decimal(&parsed_value, buf).status);
+ assert(double_matches(parsed_value, value));
+}
+
+static void
+test_round_trip(const ExessNumTestOptions opts)
+{
+ check_round_trip(DBL_MIN);
+ check_round_trip(-0.0);
+ check_round_trip(0.0);
+ check_round_trip(DBL_MAX);
+
+ fprintf(stderr, "Testing xsd:double randomly with seed %u\n", opts.seed);
+
+ uint32_t rep = opts.seed;
+ for (uint64_t i = 0; i < opts.n_tests; ++i) {
+ rep = lcg32(rep);
+
+ const double value = double_from_rep(rep);
+
+ check_round_trip(value);
+ print_num_test_progress(i, opts.n_tests);
+ }
+}
+
+int
+main(int argc, char** argv)
+{
+ const ExessNumTestOptions opts = parse_num_test_options(argc, argv);
+ if (opts.error) {
+ return 1;
+ }
+
+ test_read_decimal();
+ test_decimal_string_length();
+ test_write_decimal();
+ test_round_trip(opts);
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_double.c b/subprojects/exess/test/test_double.c
new file mode 100644
index 00000000..d4901c5a
--- /dev/null
+++ b/subprojects/exess/test/test_double.c
@@ -0,0 +1,246 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "float_test_data.h"
+#include "int_test_data.h"
+#include "num_test_utils.h"
+#include "string_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <float.h>
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const double expected_value,
+ const size_t expected_count)
+{
+ double value = (double)NAN;
+ const ExessResult r = exess_read_double(&value, string);
+
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(double_matches(value, expected_value));
+}
+
+static void
+test_read_double(void)
+{
+ // Limits
+ check_read("-1.7976931348623157E308", EXESS_SUCCESS, -DBL_MAX, 23);
+ check_read("-2.2250738585072014E-308", EXESS_SUCCESS, -DBL_MIN, 24);
+ check_read("2.2250738585072014E-308", EXESS_SUCCESS, DBL_MIN, 23);
+ check_read("1.7976931348623157E308", EXESS_SUCCESS, DBL_MAX, 22);
+
+ // Beyond limits
+ check_read("1e-326", EXESS_SUCCESS, 0.0, 6);
+ check_read("12345678901234567123", EXESS_SUCCESS, 12345678901234567000.0, 20);
+ check_read("1e309", EXESS_SUCCESS, (double)INFINITY, 5);
+
+ // Non-canonical form
+ check_read("+1E3", EXESS_SUCCESS, 1e3, 4);
+ check_read("1E+3", EXESS_SUCCESS, 1e3, 4);
+ check_read("+1.5E3", EXESS_SUCCESS, 1.5e3, 6);
+ check_read(".5E3", EXESS_SUCCESS, 0.5e3, 4);
+ check_read("+.5E3", EXESS_SUCCESS, 0.5e3, 5);
+ check_read("-.5E3", EXESS_SUCCESS, -0.5e3, 5);
+ check_read("1.E3", EXESS_SUCCESS, 1e3, 4);
+ check_read("+1.E3", EXESS_SUCCESS, 1e3, 5);
+ check_read("-1.E3", EXESS_SUCCESS, -1e3, 5);
+
+ // Special values
+ check_read("NaN", EXESS_SUCCESS, (double)NAN, 3);
+ check_read("-INF", EXESS_SUCCESS, (double)-INFINITY, 4);
+ check_read("-0.0E0", EXESS_SUCCESS, -0.0, 6);
+ check_read("0.0E0", EXESS_SUCCESS, 0.0, 5);
+ check_read("+0.0E0", EXESS_SUCCESS, 0.0, 6);
+ check_read("INF", EXESS_SUCCESS, (double)INFINITY, 3);
+ check_read("+INF", EXESS_SUCCESS, (double)INFINITY, 4);
+
+ // No exponent
+ check_read("1", EXESS_SUCCESS, 1.0, 1);
+ check_read("2.3", EXESS_SUCCESS, 2.3, 3);
+ check_read("-4.5", EXESS_SUCCESS, -4.5, 4);
+
+ // Leading whitespace
+ check_read(" \f\n\r\t\v1.2", EXESS_SUCCESS, 1.2, 9);
+
+ // Garbage
+ check_read("true", EXESS_EXPECTED_DIGIT, (double)NAN, 0);
+ check_read("+true", EXESS_EXPECTED_DIGIT, (double)NAN, 1);
+ check_read("-false", EXESS_EXPECTED_DIGIT, (double)NAN, 1);
+ check_read("1.0eX", EXESS_EXPECTED_DIGIT, (double)NAN, 4);
+ check_read("1.0EX", EXESS_EXPECTED_DIGIT, (double)NAN, 4);
+}
+
+/// Check that `str` is a canonical xsd:double string
+static void
+check_canonical(const char* const str)
+{
+ if (!strcmp(str, "NaN") || !strcmp(str, "-INF") || !strcmp(str, "INF")) {
+ return;
+ }
+
+ assert(strlen(str) > 4); // Shortest possible is something like 1.2E3
+ assert(str[0] == '-' || is_digit(str[0]));
+
+ const int first_digit = str[0] == '-' ? 1 : 0;
+ assert(is_digit(str[first_digit]));
+ assert(str[first_digit + 1] == '.');
+ assert(is_digit(str[first_digit + 2]));
+
+ const char* const e = strchr(str, 'E');
+ assert(e);
+ assert(*e == 'E');
+ assert(*(e + 1) == '-' || is_digit(*(e + 1)));
+}
+
+static void
+check_write(const double value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_DOUBLE_LENGTH + 1] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, //
+ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, //
+ 21, 22, 23};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_double(value, buf_size, buf);
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert((r.status && r.status != EXESS_NO_SPACE) ||
+ exess_write_double(value, 0, NULL).count == r.count);
+
+ if (!r.status) {
+ check_canonical(buf);
+ }
+}
+
+static void
+test_write_double(void)
+{
+ check_write((double)NAN, EXESS_SUCCESS, 4, "NaN");
+ check_write(DBL_MIN, EXESS_SUCCESS, 24, "2.2250738585072014E-308");
+ check_write(-0.0, EXESS_SUCCESS, 7, "-0.0E0");
+ check_write(0.0, EXESS_SUCCESS, 6, "0.0E0");
+ check_write(DBL_MAX, EXESS_SUCCESS, 23, "1.7976931348623157E308");
+
+ /* check_write((double)NAN, EXESS_NO_SPACE, 3, ""); */
+ /* check_write(DBL_MIN, EXESS_SUCCESS, 24, "2.2250738585072014E-308"); */
+ /* check_write(-0.0, EXESS_SUCCESS, 7, "-0.0E0"); */
+ /* check_write(0.0, EXESS_SUCCESS, 6, "0.0E0"); */
+ /* check_write(DBL_MAX, EXESS_SUCCESS, 23, "1.7976931348623157E308"); */
+}
+
+static void
+check_round_trip(const double value)
+{
+ double parsed_value = 0.0;
+
+ char buf[EXESS_MAX_DOUBLE_LENGTH + 1] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, //
+ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, //
+ 21, 22, 23};
+
+ assert(!exess_write_double(value, sizeof(buf), buf).status);
+ assert(!exess_read_double(&parsed_value, buf).status);
+ assert(double_matches(parsed_value, value));
+}
+
+static void
+test_round_trip(const ExessNumTestOptions opts)
+{
+ check_round_trip((double)NAN);
+ check_round_trip(-(double)INFINITY);
+ check_round_trip(DBL_MIN);
+ check_round_trip(-0.0);
+ check_round_trip(0.0);
+ check_round_trip(DBL_MAX);
+ check_round_trip((double)INFINITY);
+
+ check_round_trip(5.0);
+ check_round_trip(50.0);
+ check_round_trip(500000000000000000000.0);
+ check_round_trip(-0.5);
+ check_round_trip(0.5);
+ check_round_trip(0.05);
+ check_round_trip(0.005);
+ check_round_trip(0.00000000000000000005);
+
+ // Normal limits
+ check_round_trip(nextafter(DBL_MIN, (double)INFINITY));
+ check_round_trip(nextafter(DBL_EPSILON, (double)INFINITY));
+ check_round_trip(nextafter(DBL_MAX, -(double)INFINITY));
+
+ // Subnormals
+ check_round_trip(nextafter(0.0, 1.0));
+ check_round_trip(nextafter(nextafter(0.0, 1.0), 2.0));
+ check_round_trip(nextafter(0.0, -1.0));
+ check_round_trip(nextafter(nextafter(0.0, -1.0), -2.0));
+
+ // Various tricky cases
+ check_round_trip(1e23);
+ check_round_trip(6.02951420360127e-309);
+ check_round_trip(9.17857104364115e+288);
+ check_round_trip(2.68248422823759e+22);
+
+ // Powers of two (where the lower boundary is closer)
+ for (int i = -1023; i <= 1023; ++i) {
+ check_round_trip(pow(2, i));
+ }
+
+ fprintf(stderr, "Testing xsd:double randomly with seed %u\n", opts.seed);
+
+ uint64_t rep = opts.seed;
+ for (uint64_t i = 0; i < opts.n_tests; ++i) {
+ rep = lcg64(rep);
+
+ const double value = double_from_rep(rep);
+
+ check_round_trip(nextafter(value, -(double)INFINITY));
+ check_round_trip(value);
+ check_round_trip(nextafter(value, (double)INFINITY));
+
+ print_num_test_progress(i, opts.n_tests);
+ }
+}
+
+int
+main(int argc, char** argv)
+{
+ const ExessNumTestOptions opts = parse_num_test_options(argc, argv);
+ if (opts.error) {
+ return 1;
+ }
+
+ test_read_double();
+ test_write_double();
+ test_round_trip(opts);
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_duration.c b/subprojects/exess/test/test_duration.c
new file mode 100644
index 00000000..3d964e80
--- /dev/null
+++ b/subprojects/exess/test/test_duration.c
@@ -0,0 +1,310 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "int_test_data.h"
+#include "macros.h"
+#include "num_test_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+static const ExessDuration zero = {0, 0, 0};
+static const ExessDuration lowest = {-INT32_MAX, -INT32_MAX, -999999999};
+static const ExessDuration highest = {INT32_MAX, INT32_MAX, 999999999};
+static const ExessDuration year = {12, 0, 0};
+static const ExessDuration month = {1, 0, 0};
+static const ExessDuration day = {0, 24 * 60 * 60, 0};
+static const ExessDuration hour = {0, 60 * 60, 0};
+static const ExessDuration minute = {0, 60, 0};
+static const ExessDuration second = {0, 1, 0};
+static const ExessDuration nanosecond = {0, 0, 1};
+
+static const ExessDuration n_year = {-12, 0, 0};
+static const ExessDuration n_month = {-1, 0, 0};
+static const ExessDuration n_day = {0, -24 * 60 * 60, 0};
+static const ExessDuration n_hour = {0, -60 * 60, 0};
+static const ExessDuration n_minute = {0, -60, 0};
+static const ExessDuration n_second = {0, -1, 0};
+static const ExessDuration n_nanosecond = {0, 0, -1};
+
+static const ExessDuration garbage1 = {1, 1, -1};
+static const ExessDuration garbage2 = {1, -1, 1};
+static const ExessDuration garbage3 = {1, -1, -1};
+static const ExessDuration garbage4 = {-1, 1, 1};
+static const ExessDuration garbage5 = {-1, 1, -1};
+static const ExessDuration garbage6 = {-1, -1, 1};
+static const ExessDuration garbage7 = {INT32_MIN, 0, -999999999};
+static const ExessDuration garbage8 = {0, INT32_MIN, -999999999};
+static const ExessDuration garbage9 = {INT32_MIN, INT32_MIN, -999999999};
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const size_t expected_count,
+ const int32_t expected_years,
+ const int32_t expected_months,
+ const int32_t expected_days,
+ const int32_t expected_hours,
+ const int32_t expected_minutes,
+ const int32_t expected_seconds,
+ const int32_t expected_nanoseconds,
+ const bool expected_is_negative)
+{
+ ExessDuration value = {0, 0, 0.0f};
+ const ExessResult r = exess_read_duration(&value, string);
+
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+
+ assert(value.months == (expected_is_negative ? -1 : 1) * 12 * expected_years +
+ expected_months);
+
+ assert(value.seconds ==
+ (expected_is_negative ? -1 : 1) *
+ ((expected_seconds + (60 * expected_minutes) +
+ (60 * 60 * expected_hours) + (24 * 60 * 60 * expected_days))));
+
+ assert(value.nanoseconds == expected_nanoseconds);
+}
+
+static void
+test_read_duration(void)
+{
+ // No input
+ check_read("", EXESS_EXPECTED_DURATION, 0, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ " \f\n\r\t\v", EXESS_EXPECTED_DURATION, 6, 0, 0, 0, 0, 0, 0, 0, false);
+
+ // Good values
+
+ check_read(
+ "P2Y6M5DT12H35M30S", EXESS_SUCCESS, 17, 2, 6, 5, 12, 35, 30, 0, false);
+
+ check_read("P1DT2H", EXESS_SUCCESS, 6, 0, 0, 1, 2, 0, 0, 0, false);
+ check_read("P20M", EXESS_SUCCESS, 4, 0, 20, 0, 0, 0, 0, 0, false);
+ check_read("PT20M", EXESS_SUCCESS, 5, 0, 0, 0, 0, 20, 0, 0, false);
+ check_read("P0Y20M0D", EXESS_SUCCESS, 8, 0, 20, 0, 0, 0, 0, 0, false);
+ check_read("P0Y", EXESS_SUCCESS, 3, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("-P60D", EXESS_SUCCESS, 5, 0, 0, 60, 0, 0, 0, 0, true);
+ check_read(
+ "PT1M30.5S", EXESS_SUCCESS, 9, 0, 0, 0, 0, 1, 30, 500000000, false);
+
+ // Leading and trailing whitespace
+ check_read(" \f\n\r\t\vP1Y", EXESS_SUCCESS, 9, 1, 0, 0, 0, 0, 0, 0, false);
+ check_read("P1MT2H \f\n\r\t\v", EXESS_SUCCESS, 6, 0, 1, 0, 2, 0, 0, 0, false);
+ check_read(" \f\n\r\t\vP1Y", EXESS_SUCCESS, 9, 1, 0, 0, 0, 0, 0, 0, false);
+ check_read("P1YT2H \f\n\r\t\v", EXESS_SUCCESS, 6, 1, 0, 0, 2, 0, 0, 0, false);
+
+ // Non-canonical form
+ check_read("P06D", EXESS_SUCCESS, 4, 0, 0, 6, 0, 0, 0, 0, false);
+ check_read("PT7.0S", EXESS_SUCCESS, 6, 0, 0, 0, 0, 0, 7, 0, false);
+ check_read(
+ "P0Y0M01DT06H00M00S", EXESS_SUCCESS, 18, 0, 0, 1, 6, 0, 0, 0, false);
+
+ // Out of range fields
+ check_read(
+ "P2147483647Y", EXESS_OUT_OF_RANGE, 11, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "P2147483647M", EXESS_OUT_OF_RANGE, 11, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "P2147483647D", EXESS_OUT_OF_RANGE, 11, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "PT2147483647H", EXESS_OUT_OF_RANGE, 12, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "PT2147483647M", EXESS_OUT_OF_RANGE, 12, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "PT2147483647S", EXESS_OUT_OF_RANGE, 12, 0, 0, 0, 0, 0, 0, 0, false);
+
+ // Garbage
+ check_read("P-20M", EXESS_EXPECTED_DIGIT, 1, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("P20MT", EXESS_EXPECTED_DIGIT, 5, 0, 20, 0, 0, 0, 0, 0, false);
+ check_read("P1YM5D", EXESS_EXPECTED_DIGIT, 3, 1, 0, 0, 0, 0, 0, 0, false);
+ check_read("P15.5Y", EXESS_EXPECTED_DATE_TAG, 3, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("P1D2H", EXESS_EXPECTED_TIME_SEP, 3, 0, 0, 1, 0, 0, 0, 0, false);
+ check_read("1Y2M", EXESS_EXPECTED_DURATION, 0, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("P2M1Y", EXESS_BAD_ORDER, 4, 0, 2, 0, 0, 0, 0, 0, false);
+ check_read("P2D1Y", EXESS_EXPECTED_TIME_SEP, 3, 0, 0, 2, 0, 0, 0, 0, false);
+ check_read("P2D1M", EXESS_EXPECTED_TIME_SEP, 3, 0, 0, 2, 0, 0, 0, 0, false);
+ check_read("P", EXESS_EXPECTED_DIGIT, 1, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("PT15.5H", EXESS_EXPECTED_TIME_TAG, 6, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("PT2M1H", EXESS_BAD_ORDER, 5, 0, 0, 0, 0, 2, 0, 0, false);
+ check_read("PT2S1H", EXESS_EXPECTED_END, 4, 0, 0, 0, 0, 0, 2, 0, false);
+ check_read("PT2S1M", EXESS_EXPECTED_END, 4, 0, 0, 0, 0, 0, 2, 0, false);
+ check_read("PT15.S", EXESS_EXPECTED_DIGIT, 5, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("P1Q", EXESS_EXPECTED_DATE_TAG, 2, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("PT1Q", EXESS_EXPECTED_TIME_TAG, 3, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("P-1Y", EXESS_EXPECTED_DIGIT, 1, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("P-1M", EXESS_EXPECTED_DIGIT, 1, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("P-1D", EXESS_EXPECTED_DIGIT, 1, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("PT-1H", EXESS_EXPECTED_DIGIT, 2, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("PT-1M", EXESS_EXPECTED_DIGIT, 2, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("PT-1S", EXESS_EXPECTED_DIGIT, 2, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "P4294967296Y", EXESS_OUT_OF_RANGE, 11, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "P4294967296M", EXESS_OUT_OF_RANGE, 11, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "P4294967296D", EXESS_OUT_OF_RANGE, 11, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "PT4294967296H", EXESS_OUT_OF_RANGE, 12, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read(
+ "PT4294967296M", EXESS_OUT_OF_RANGE, 12, 0, 0, 0, 0, 0, 0, 0, false);
+ check_read("", EXESS_EXPECTED_DURATION, 0, 0, 0, 0, 0, 0, 0, 0, false);
+}
+
+static void
+check_write(const ExessDuration value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_DURATION_LENGTH + 1] = {42};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_duration(value, buf_size, buf);
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert(r.status || exess_write_duration(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_duration(void)
+{
+ check_write(zero, EXESS_SUCCESS, 4, "P0Y");
+
+ check_write(
+ lowest, EXESS_SUCCESS, 39, "-P178956970Y7M24855DT3H14M7.999999999S");
+
+ check_write(
+ highest, EXESS_SUCCESS, 38, "P178956970Y7M24855DT3H14M7.999999999S");
+
+ check_write(year, EXESS_SUCCESS, 4, "P1Y");
+ check_write(month, EXESS_SUCCESS, 4, "P1M");
+ check_write(day, EXESS_SUCCESS, 4, "P1D");
+ check_write(hour, EXESS_SUCCESS, 5, "PT1H");
+ check_write(minute, EXESS_SUCCESS, 5, "PT1M");
+ check_write(second, EXESS_SUCCESS, 5, "PT1S");
+ check_write(nanosecond, EXESS_SUCCESS, 15, "PT0.000000001S");
+
+ check_write(n_year, EXESS_SUCCESS, 5, "-P1Y");
+ check_write(n_month, EXESS_SUCCESS, 5, "-P1M");
+ check_write(n_day, EXESS_SUCCESS, 5, "-P1D");
+ check_write(n_hour, EXESS_SUCCESS, 6, "-PT1H");
+ check_write(n_minute, EXESS_SUCCESS, 6, "-PT1M");
+ check_write(n_second, EXESS_SUCCESS, 6, "-PT1S");
+ check_write(n_nanosecond, EXESS_SUCCESS, 16, "-PT0.000000001S");
+
+ check_write(garbage1, EXESS_BAD_VALUE, 41, "");
+ check_write(garbage2, EXESS_BAD_VALUE, 41, "");
+ check_write(garbage3, EXESS_BAD_VALUE, 41, "");
+ check_write(garbage4, EXESS_BAD_VALUE, 41, "");
+ check_write(garbage5, EXESS_BAD_VALUE, 41, "");
+ check_write(garbage6, EXESS_BAD_VALUE, 41, "");
+ check_write(garbage7, EXESS_OUT_OF_RANGE, 41, "");
+ check_write(garbage8, EXESS_OUT_OF_RANGE, 41, "");
+ check_write(garbage9, EXESS_OUT_OF_RANGE, 41, "");
+
+ check_write(zero, EXESS_NO_SPACE, 3, "");
+ check_write(lowest, EXESS_NO_SPACE, 24, "");
+ check_write(highest, EXESS_NO_SPACE, 4, "");
+ check_write(highest, EXESS_NO_SPACE, 10, "");
+ check_write(highest, EXESS_NO_SPACE, 13, "");
+ check_write(highest, EXESS_NO_SPACE, 16, "");
+ check_write(highest, EXESS_NO_SPACE, 20, "");
+ check_write(highest, EXESS_NO_SPACE, 23, "");
+ check_write(year, EXESS_NO_SPACE, 3, "");
+ check_write(month, EXESS_NO_SPACE, 3, "");
+ check_write(day, EXESS_NO_SPACE, 3, "");
+ check_write(hour, EXESS_NO_SPACE, 4, "");
+ check_write(minute, EXESS_NO_SPACE, 4, "");
+ check_write(second, EXESS_NO_SPACE, 4, "");
+
+ // Check that nothing is written when there isn't enough space
+ char c = 42;
+ const ExessResult r = exess_write_duration(zero, 0, &c);
+ assert(c == 42);
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 0);
+}
+
+static void
+check_round_trip(const ExessDuration value)
+{
+ ExessDuration parsed_value = {0, 0, 0};
+ char buf[EXESS_MAX_DURATION_LENGTH + 1] = {0};
+
+ assert(exess_write_duration(value, 0, NULL).count <=
+ EXESS_MAX_DURATION_LENGTH);
+
+ assert(!exess_write_duration(value, sizeof(buf), buf).status);
+ assert(!exess_read_duration(&parsed_value, buf).status);
+ assert(parsed_value.months == value.months);
+ assert(parsed_value.seconds == value.seconds);
+ assert(parsed_value.nanoseconds == value.nanoseconds);
+}
+
+static void
+test_round_trip(const ExessNumTestOptions opts)
+{
+ fprintf(stderr, "Testing xsd:duration randomly with seed %u\n", opts.seed);
+
+ const uint64_t n_tests = MAX(256, opts.n_tests / 16);
+
+ uint32_t rng = opts.seed;
+ for (size_t i = 0; i < n_tests; ++i) {
+ rng = lcg32(rng);
+
+ const int32_t months = (int32_t)rng;
+
+ rng = lcg32(rng);
+
+ const int32_t seconds = (months < 0 ? -1 : 1) * (int32_t)(rng % INT32_MAX);
+
+ rng = lcg32(rng);
+
+ const int32_t nanoseconds =
+ (months < 0 ? -1 : 1) * (int32_t)(rng % 1000000000);
+
+ const ExessDuration value = {months, seconds, nanoseconds};
+ check_round_trip(value);
+
+ print_num_test_progress(i, n_tests);
+ }
+}
+
+int
+main(int argc, char** argv)
+{
+ const ExessNumTestOptions opts = parse_num_test_options(argc, argv);
+ if (opts.error) {
+ return 1;
+ }
+
+ test_read_duration();
+ test_write_duration();
+ test_round_trip(opts);
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_float.c b/subprojects/exess/test/test_float.c
new file mode 100644
index 00000000..e8cf9141
--- /dev/null
+++ b/subprojects/exess/test/test_float.c
@@ -0,0 +1,240 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "float_test_data.h"
+#include "int_test_data.h"
+#include "num_test_utils.h"
+#include "string_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <float.h>
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const float expected_value,
+ const size_t expected_count)
+{
+ float value = NAN;
+ const ExessResult r = exess_read_float(&value, string);
+
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(float_matches(value, expected_value));
+}
+
+static void
+test_read_float(void)
+{
+ // Limits
+ check_read("-3.40282347E38", EXESS_SUCCESS, -FLT_MAX, 14);
+ check_read("-1.17549435E-38", EXESS_SUCCESS, -FLT_MIN, 15);
+ check_read("1.17549435E-38", EXESS_SUCCESS, FLT_MIN, 14);
+ check_read("3.40282347E38", EXESS_SUCCESS, FLT_MAX, 13);
+
+ // Special values
+ check_read("NaN", EXESS_SUCCESS, NAN, 3);
+ check_read("-INF", EXESS_SUCCESS, -INFINITY, 4);
+ check_read("-0.0E0", EXESS_SUCCESS, -0.0f, 6);
+ check_read("0.0E0", EXESS_SUCCESS, 0.0f, 5);
+ check_read("+0.0E0", EXESS_SUCCESS, 0.0f, 6);
+ check_read("INF", EXESS_SUCCESS, INFINITY, 3);
+ check_read("+INF", EXESS_SUCCESS, INFINITY, 4);
+
+ // Various normal cases
+ check_read("-1.0E0", EXESS_SUCCESS, -1.0f, 6);
+ check_read("1.0E0", EXESS_SUCCESS, +1.0f, 5);
+ check_read("5.0E0", EXESS_SUCCESS, 5.0f, 5);
+ check_read("5.0E1", EXESS_SUCCESS, 50.0f, 5);
+ check_read("5.0E9", EXESS_SUCCESS, 5000000000.0f, 5);
+ check_read("-5.0E-1", EXESS_SUCCESS, -0.5f, 7);
+ check_read("5.0E-1", EXESS_SUCCESS, 0.5f, 6);
+ check_read("6.25E-2", EXESS_SUCCESS, 0.0625f, 7);
+ check_read("7.8125E-3", EXESS_SUCCESS, 0.0078125f, 9);
+
+ // No exponent
+ check_read("1", EXESS_SUCCESS, 1.0f, 1);
+ check_read("2.3", EXESS_SUCCESS, 2.3f, 3);
+ check_read("-4.5", EXESS_SUCCESS, -4.5f, 4);
+
+ // Trailing garbage
+ check_read("1.2.", EXESS_SUCCESS, 1.2f, 3);
+
+ // Garbage
+ check_read("true", EXESS_EXPECTED_DIGIT, NAN, 0);
+ check_read("+true", EXESS_EXPECTED_DIGIT, NAN, 1);
+ check_read("-false", EXESS_EXPECTED_DIGIT, NAN, 1);
+ check_read("1.0eX", EXESS_EXPECTED_DIGIT, NAN, 4);
+ check_read("1.0EX", EXESS_EXPECTED_DIGIT, NAN, 4);
+}
+
+static void
+test_float_string_length(void)
+{
+ // Limits
+ assert(exess_write_float(FLT_MIN, 0, NULL).count == 14);
+ assert(exess_write_float(FLT_MAX, 0, NULL).count == 13);
+
+ // Special values
+ assert(exess_write_float((float)NAN, 0, NULL).count == 3);
+ assert(exess_write_float(-1.0f, 0, NULL).count == 6);
+ assert(exess_write_float(-0.0f, 0, NULL).count == 6);
+ assert(exess_write_float(0.0f, 0, NULL).count == 5);
+ assert(exess_write_float(1.0f, 0, NULL).count == 5);
+ assert(exess_write_float((float)INFINITY, 0, NULL).count == 3);
+ assert(exess_write_float((float)-INFINITY, 0, NULL).count == 4);
+}
+
+/// Check that `str` is a canonical xsd:float string
+static void
+check_canonical(const char* const str)
+{
+ if (!strcmp(str, "NaN") || !strcmp(str, "-INF") || !strcmp(str, "INF")) {
+ return;
+ }
+
+ assert(strlen(str) > 4); // Shortest possible is something like 1.2E3
+ assert(str[0] == '-' || is_digit(str[0]));
+
+ const int first_digit = str[0] == '-' ? 1 : 0;
+ assert(is_digit(str[first_digit]));
+ assert(str[first_digit + 1] == '.');
+ assert(is_digit(str[first_digit + 2]));
+
+ const char* const e = strchr(str, 'E');
+ assert(e);
+ assert(*e == 'E');
+ assert(*(e + 1) == '-' || is_digit(*(e + 1)));
+}
+
+static void
+check_write(const float value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_FLOAT_LENGTH + 1] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_float(value, buf_size, buf);
+ assert(r.status == expected_status);
+ if (expected_string) {
+ assert(r.count == strlen(buf));
+ assert(!expected_string || !strcmp(buf, expected_string));
+ assert(r.status || exess_write_float(value, 0, NULL).count == r.count);
+ check_canonical(buf);
+ } else {
+ assert(r.count == 0);
+ }
+}
+
+static void
+test_write_float(void)
+{
+ check_write(NAN, EXESS_SUCCESS, 4, "NaN");
+ check_write(-INFINITY, EXESS_SUCCESS, 5, "-INF");
+ check_write(FLT_MIN, EXESS_SUCCESS, 15, "1.17549435E-38");
+ check_write(-0.0f, EXESS_SUCCESS, 7, "-0.0E0");
+ check_write(0.0f, EXESS_SUCCESS, 6, "0.0E0");
+ check_write(100.25f, EXESS_SUCCESS, 9, "1.0025E2");
+ check_write(FLT_MAX, EXESS_SUCCESS, 14, "3.40282346E38");
+ check_write(INFINITY, EXESS_SUCCESS, 4, "INF");
+
+ check_write(NAN, EXESS_NO_SPACE, 3, NULL);
+ check_write(-INFINITY, EXESS_NO_SPACE, 4, NULL);
+ check_write(FLT_MIN, EXESS_NO_SPACE, 13, NULL);
+ check_write(-1.0f, EXESS_NO_SPACE, 2, NULL);
+ check_write(-0.0f, EXESS_NO_SPACE, 6, NULL);
+ check_write(0.0f, EXESS_NO_SPACE, 5, NULL);
+ check_write(100.25f, EXESS_NO_SPACE, 5, NULL);
+ check_write(100.25f, EXESS_NO_SPACE, 8, NULL);
+ check_write(FLT_MAX, EXESS_NO_SPACE, 13, NULL);
+ check_write(INFINITY, EXESS_NO_SPACE, 3, NULL);
+}
+
+static void
+check_round_trip(const float value)
+{
+ float parsed_value = 0.0f;
+ char buf[EXESS_MAX_FLOAT_LENGTH + 1] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+
+ assert(!exess_write_float(value, sizeof(buf), buf).status);
+ assert(!exess_read_float(&parsed_value, buf).status);
+ assert(float_matches(parsed_value, value));
+}
+
+static void
+test_round_trip(const ExessNumTestOptions opts)
+{
+ check_round_trip(NAN);
+ check_round_trip(FLT_MIN);
+ check_round_trip(-0.0f);
+ check_round_trip(0.0f);
+ check_round_trip(FLT_MAX);
+
+ if (opts.exhaustive) {
+ fprintf(stderr, "Testing xsd:float exhaustively\n");
+
+ for (int64_t i = 0; i <= UINT32_MAX; ++i) {
+ const float value = float_from_rep((uint32_t)i);
+
+ check_round_trip(value);
+ print_num_test_progress((uint64_t)(i - (int64_t)INT32_MIN), UINT32_MAX);
+ }
+ } else {
+ fprintf(stderr, "Testing xsd:float randomly with seed %u\n", opts.seed);
+
+ uint32_t rep = opts.seed;
+ for (uint64_t i = 0; i < opts.n_tests; ++i) {
+ rep = lcg32(rep);
+
+ const float value = float_from_rep(rep);
+
+ check_round_trip(nextafterf(value, -INFINITY));
+ check_round_trip(value);
+ check_round_trip(nextafterf(value, INFINITY));
+
+ print_num_test_progress(i, opts.n_tests);
+ }
+ }
+}
+
+int
+main(int argc, char** argv)
+{
+ const ExessNumTestOptions opts = parse_num_test_options(argc, argv);
+ if (opts.error) {
+ return 1;
+ }
+
+ test_read_float();
+ test_float_string_length();
+ test_write_float();
+ test_round_trip(opts);
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_hex.c b/subprojects/exess/test/test_hex.c
new file mode 100644
index 00000000..5ceec90b
--- /dev/null
+++ b/subprojects/exess/test/test_hex.c
@@ -0,0 +1,180 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const size_t expected_value_length,
+ const char* const expected_value,
+ const size_t expected_value_size,
+ const size_t expected_count)
+{
+ char buf[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
+ ExessBlob blob = {sizeof(buf), buf};
+
+ ExessResult r = exess_read_hex(&blob, string);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(r.status || blob.size == expected_value_size);
+ if (expected_value_length > 0) {
+ assert(!strncmp(buf, expected_value, expected_value_length));
+ assert(blob.size <= exess_hex_decoded_size(strlen(string)));
+ }
+}
+
+static void
+test_lowercase(void)
+{
+ char buf[6] = {0, 0, 0, 0, 0, 0};
+ ExessBlob blob = {sizeof(buf), buf};
+
+ ExessResult r = exess_read_hex(&blob, "6A6B6C6D6E6F");
+ assert(r.status == EXESS_SUCCESS);
+ assert(r.count == 12);
+ assert(blob.size == 6);
+ assert(!strncmp((const char*)blob.data, "jklmno", 6));
+
+ r = exess_read_hex(&blob, "6a6b6c6d6e6f");
+ assert(r.status == EXESS_SUCCESS);
+ assert(r.count == 12);
+ assert(blob.size == 6);
+ assert(!strncmp((const char*)blob.data, "jklmno", 6));
+}
+
+static void
+test_whitespace(void)
+{
+ check_read("666F6F", EXESS_SUCCESS, 3, "foo", 3, 6);
+ check_read(" 666F6F", EXESS_SUCCESS, 3, "foo", 3, 7);
+ check_read("6\f66F6F", EXESS_SUCCESS, 3, "foo", 3, 7);
+ check_read("66\n6F6F", EXESS_SUCCESS, 3, "foo", 3, 7);
+ check_read("666\rF6F", EXESS_SUCCESS, 3, "foo", 3, 7);
+ check_read("666F\t6F", EXESS_SUCCESS, 3, "foo", 3, 7);
+ check_read(" \f\n\r\t\v666F6F", EXESS_SUCCESS, 3, "foo", 3, 12);
+ check_read("666F6F \f\n\r\t\v", EXESS_SUCCESS, 3, "foo", 3, 12);
+}
+
+static void
+test_syntax_errors(void)
+{
+ check_read("G6", EXESS_EXPECTED_HEX, 0, NULL, 0, 1);
+ check_read("g6", EXESS_EXPECTED_HEX, 0, NULL, 0, 1);
+ check_read("!6", EXESS_EXPECTED_HEX, 0, NULL, 0, 1);
+ check_read("^6", EXESS_EXPECTED_HEX, 0, NULL, 0, 1);
+ check_read("6G", EXESS_EXPECTED_HEX, 0, NULL, 0, 2);
+ check_read("6g", EXESS_EXPECTED_HEX, 0, NULL, 0, 2);
+ check_read("6!", EXESS_EXPECTED_HEX, 0, NULL, 0, 2);
+ check_read("6^", EXESS_EXPECTED_HEX, 0, NULL, 0, 2);
+ check_read("6", EXESS_EXPECTED_HEX, 0, NULL, 0, 1);
+ check_read("66G6", EXESS_EXPECTED_HEX, 0, NULL, 1, 3);
+ check_read("66 G6", EXESS_EXPECTED_HEX, 0, NULL, 1, 4);
+}
+
+static void
+test_read_overflow(void)
+{
+ char buf[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
+ ExessBlob blob0 = {0, buf};
+ ExessBlob blob1 = {1, buf};
+ ExessBlob blob2 = {2, buf};
+
+ ExessResult r = exess_read_hex(&blob0, "666F6F");
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 2);
+ assert(blob0.size == 0);
+
+ r = exess_read_hex(&blob1, "666F6F");
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 4);
+ assert(blob1.size == 1);
+
+ r = exess_read_hex(&blob2, "666F6F");
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 6);
+ assert(blob2.size == 2);
+}
+
+static void
+test_write_overflow(void)
+{
+ char buf[7] = {1, 2, 3, 4, 5, 6, 7};
+
+ assert(exess_write_hex(3, "foo", 0, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_hex(3, "foo", 1, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_hex(3, "foo", 2, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_hex(3, "foo", 3, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_hex(3, "foo", 4, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_hex(3, "foo", 5, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_hex(3, "foo", 6, buf).status == EXESS_NO_SPACE);
+ assert(exess_write_hex(3, "foo", 7, buf).status == EXESS_SUCCESS);
+}
+
+static void
+test_round_trip(void)
+{
+ for (size_t size = 1; size < 256; ++size) {
+ // Allocate and generate data
+ uint8_t* const data = (uint8_t*)malloc(size);
+ for (size_t i = 0; i < size; ++i) {
+ data[i] = (uint8_t)((size + i) % 256);
+ }
+
+ // Allocate buffer for encoding with minimum required size
+ const size_t str_len = exess_write_hex(size, data, 0, NULL).count;
+ char* const str = (char*)malloc(str_len + 1);
+
+ // Encode data to string buffer
+ assert(!exess_write_hex(size, data, str_len + 1, str).status);
+ assert(strlen(str) == str_len);
+ assert(str_len % 2 == 0);
+
+ // Allocate buffer for decoded data with the same size as the input
+ uint8_t* const decoded = (uint8_t*)malloc(size);
+ ExessBlob decoded_blob = {size, decoded};
+
+ // Decode and check that data matches the original input
+ assert(!exess_read_hex(&decoded_blob, str).status);
+ assert(decoded_blob.size == size);
+ assert(!memcmp(decoded, data, size));
+
+ free(decoded);
+ free(str);
+ free(data);
+ }
+}
+
+int
+main(void)
+{
+ test_lowercase();
+ test_whitespace();
+ test_syntax_errors();
+ test_read_overflow();
+ test_write_overflow();
+ test_round_trip();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_int.c b/subprojects/exess/test/test_int.c
new file mode 100644
index 00000000..d3e43f7c
--- /dev/null
+++ b/subprojects/exess/test/test_int.c
@@ -0,0 +1,129 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "int_test_data.h"
+#include "num_test_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const int32_t expected_value,
+ const size_t expected_count)
+{
+ int32_t value = 0;
+ const ExessResult r = exess_read_int(&value, string);
+
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(value == expected_value);
+}
+
+static void
+test_read_int(void)
+{
+ // Limits
+ check_read("-2147483648", EXESS_SUCCESS, INT32_MIN, EXESS_MAX_INT_LENGTH);
+ check_read("2147483647", EXESS_SUCCESS, INT32_MAX, 10);
+
+ // Out of range
+ check_read("-2147483649", EXESS_OUT_OF_RANGE, 0, 11);
+ check_read("2147483648", EXESS_OUT_OF_RANGE, 0, 10);
+ check_read("10000000000", EXESS_OUT_OF_RANGE, 0, 11);
+
+ // Garbage
+ check_read("+", EXESS_EXPECTED_DIGIT, 0, 1);
+}
+
+static void
+check_write(const int32_t value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_INT_LENGTH + 1] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_int(value, buf_size, buf);
+ assert(!strcmp(buf, expected_string));
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(r.status || exess_write_int(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_int(void)
+{
+ check_write(INT32_MIN, EXESS_SUCCESS, 12, "-2147483648");
+ check_write(INT32_MAX, EXESS_SUCCESS, 11, "2147483647");
+}
+
+static void
+test_round_trip(const ExessNumTestOptions opts)
+{
+ int32_t parsed_value = 0;
+ char buf[EXESS_MAX_INT_LENGTH + 1] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
+
+ if (opts.exhaustive) {
+ fprintf(stderr, "Testing xsd:int exhaustively\n");
+
+ for (int64_t i = INT32_MIN; i <= INT32_MAX; ++i) {
+ assert(!exess_write_int((int32_t)i, sizeof(buf), buf).status);
+ assert(!exess_read_int(&parsed_value, buf).status);
+ assert(parsed_value == i);
+
+ print_num_test_progress((uint64_t)(i - (int64_t)INT32_MIN), UINT32_MAX);
+ }
+ } else {
+ fprintf(stderr, "Testing xsd:int randomly with seed %u\n", opts.seed);
+ uint32_t rep = opts.seed;
+ for (uint64_t i = 0; i < opts.n_tests; ++i) {
+ rep = lcg32(rep);
+
+ const int32_t value = (int32_t)rep;
+
+ assert(!exess_write_int(value, sizeof(buf), buf).status);
+ assert(!exess_read_int(&parsed_value, buf).status);
+ assert(parsed_value == value);
+
+ print_num_test_progress(i, opts.n_tests);
+ }
+ }
+}
+
+int
+main(int argc, char** argv)
+{
+ const ExessNumTestOptions opts = parse_num_test_options(argc, argv);
+ if (opts.error) {
+ return 1;
+ }
+
+ test_read_int();
+ test_write_int();
+ test_round_trip(opts);
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_int_math.c b/subprojects/exess/test/test_int_math.c
new file mode 100644
index 00000000..8ab3f63f
--- /dev/null
+++ b/subprojects/exess/test/test_int_math.c
@@ -0,0 +1,97 @@
+/*
+ Copyright 2019-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "attributes.h"
+#include "int_math.h"
+
+#include <assert.h>
+#include <stdint.h>
+
+static void
+test_clz32(void)
+{
+ for (unsigned i = 0; i < 32; ++i) {
+ assert(exess_clz32(1u << i) == 32u - i - 1u);
+ }
+}
+
+static void
+test_clz64(void)
+{
+ for (unsigned i = 0; i < 64; ++i) {
+ assert(exess_clz64(1ull << i) == 64u - i - 1u);
+ }
+}
+
+static void
+test_ilog2(void)
+{
+ for (unsigned i = 0; i < 64; ++i) {
+ assert(exess_ilog2(1ull << i) == i);
+ }
+}
+
+static void
+test_ilog10(void)
+{
+ uint64_t power = 1;
+ for (unsigned i = 0; i < 20; ++i, power *= 10) {
+ assert(exess_ilog10(power) == i);
+ }
+}
+
+static void
+test_num_digits(void)
+{
+ assert(1 == exess_num_digits(0));
+ assert(1 == exess_num_digits(1));
+ assert(1 == exess_num_digits(9));
+ assert(2 == exess_num_digits(10));
+ assert(2 == exess_num_digits(99ull));
+ assert(3 == exess_num_digits(999ull));
+ assert(4 == exess_num_digits(9999ull));
+ assert(5 == exess_num_digits(99999ull));
+ assert(6 == exess_num_digits(999999ull));
+ assert(7 == exess_num_digits(9999999ull));
+ assert(8 == exess_num_digits(99999999ull));
+ assert(9 == exess_num_digits(999999999ull));
+ assert(10 == exess_num_digits(9999999999ull));
+ assert(11 == exess_num_digits(99999999999ull));
+ assert(12 == exess_num_digits(999999999999ull));
+ assert(13 == exess_num_digits(9999999999999ull));
+ assert(14 == exess_num_digits(99999999999999ull));
+ assert(15 == exess_num_digits(999999999999999ull));
+ assert(16 == exess_num_digits(9999999999999999ull));
+ assert(17 == exess_num_digits(99999999999999999ull));
+ assert(18 == exess_num_digits(999999999999999999ull));
+ assert(19 == exess_num_digits(9999999999999999999ull));
+ assert(20 == exess_num_digits(18446744073709551615ull));
+}
+
+EXESS_I_PURE_FUNC
+int
+main(void)
+{
+ test_clz32();
+ test_clz64();
+ test_ilog2();
+ test_ilog10();
+ test_num_digits();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_long.c b/subprojects/exess/test/test_long.c
new file mode 100644
index 00000000..5631ca9f
--- /dev/null
+++ b/subprojects/exess/test/test_long.c
@@ -0,0 +1,144 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const int64_t expected_value,
+ const size_t expected_count)
+{
+ int64_t value = 0;
+
+ const ExessResult r = exess_read_long(&value, string);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(value == expected_value);
+}
+
+static void
+test_read_long(void)
+{
+ // No input
+ check_read("", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read(" \f\n\r\t\v", EXESS_EXPECTED_DIGIT, 0, 6);
+
+ // Canonical form
+ check_read("-1", EXESS_SUCCESS, -1, 2);
+ check_read("0", EXESS_SUCCESS, 0, 1);
+ check_read("1", EXESS_SUCCESS, 1, 1);
+ check_read("1234", EXESS_SUCCESS, 1234, 4);
+ check_read("-1234", EXESS_SUCCESS, -1234, 5);
+
+ // Non-canonical form
+ check_read(" \f\n\r\t\v1234 ", EXESS_SUCCESS, 1234, 10);
+ check_read(" \f\n\r\t\v-1234 ", EXESS_SUCCESS, -1234, 11);
+ check_read(" \f\n\r\t\v+1234 ", EXESS_SUCCESS, 1234, 11);
+ check_read(" \f\n\r\t\v01234 ", EXESS_SUCCESS, 1234, 11);
+ check_read(" \f\n\r\t\v-01234 ", EXESS_SUCCESS, -1234, 12);
+ check_read("-01", EXESS_SUCCESS, -1, 3);
+ check_read("-0", EXESS_SUCCESS, 0, 2);
+ check_read("-00", EXESS_SUCCESS, 0, 3);
+ check_read("00", EXESS_SUCCESS, 0, 2);
+ check_read("+0", EXESS_SUCCESS, 0, 2);
+ check_read("+00", EXESS_SUCCESS, 0, 3);
+ check_read("+1", EXESS_SUCCESS, 1, 2);
+ check_read("+01", EXESS_SUCCESS, 1, 3);
+ check_read("+1234", EXESS_SUCCESS, 1234, 5);
+ check_read("01234", EXESS_SUCCESS, 1234, 5);
+ check_read("-01234", EXESS_SUCCESS, -1234, 6);
+
+ // Limits
+ check_read(
+ "-9223372036854775808", EXESS_SUCCESS, INT64_MIN, EXESS_MAX_LONG_LENGTH);
+ check_read("9223372036854775807", EXESS_SUCCESS, INT64_MAX, 19);
+
+ // Out of range
+ check_read("-9223372036854775809", EXESS_OUT_OF_RANGE, 0, 20);
+ check_read("9223372036854775808", EXESS_OUT_OF_RANGE, 0, 19);
+ check_read("12345678901234567890", EXESS_OUT_OF_RANGE, 0, 20);
+
+ // Trailing garbage
+ check_read("1234extra", EXESS_EXPECTED_END, 1234, 4);
+
+ // Garbage
+ check_read("+", EXESS_EXPECTED_DIGIT, 0, 1);
+ check_read("-", EXESS_EXPECTED_DIGIT, 0, 1);
+ check_read("true", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("false", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("zero", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("NaN", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("INF", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("-INF", EXESS_EXPECTED_DIGIT, 0, 1);
+}
+
+static void
+check_write(const int64_t value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_LONG_LENGTH + 1] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, //
+ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, //
+ 21};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_long(value, buf_size, buf);
+ assert(!strcmp(buf, expected_string));
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(r.status || exess_write_long(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_long(void)
+{
+ check_write(-1, EXESS_SUCCESS, 3, "-1");
+ check_write(0, EXESS_SUCCESS, 2, "0");
+ check_write(1, EXESS_SUCCESS, 2, "1");
+ check_write(INT64_MIN, EXESS_SUCCESS, 21, "-9223372036854775808");
+ check_write(INT64_MAX, EXESS_SUCCESS, 20, "9223372036854775807");
+
+ check_write(INT64_MIN, EXESS_NO_SPACE, 20, "");
+ check_write(INT64_MAX, EXESS_NO_SPACE, 19, "");
+ check_write(1234, EXESS_NO_SPACE, 4, "");
+ check_write(-1234, EXESS_NO_SPACE, 5, "");
+
+ // Check that nothing is written when there isn't enough space
+ char c = 42;
+ const ExessResult r = exess_write_long(1234, 0, &c);
+ assert(c == 42);
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 0);
+}
+
+int
+main(void)
+{
+ test_read_long();
+ test_write_long();
+ return 0;
+}
diff --git a/subprojects/exess/test/test_short.c b/subprojects/exess/test/test_short.c
new file mode 100644
index 00000000..e0f3aa82
--- /dev/null
+++ b/subprojects/exess/test/test_short.c
@@ -0,0 +1,100 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const int16_t expected_value,
+ const size_t expected_count)
+{
+ int16_t value = 0;
+
+ const ExessResult r = exess_read_short(&value, string);
+ assert(value == expected_value);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+}
+
+static void
+test_read_short(void)
+{
+ // Limits
+ check_read("-32768", EXESS_SUCCESS, INT16_MIN, EXESS_MAX_SHORT_LENGTH);
+ check_read("32767", EXESS_SUCCESS, INT16_MAX, 5);
+
+ // Out of range
+ check_read("-32769", EXESS_OUT_OF_RANGE, 0, 6);
+ check_read("32768", EXESS_OUT_OF_RANGE, 0, 5);
+
+ // Garbage
+ check_read("+", EXESS_EXPECTED_DIGIT, 0, 1);
+}
+
+static void
+check_write(const int16_t value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_SHORT_LENGTH + 1] = {1, 2, 3, 4, 5, 6};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_short(value, buf_size, buf);
+ assert(!strcmp(buf, expected_string));
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(r.status || exess_write_short(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_short(void)
+{
+ check_write(INT16_MIN, EXESS_SUCCESS, 7, "-32768");
+ check_write(INT16_MAX, EXESS_SUCCESS, 6, "32767");
+}
+
+static void
+test_round_trip(void)
+{
+ int16_t value = 0;
+ char buf[EXESS_MAX_SHORT_LENGTH + 1] = {1, 2, 3, 4, 5, 6};
+
+ for (int32_t i = INT16_MIN; i <= INT16_MAX; ++i) {
+ assert(!exess_write_short((int16_t)i, sizeof(buf), buf).status);
+ assert(!exess_read_short(&value, buf).status);
+ assert(value == i);
+ }
+}
+
+int
+main(void)
+{
+ test_read_short();
+ test_write_short();
+ test_round_trip();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_strerror.c b/subprojects/exess/test/test_strerror.c
new file mode 100644
index 00000000..1e5534ad
--- /dev/null
+++ b/subprojects/exess/test/test_strerror.c
@@ -0,0 +1,35 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <string.h>
+
+int
+main(void)
+{
+ for (ExessStatus i = EXESS_SUCCESS; i <= EXESS_UNSUPPORTED;
+ i = (ExessStatus)(i + 1)) {
+ assert(strlen(exess_strerror(i)) > 0);
+ }
+
+ assert(!strcmp(exess_strerror((ExessStatus)9999), "Unknown error"));
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_time.c b/subprojects/exess/test/test_time.c
new file mode 100644
index 00000000..6cf20f6d
--- /dev/null
+++ b/subprojects/exess/test/test_time.c
@@ -0,0 +1,222 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "float_test_data.h"
+#include "int_test_data.h"
+#include "time_test_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+static const ExessTime nozone = {{EXESS_LOCAL}, 0, 0, 0, 0};
+static const ExessTime utc = {{0}, 12, 15, 1, 250000000};
+static const ExessTime zoned = {INIT_ZONE(11, 30), 23, 59, 59, 1000000};
+static const ExessTime high = {INIT_ZONE(11, 30), 24, 0, 0, 0};
+static const ExessTime garbage1 = {INIT_ZONE(11, 30), 0, 0, 0, 1000000000};
+static const ExessTime garbage2 = {INIT_ZONE(11, 30), 0, 0, 60, 0};
+static const ExessTime garbage3 = {INIT_ZONE(11, 30), 0, 60, 0, 0};
+static const ExessTime garbage4 = {INIT_ZONE(11, 30), 24, 0, 0, 1};
+static const ExessTime garbage5 = {INIT_ZONE(11, 30), 24, 0, 1, 0};
+static const ExessTime garbage6 = {INIT_ZONE(11, 30), 24, 1, 0, 0};
+static const ExessTime garbage7 = {INIT_ZONE(11, 30), 25, 0, 0, 0};
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const uint8_t expected_hour,
+ const uint8_t expected_minute,
+ const uint8_t expected_second,
+ const uint32_t expected_nanosecond,
+ const int8_t expected_tz_hour,
+ const int8_t expected_tz_minute,
+ const bool expected_tz_is_present,
+ const size_t expected_count)
+{
+ ExessTime value = {{0}, 0, 0, 0, 0};
+
+ const ExessResult r = exess_read_time(&value, string);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(value.hour == expected_hour);
+ assert(value.minute == expected_minute);
+ assert(value.second == expected_second);
+ assert(value.nanosecond == expected_nanosecond);
+ assert((!expected_tz_is_present && value.zone.quarter_hours == EXESS_LOCAL) ||
+ value.zone.quarter_hours ==
+ 4 * expected_tz_hour + expected_tz_minute / 15);
+}
+
+static void
+test_read_time(void)
+{
+ // No value
+ check_read("", EXESS_EXPECTED_DIGIT, 0, 0, 0, 0, 0, 0, false, 0);
+ check_read(" \f\n\r\t\v", EXESS_EXPECTED_DIGIT, 0, 0, 0, 0, 0, 0, false, 6);
+
+ // Good values
+ check_read("13:20:00", EXESS_SUCCESS, 13, 20, 0, 0, 0, 0, false, 8);
+ check_read(
+ "13:20:30.5555", EXESS_SUCCESS, 13, 20, 30, 555500000, 0, 0, false, 13);
+ check_read("13:20:00-05:00", EXESS_SUCCESS, 13, 20, 0, 0, -5, 0, true, 14);
+ check_read("13:20:00Z", EXESS_SUCCESS, 13, 20, 0, 0, 0, 0, true, 9);
+ check_read("00:00:00", EXESS_SUCCESS, 0, 0, 0, 0, 0, 0, false, 8);
+ check_read("24:00:00", EXESS_SUCCESS, 24, 0, 0, 0, 0, 0, false, 8);
+ check_read("21:32:52", EXESS_SUCCESS, 21, 32, 52, 0, 0, 0, false, 8);
+ check_read("21:32:52+02:00", EXESS_SUCCESS, 21, 32, 52, 0, 2, 0, true, 14);
+ check_read("19:32:52Z", EXESS_SUCCESS, 19, 32, 52, 0, 0, 0, true, 9);
+ check_read("19:32:52+00:00", EXESS_SUCCESS, 19, 32, 52, 0, 0, 0, true, 14);
+ check_read(
+ "21:32:52.12679", EXESS_SUCCESS, 21, 32, 52, 126790000, 0, 0, false, 14);
+
+ // Longest possible string
+ check_read("24:59:59.000000001-14:00",
+ EXESS_SUCCESS,
+ 24,
+ 59,
+ 59,
+ 1,
+ -14,
+ 0,
+ true,
+ EXESS_MAX_TIME_LENGTH);
+
+ // Non-canonical form
+ check_read(
+ " \f\n\r\t\v13:20:00 ", EXESS_SUCCESS, 13, 20, 0, 0, 0, 0, false, 14);
+
+ // Trailing garbage
+ check_read("13:20:00junk", EXESS_EXPECTED_SIGN, 13, 20, 0, 0, 0, 0, false, 8);
+ check_read("13:20:00Zjunk", EXESS_EXPECTED_END, 13, 20, 0, 0, 0, 0, true, 9);
+
+ // Garbage
+ check_read("13.20.00", EXESS_EXPECTED_COLON, 13, 0, 0, 0, 0, 0, false, 2);
+ check_read("13:20:", EXESS_EXPECTED_DIGIT, 13, 20, 0, 0, 0, 0, false, 6);
+ check_read("5:20:00", EXESS_EXPECTED_DIGIT, 5, 0, 0, 0, 0, 0, false, 1);
+ check_read("13:20", EXESS_EXPECTED_COLON, 13, 20, 0, 0, 0, 0, false, 5);
+ check_read("13:20.5:00", EXESS_EXPECTED_COLON, 13, 20, 0, 0, 0, 0, false, 5);
+ check_read("13:65:00", EXESS_OUT_OF_RANGE, 13, 65, 0, 0, 0, 0, false, 5);
+ check_read("21:32", EXESS_EXPECTED_COLON, 21, 32, 0, 0, 0, 0, false, 5);
+ check_read("25:25:10", EXESS_OUT_OF_RANGE, 25, 0, 0, 0, 0, 0, false, 2);
+ check_read("-10:00:00", EXESS_EXPECTED_DIGIT, 0, 0, 0, 0, 0, 0, false, 0);
+ check_read("1:20:10", EXESS_EXPECTED_DIGIT, 1, 0, 0, 0, 0, 0, false, 1);
+ check_read("13:20:00A", EXESS_EXPECTED_SIGN, 13, 20, 0, 0, 0, 0, false, 8);
+}
+
+static void
+check_write(const ExessTime value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_TIME_LENGTH + 1] = {1, 2, 3, 4, 5, 6, 7, 8, 9,
+ 10, 11, 12, 13, 14, 15, 16, 17, 18,
+ 19, 20, 21, 22, 23, 24, 25};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_time(value, buf_size, buf);
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert(r.status || exess_write_time(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_time(void)
+{
+ check_write(nozone, EXESS_SUCCESS, 9, "00:00:00");
+ check_write(utc, EXESS_SUCCESS, 13, "12:15:01.25Z");
+ check_write(zoned, EXESS_SUCCESS, 19, "23:59:59.001+11:30");
+ check_write(high, EXESS_SUCCESS, 15, "24:00:00+11:30");
+
+ check_write(garbage1, EXESS_BAD_VALUE, 19, "");
+ check_write(garbage2, EXESS_BAD_VALUE, 19, "");
+ check_write(garbage3, EXESS_BAD_VALUE, 19, "");
+ check_write(garbage4, EXESS_BAD_VALUE, 19, "");
+ check_write(garbage5, EXESS_BAD_VALUE, 19, "");
+ check_write(garbage6, EXESS_BAD_VALUE, 19, "");
+ check_write(garbage7, EXESS_BAD_VALUE, 19, "");
+
+ check_write(nozone, EXESS_NO_SPACE, 8, "");
+ check_write(utc, EXESS_NO_SPACE, 12, "");
+ check_write(zoned, EXESS_NO_SPACE, 18, "");
+ check_write(zoned, EXESS_NO_SPACE, 12, "");
+ check_write(high, EXESS_NO_SPACE, 14, "");
+
+ // Check that nothing is written when there isn't enough space
+ char c = 42;
+ const ExessResult r = exess_write_time(nozone, 0, &c);
+ assert(c == 42);
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 0);
+}
+
+static void
+check_round_trip(const ExessTime value)
+{
+ ExessTime parsed_value = {{0}, 0, 0, 0, 0};
+ char buf[EXESS_MAX_TIME_LENGTH + 1] = {0};
+
+ assert(!exess_write_time(value, sizeof(buf), buf).status);
+ assert(!exess_read_time(&parsed_value, buf).status);
+ assert(parsed_value.hour == value.hour);
+ assert(parsed_value.minute == value.minute);
+ assert(double_matches(parsed_value.second, value.second));
+ assert(parsed_value.zone.quarter_hours == value.zone.quarter_hours);
+}
+
+static void
+test_round_trip(void)
+{
+ uint32_t rng = 0;
+ for (uint8_t h = 0; h < 24; ++h) {
+ for (uint8_t m = 0; m < 60; ++m) {
+ rng = lcg32(rng);
+
+ const uint32_t ns = rng % 1000000000u;
+
+ rng = lcg32(rng);
+
+ const uint8_t s = (uint8_t)(rng % 60u);
+ const ExessTime no_zone = {{EXESS_LOCAL}, h, m, s, ns};
+ const ExessTime lowest_zone = {INIT_ZONE(-14, 0), h, m, s, ns};
+ const ExessTime highest_zone = {INIT_ZONE(14, 0), h, m, s, ns};
+
+ check_round_trip(no_zone);
+ check_round_trip(lowest_zone);
+ check_round_trip(highest_zone);
+
+ const ExessTime value = {random_timezone(&rng), h, m, s, ns};
+ check_round_trip(value);
+ }
+ }
+}
+
+int
+main(void)
+{
+ test_read_time();
+ test_write_time();
+ test_round_trip();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_timezone.c b/subprojects/exess/test/test_timezone.c
new file mode 100644
index 00000000..8ccfb12b
--- /dev/null
+++ b/subprojects/exess/test/test_timezone.c
@@ -0,0 +1,178 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "time_test_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+#include <stdio.h>
+
+static const ExessTimezone missing = {EXESS_LOCAL};
+static const ExessTimezone utc = INIT_ZONE(0, 0);
+static const ExessTimezone plus = INIT_ZONE(11, 30);
+static const ExessTimezone minus = INIT_ZONE(-11, -30);
+static const ExessTimezone slight = INIT_ZONE(0, 30);
+static const ExessTimezone lowest = INIT_ZONE(-14, 0);
+static const ExessTimezone highest = INIT_ZONE(14, 0);
+static const ExessTimezone garbage1 = INIT_ZONE(-14, -15);
+static const ExessTimezone garbage2 = INIT_ZONE(14, 15);
+static const ExessTimezone garbage3 = INIT_ZONE(-15, 0);
+static const ExessTimezone garbage4 = INIT_ZONE(15, 0);
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const int8_t expected_hour,
+ const int8_t expected_minute,
+ const bool expected_is_present,
+ const size_t expected_count)
+{
+ // The timezone interface is not public, so we test it via time
+ char time_string[] = "12:00:00XXXXXX";
+ strncpy(time_string + 8, string, sizeof(time_string) - 9);
+
+ ExessTime value = {{0}, 0, 0, 0, 0};
+ const ExessResult r = exess_read_time(&value, time_string);
+
+ assert(r.status == expected_status);
+ assert(r.count == 8 + expected_count);
+ assert((!expected_is_present && value.zone.quarter_hours == EXESS_LOCAL) ||
+ value.zone.quarter_hours == 4 * expected_hour + expected_minute / 15);
+}
+
+static void
+test_read_timezone(void)
+{
+ // Basic values
+ check_read("Z", EXESS_SUCCESS, 0, 0, true, 1);
+ check_read("-05:00", EXESS_SUCCESS, -5, 0, true, 6);
+ check_read("+02:00", EXESS_SUCCESS, 2, 0, true, 6);
+ check_read("+00:00", EXESS_SUCCESS, 0, 0, true, 6);
+ check_read("-00:00", EXESS_SUCCESS, 0, 0, true, 6);
+
+ // Limits
+ check_read("-14:00", EXESS_SUCCESS, -14, 0, true, 6);
+ check_read("+14:00", EXESS_SUCCESS, 14, 0, true, 6);
+ check_read("-13:45", EXESS_SUCCESS, -13, -45, true, 6);
+ check_read("+13:45", EXESS_SUCCESS, 13, 45, true, 6);
+
+ // Out of range
+ check_read("-14:15", EXESS_OUT_OF_RANGE, 0, 0, false, 6);
+ check_read("+14:15", EXESS_OUT_OF_RANGE, 0, 0, false, 6);
+ check_read("-15:00", EXESS_OUT_OF_RANGE, 0, 0, false, 3);
+ check_read("+15:00", EXESS_OUT_OF_RANGE, 0, 0, false, 3);
+ check_read("-13:60", EXESS_OUT_OF_RANGE, 0, 0, false, 6);
+ check_read("+13:60", EXESS_OUT_OF_RANGE, 0, 0, false, 6);
+
+ // Garbage
+ check_read("+05:01", EXESS_UNSUPPORTED, 0, 0, false, 6);
+ check_read("05:00", EXESS_EXPECTED_SIGN, 0, 0, false, 0);
+ check_read("+5:00", EXESS_EXPECTED_DIGIT, 0, 0, false, 2);
+ check_read("+5:0", EXESS_EXPECTED_DIGIT, 0, 0, false, 2);
+ check_read("+5:", EXESS_EXPECTED_DIGIT, 0, 0, false, 2);
+ check_read("+:0", EXESS_EXPECTED_DIGIT, 0, 0, false, 1);
+ check_read("+A5:00", EXESS_EXPECTED_DIGIT, 0, 0, false, 1);
+ check_read("+0A:00", EXESS_EXPECTED_DIGIT, 0, 0, false, 2);
+ check_read("+05A00", EXESS_EXPECTED_COLON, 0, 0, false, 3);
+ check_read("+05:A0", EXESS_EXPECTED_DIGIT, 0, 0, false, 4);
+ check_read("+05:0A", EXESS_EXPECTED_DIGIT, 0, 0, false, 5);
+}
+
+static void
+check_write(const ExessTimezone value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ // The timezone interface is not public, so we test it via time
+ char buf[EXESS_MAX_TIME_LENGTH + 1] = {1, 2, 3, 4, 5, 6, 7, 8, 9,
+ 10, 11, 12, 13, 14, 15, 16, 17, 18,
+ 19, 20, 21, 22, 23, 24, 25};
+
+ assert(buf_size <= sizeof(buf) - 8);
+
+ const ExessTime time = {value, 12, 0, 0, 0};
+ const ExessResult r = exess_write_time(time, 8 + buf_size, buf);
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert((!buf[0] && !expected_string[0]) || !strcmp(buf + 8, expected_string));
+ assert(r.status || exess_write_time(time, 0, NULL).count == r.count);
+}
+
+static void
+test_write_timezone(void)
+{
+ check_write(missing, EXESS_SUCCESS, 1, "");
+ check_write(utc, EXESS_SUCCESS, 2, "Z");
+ check_write(plus, EXESS_SUCCESS, 7, "+11:30");
+ check_write(minus, EXESS_SUCCESS, 7, "-11:30");
+ check_write(slight, EXESS_SUCCESS, 7, "+00:30");
+ check_write(lowest, EXESS_SUCCESS, 7, "-14:00");
+ check_write(highest, EXESS_SUCCESS, 7, "+14:00");
+
+ check_write(garbage1, EXESS_BAD_VALUE, 7, "");
+ check_write(garbage2, EXESS_BAD_VALUE, 7, "");
+ check_write(garbage3, EXESS_BAD_VALUE, 7, "");
+ check_write(garbage4, EXESS_BAD_VALUE, 7, "");
+
+ check_write(utc, EXESS_NO_SPACE, 1, "");
+ check_write(plus, EXESS_NO_SPACE, 6, "");
+}
+
+static void
+check_round_trip(const ExessTimezone value)
+{
+ ExessTime parsed_time = {{0}, 0, 0, 0, 0};
+ char buf[EXESS_MAX_TIME_LENGTH + 1] = {42};
+
+ const ExessTime time = {value, 12, 0, 0, 0};
+
+ assert(!exess_write_time(time, sizeof(buf), buf).status);
+ assert(!exess_read_time(&parsed_time, buf).status);
+ assert(!memcmp(&parsed_time.zone, &value, sizeof(ExessTimezone)));
+}
+
+static void
+test_round_trip(void)
+{
+ check_round_trip(lowest);
+ check_round_trip(highest);
+
+ for (int8_t h = -13; h < 13; ++h) {
+ for (int8_t q = 0; q < 4; ++q) {
+ const ExessTimezone value = {(int8_t)(4 * h + q)};
+
+ check_round_trip(value);
+ }
+ }
+}
+
+int
+main(void)
+{
+ test_read_timezone();
+ test_write_timezone();
+ test_round_trip();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_ubyte.c b/subprojects/exess/test/test_ubyte.c
new file mode 100644
index 00000000..7129c85d
--- /dev/null
+++ b/subprojects/exess/test/test_ubyte.c
@@ -0,0 +1,100 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const uint8_t expected_value,
+ const size_t expected_count)
+{
+ uint8_t value = 0;
+
+ const ExessResult r = exess_read_ubyte(&value, string);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(value == expected_value);
+}
+
+static void
+test_read_ubyte(void)
+{
+ // Limits
+ check_read("0", EXESS_SUCCESS, 0, 1);
+ check_read("255", EXESS_SUCCESS, UINT8_MAX, EXESS_MAX_UBYTE_LENGTH);
+
+ // Out of range
+ check_read("256", EXESS_OUT_OF_RANGE, 0, 3);
+
+ // Garbage
+ check_read("-1", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("+", EXESS_EXPECTED_DIGIT, 0, 0);
+}
+
+static void
+check_write(const uint8_t value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_UBYTE_LENGTH + 1] = {1, 2, 3, 4};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_ubyte(value, buf_size, buf);
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert(r.status || exess_write_ubyte(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_ubyte(void)
+{
+ check_write(0, EXESS_SUCCESS, 2, "0");
+ check_write(UINT8_MAX, EXESS_SUCCESS, 4, "255");
+}
+
+static void
+test_round_trip(void)
+{
+ uint8_t value = 0;
+ char buf[EXESS_MAX_UBYTE_LENGTH + 1] = {1, 2, 3, 4};
+
+ for (uint16_t i = 0; i <= UINT8_MAX; ++i) {
+ assert(!exess_write_ubyte((uint8_t)i, sizeof(buf), buf).status);
+ assert(!exess_read_ubyte(&value, buf).status);
+ assert(value == i);
+ }
+}
+
+int
+main(void)
+{
+ test_read_ubyte();
+ test_write_ubyte();
+ test_round_trip();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_uint.c b/subprojects/exess/test/test_uint.c
new file mode 100644
index 00000000..5889fc20
--- /dev/null
+++ b/subprojects/exess/test/test_uint.c
@@ -0,0 +1,128 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "int_test_data.h"
+#include "num_test_utils.h"
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const uint32_t expected_value,
+ const size_t expected_count)
+{
+ uint32_t value = 0;
+ const ExessResult r = exess_read_uint(&value, string);
+
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(value == expected_value);
+}
+
+static void
+test_read_uint(void)
+{
+ // Limits
+ check_read("0", EXESS_SUCCESS, 0, 1);
+ check_read("4294967295", EXESS_SUCCESS, UINT32_MAX, EXESS_MAX_UINT_LENGTH);
+
+ // Out of range
+ check_read("4294967296", EXESS_OUT_OF_RANGE, 0, 10);
+
+ // Garbage
+ check_read("-1", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("+", EXESS_EXPECTED_DIGIT, 0, 0);
+}
+
+static void
+check_write(const uint32_t value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_UINT_LENGTH + 1] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_uint(value, buf_size, buf);
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert(r.status || exess_write_uint(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_uint(void)
+{
+ check_write(0u, EXESS_SUCCESS, 2, "0");
+ check_write(UINT32_MAX, EXESS_SUCCESS, 11, "4294967295");
+}
+
+static void
+test_round_trip(const ExessNumTestOptions opts)
+{
+ uint32_t parsed_value = 0;
+ char buf[EXESS_MAX_UINT_LENGTH + 1] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
+
+ if (opts.exhaustive) {
+ fprintf(stderr, "Testing xsd:unsignedInt exhaustively\n");
+
+ for (uint64_t i = 0u; i <= UINT32_MAX; ++i) {
+ assert(!exess_write_uint((uint32_t)i, sizeof(buf), buf).status);
+ assert(!exess_read_uint(&parsed_value, buf).status);
+ assert(parsed_value == i);
+
+ print_num_test_progress(i, UINT32_MAX);
+ }
+ } else {
+ fprintf(
+ stderr, "Testing xsd:unsignedInt randomly with seed %u\n", opts.seed);
+
+ uint32_t value = opts.seed;
+ for (uint64_t i = 0; i < opts.n_tests; ++i) {
+ value = lcg32(value);
+
+ assert(!exess_write_uint(value, sizeof(buf), buf).status);
+ assert(!exess_read_uint(&parsed_value, buf).status);
+ assert(parsed_value == value);
+
+ print_num_test_progress(i, opts.n_tests);
+ }
+ }
+}
+
+int
+main(int argc, char** argv)
+{
+ const ExessNumTestOptions opts = parse_num_test_options(argc, argv);
+ if (opts.error) {
+ return 1;
+ }
+
+ test_read_uint();
+ test_write_uint();
+ test_round_trip(opts);
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_ulong.c b/subprojects/exess/test/test_ulong.c
new file mode 100644
index 00000000..ba8ba05f
--- /dev/null
+++ b/subprojects/exess/test/test_ulong.c
@@ -0,0 +1,126 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const uint64_t expected_value,
+ const size_t expected_count)
+{
+ uint64_t value = 0;
+ const ExessResult r = exess_read_ulong(&value, string);
+
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(value == expected_value);
+}
+
+static void
+test_read_ulong(void)
+{
+ // No input
+ check_read("", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read(" \f\n\r\t\v", EXESS_EXPECTED_DIGIT, 0, 6);
+
+ // Canonical form
+ check_read("0", EXESS_SUCCESS, 0, 1);
+ check_read("1234", EXESS_SUCCESS, 1234, 4);
+
+ // Non-canonical form
+ check_read(" \f\n\r\t\v1234 ", EXESS_SUCCESS, 1234, 10);
+ check_read(" \f\n\r\t\v01234 ", EXESS_SUCCESS, 1234, 11);
+ check_read("01234", EXESS_SUCCESS, 1234, 5);
+ check_read("00", EXESS_SUCCESS, 0, 2);
+
+ // Limits
+ check_read("0", EXESS_SUCCESS, 0, 1);
+ check_read(
+ "18446744073709551615", EXESS_SUCCESS, UINT64_MAX, EXESS_MAX_ULONG_LENGTH);
+
+ // Out of range
+ check_read("18446744073709551616", EXESS_OUT_OF_RANGE, 0, 19);
+
+ // Trailing garbage
+ check_read("1234extra", EXESS_EXPECTED_END, 1234, 4);
+
+ // Garbage
+ check_read(" \f\n\r\t\v+1234 ", EXESS_EXPECTED_DIGIT, 0, 6);
+ check_read("+1234", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("+0", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("+", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("-", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("true", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("false", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("zero", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("NaN", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("INF", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("-INF", EXESS_EXPECTED_DIGIT, 0, 0);
+}
+
+static void
+check_write(const uint64_t value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_ULONG_LENGTH + 1] = {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, //
+ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, //
+ 21};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_ulong(value, buf_size, buf);
+
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ assert(r.status || exess_write_ulong(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_ulong(void)
+{
+ check_write(0u, EXESS_SUCCESS, 2, "0");
+ check_write(1u, EXESS_SUCCESS, 2, "1");
+ check_write(UINT64_MAX, EXESS_SUCCESS, 21, "18446744073709551615");
+
+ check_write(1234u, EXESS_NO_SPACE, 4, "");
+
+ // Check that nothing is written when there isn't enough space
+ char c = 42;
+ const ExessResult r = exess_write_ulong(1234u, 0, &c);
+ assert(c == 42);
+ assert(r.status == EXESS_NO_SPACE);
+ assert(r.count == 0);
+}
+
+int
+main(void)
+{
+ test_read_ulong();
+ test_write_ulong();
+ return 0;
+}
diff --git a/subprojects/exess/test/test_ushort.c b/subprojects/exess/test/test_ushort.c
new file mode 100644
index 00000000..056e182a
--- /dev/null
+++ b/subprojects/exess/test/test_ushort.c
@@ -0,0 +1,100 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+static void
+check_read(const char* const string,
+ const ExessStatus expected_status,
+ const uint16_t expected_value,
+ const size_t expected_count)
+{
+ uint16_t value = 0;
+
+ const ExessResult r = exess_read_ushort(&value, string);
+ assert(value == expected_value);
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+}
+
+static void
+test_read_ushort(void)
+{
+ // Limits
+ check_read("0", EXESS_SUCCESS, 0, 1);
+ check_read("65535", EXESS_SUCCESS, UINT16_MAX, EXESS_MAX_USHORT_LENGTH);
+
+ // Out of range
+ check_read("65536", EXESS_OUT_OF_RANGE, 0, 5);
+
+ // Garbage
+ check_read("-1", EXESS_EXPECTED_DIGIT, 0, 0);
+ check_read("+", EXESS_EXPECTED_DIGIT, 0, 0);
+}
+
+static void
+check_write(const uint16_t value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[EXESS_MAX_USHORT_LENGTH + 1] = {1, 2, 3, 4, 5, 6};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_ushort(value, buf_size, buf);
+ assert(!strcmp(buf, expected_string));
+ assert(r.status == expected_status);
+ assert(r.count == strlen(buf));
+ assert(r.status || exess_write_ushort(value, 0, NULL).count == r.count);
+}
+
+static void
+test_write_ushort(void)
+{
+ check_write(0u, EXESS_SUCCESS, 2, "0");
+ check_write(UINT16_MAX, EXESS_SUCCESS, 6, "65535");
+}
+
+static void
+test_round_trip(void)
+{
+ uint16_t value = 0;
+ char buf[EXESS_MAX_USHORT_LENGTH + 1] = {1, 2, 3, 4, 5, 6};
+
+ for (uint32_t i = 0; i <= UINT16_MAX; ++i) {
+ assert(!exess_write_ushort((uint16_t)i, sizeof(buf), buf).status);
+ assert(!exess_read_ushort(&value, buf).status);
+ assert(value == i);
+ }
+}
+
+int
+main(void)
+{
+ test_read_ushort();
+ test_write_ushort();
+ test_round_trip();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/test_variant.c b/subprojects/exess/test/test_variant.c
new file mode 100644
index 00000000..7ef7803d
--- /dev/null
+++ b/subprojects/exess/test/test_variant.c
@@ -0,0 +1,301 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "exess/exess.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+
+#define CHECK_POINTEE_EQUALS(p, v) assert((p) && (*(p) == (v)))
+
+static const ExessDuration duration = {14,
+ 3 * 24 * 60 * 60 + 4 * 60 * 60 + 5 * 60 +
+ 6,
+ 0};
+
+static const ExessDateTime datetime = {2001, 2, 3, false, 4, 5, 6, 0};
+static const ExessTime time = {{0}, 1, 2, 3, 0};
+static const ExessDate date = {2001, 2, 3, {0}};
+
+static void
+check_read(ExessVariant* const variant,
+ const ExessDatatype datatype,
+ const char* const string,
+ const ExessStatus expected_status,
+ const size_t expected_count)
+{
+ const ExessResult r = exess_read_variant(variant, datatype, string);
+
+ assert(r.status == expected_status);
+ assert(r.count == expected_count);
+ assert(variant->datatype == datatype);
+}
+
+static void
+test_read_variant(void)
+{
+ ExessVariant variant;
+
+ check_read(&variant, EXESS_NOTHING, "42", EXESS_UNSUPPORTED, 0);
+
+ check_read(&variant, EXESS_DECIMAL, "1.2", EXESS_SUCCESS, 3);
+ CHECK_POINTEE_EQUALS(exess_get_double(&variant), 1.2);
+
+ check_read(&variant, EXESS_DOUBLE, "3.4", EXESS_SUCCESS, 3);
+ assert(variant.value.as_double == 3.4);
+
+ check_read(&variant, EXESS_FLOAT, "5.6", EXESS_SUCCESS, 3);
+ assert(variant.value.as_float == 5.6f);
+
+ check_read(&variant, EXESS_BOOLEAN, "true", EXESS_SUCCESS, 4);
+ assert(variant.value.as_bool);
+
+ check_read(&variant, EXESS_INTEGER, "7", EXESS_SUCCESS, 1);
+ assert(variant.value.as_long == 7);
+
+ check_read(
+ &variant, EXESS_NON_POSITIVE_INTEGER, "f", EXESS_EXPECTED_DIGIT, 0);
+ check_read(&variant, EXESS_NON_POSITIVE_INTEGER, "1", EXESS_OUT_OF_RANGE, 1);
+ check_read(&variant, EXESS_NON_POSITIVE_INTEGER, "-8", EXESS_SUCCESS, 2);
+ assert(variant.value.as_long == -8);
+
+ check_read(&variant, EXESS_NEGATIVE_INTEGER, "f", EXESS_EXPECTED_DIGIT, 0);
+ check_read(&variant, EXESS_NEGATIVE_INTEGER, "1", EXESS_OUT_OF_RANGE, 1);
+ check_read(&variant, EXESS_NEGATIVE_INTEGER, "-9", EXESS_SUCCESS, 2);
+ assert(variant.value.as_long == -9);
+
+ check_read(&variant, EXESS_LONG, "10", EXESS_SUCCESS, 2);
+ assert(variant.value.as_long == 10);
+
+ check_read(&variant, EXESS_INT, "11", EXESS_SUCCESS, 2);
+ assert(variant.value.as_int == 11);
+
+ check_read(&variant, EXESS_SHORT, "12", EXESS_SUCCESS, 2);
+ assert(variant.value.as_short == 12);
+
+ check_read(&variant, EXESS_BYTE, "13", EXESS_SUCCESS, 2);
+ assert(variant.value.as_byte == 13);
+
+ check_read(&variant, EXESS_ULONG, "14", EXESS_SUCCESS, 2);
+ assert(variant.value.as_long == 14);
+
+ check_read(&variant, EXESS_UINT, "15", EXESS_SUCCESS, 2);
+ assert(variant.value.as_int == 15);
+
+ check_read(&variant, EXESS_USHORT, "16", EXESS_SUCCESS, 2);
+ assert(variant.value.as_short == 16);
+
+ check_read(&variant, EXESS_UBYTE, "17", EXESS_SUCCESS, 2);
+ assert(variant.value.as_byte == 17);
+
+ check_read(&variant, EXESS_POSITIVE_INTEGER, "-1", EXESS_EXPECTED_DIGIT, 0);
+ check_read(&variant, EXESS_POSITIVE_INTEGER, "0", EXESS_OUT_OF_RANGE, 1);
+ check_read(&variant, EXESS_POSITIVE_INTEGER, "18", EXESS_SUCCESS, 2);
+ assert(variant.value.as_long == 18);
+
+ check_read(&variant, EXESS_DATE, "2001-01-02", EXESS_SUCCESS, 10);
+ assert(variant.value.as_date.year == 2001);
+ assert(variant.value.as_date.month == 1);
+ assert(variant.value.as_date.day == 2);
+
+ check_read(&variant, EXESS_TIME, "12:15:01.25", EXESS_SUCCESS, 11);
+ assert(variant.value.as_time.hour == 12);
+ assert(variant.value.as_time.minute == 15);
+ assert(variant.value.as_time.second == 1);
+ assert(variant.value.as_time.nanosecond == 250000000);
+
+ char blob_data[] = {0, 0, 0};
+
+ variant.datatype = EXESS_HEX;
+ variant.value.as_blob.size = sizeof(blob_data);
+ variant.value.as_blob.data = blob_data;
+ check_read(&variant, EXESS_HEX, "666F6F", EXESS_SUCCESS, 6);
+ assert(!strncmp(blob_data, "foo", sizeof(blob_data)));
+
+ variant.datatype = EXESS_BASE64;
+ variant.value.as_blob.size = sizeof(blob_data);
+ variant.value.as_blob.data = blob_data;
+ check_read(&variant, EXESS_BASE64, "Zm9v", EXESS_SUCCESS, 4);
+ assert(!strncmp(blob_data, "foo", sizeof(blob_data)));
+}
+
+static void
+test_variant_string_length(void)
+{
+ const ExessVariant variant = {EXESS_DECIMAL, {.as_double = 12.3456}};
+
+ assert(exess_write_variant(variant, 0, NULL).count == 7);
+}
+
+static void
+check_write(const ExessVariant value,
+ const ExessStatus expected_status,
+ const size_t buf_size,
+ const char* const expected_string)
+{
+ char buf[328] = {42};
+
+ assert(buf_size <= sizeof(buf));
+
+ const ExessResult r = exess_write_variant(value, buf_size, buf);
+ assert(r.status == expected_status);
+ if (buf_size > 0) {
+ assert(r.count == strlen(buf));
+ assert(!strcmp(buf, expected_string));
+ }
+}
+
+static void
+test_write_variant(void)
+{
+ char blob_data[] = {'f', 'o', 'o'};
+ const ExessBlob blob = {sizeof(blob_data), blob_data};
+
+ const ExessVariant a_nothing = exess_make_nothing(EXESS_SUCCESS);
+ const ExessVariant a_bool = exess_make_boolean(true);
+ const ExessVariant a_decimal = exess_make_decimal(1.2);
+ const ExessVariant a_double = exess_make_double(3.4);
+ const ExessVariant a_float = exess_make_float(5.6f);
+ const ExessVariant a_long = exess_make_long(7);
+ const ExessVariant a_int = exess_make_int(8);
+ const ExessVariant a_short = exess_make_short(9);
+ const ExessVariant a_byte = exess_make_byte(10);
+ const ExessVariant a_ulong = exess_make_ulong(11);
+ const ExessVariant a_uint = exess_make_uint(12);
+ const ExessVariant a_ushort = exess_make_ushort(13);
+ const ExessVariant a_ubyte = exess_make_ubyte(14);
+ const ExessVariant a_duration = exess_make_duration(duration);
+ const ExessVariant a_datetime = exess_make_datetime(datetime);
+ const ExessVariant a_time = exess_make_time(time);
+ const ExessVariant a_date = exess_make_date(date);
+ const ExessVariant a_hex = exess_make_hex(blob);
+ const ExessVariant a_base64 = exess_make_base64(blob);
+
+ check_write(a_nothing, EXESS_BAD_VALUE, 0, "");
+ check_write(a_nothing, EXESS_BAD_VALUE, 1, "");
+ check_write(a_decimal, EXESS_SUCCESS, 4, "1.2");
+ check_write(a_double, EXESS_SUCCESS, 6, "3.4E0");
+ check_write(a_float, EXESS_SUCCESS, 12, "5.5999999E0");
+ check_write(a_bool, EXESS_SUCCESS, 5, "true");
+ check_write(a_long, EXESS_SUCCESS, 2, "7");
+ check_write(a_int, EXESS_SUCCESS, 2, "8");
+ check_write(a_short, EXESS_SUCCESS, 2, "9");
+ check_write(a_byte, EXESS_SUCCESS, 3, "10");
+ check_write(a_ulong, EXESS_SUCCESS, 3, "11");
+ check_write(a_uint, EXESS_SUCCESS, 3, "12");
+ check_write(a_ushort, EXESS_SUCCESS, 3, "13");
+ check_write(a_ubyte, EXESS_SUCCESS, 3, "14");
+ check_write(a_duration, EXESS_SUCCESS, 15, "P1Y2M3DT4H5M6S");
+ check_write(a_datetime, EXESS_SUCCESS, 40, "2001-02-03T04:05:06");
+ check_write(a_time, EXESS_SUCCESS, 40, "01:02:03Z");
+ check_write(a_date, EXESS_SUCCESS, 40, "2001-02-03Z");
+ check_write(a_hex, EXESS_SUCCESS, 7, "666F6F");
+ check_write(a_base64, EXESS_SUCCESS, 5, "Zm9v");
+
+ const ExessBlob null_blob = {0, NULL};
+
+ const ExessVariant null_hex = exess_make_hex(null_blob);
+ check_write(null_hex, EXESS_BAD_VALUE, 99, "");
+
+ const ExessVariant null_base64 = exess_make_base64(null_blob);
+ check_write(null_base64, EXESS_BAD_VALUE, 99, "");
+}
+
+static void
+test_make_get(void)
+{
+ char blob_data[] = {'f', 'o', 'o'};
+ const ExessBlob blob = {sizeof(blob_data), blob_data};
+
+ const ExessVariant a_nothing = exess_make_nothing(EXESS_NO_SPACE);
+ const ExessVariant a_bool = exess_make_boolean(true);
+ const ExessVariant a_decimal = exess_make_decimal(1.2);
+ const ExessVariant a_double = exess_make_double(3.4);
+ const ExessVariant a_float = exess_make_float(5.6f);
+ const ExessVariant a_long = exess_make_long(7);
+ const ExessVariant a_int = exess_make_int(8);
+ const ExessVariant a_short = exess_make_short(9);
+ const ExessVariant a_byte = exess_make_byte(10);
+ const ExessVariant a_ulong = exess_make_ulong(11);
+ const ExessVariant a_uint = exess_make_uint(12);
+ const ExessVariant a_ushort = exess_make_ushort(13);
+ const ExessVariant a_ubyte = exess_make_ubyte(14);
+ const ExessVariant a_duration = exess_make_duration(duration);
+ const ExessVariant a_datetime = exess_make_datetime(datetime);
+ const ExessVariant a_time = exess_make_time(time);
+ const ExessVariant a_date = exess_make_date(date);
+ const ExessVariant a_hex = exess_make_hex(blob);
+ const ExessVariant a_base64 = exess_make_base64(blob);
+
+ // Different types as status
+ assert(exess_get_status(&a_nothing) == EXESS_NO_SPACE);
+ assert(exess_get_status(&a_bool) == EXESS_SUCCESS);
+
+ // Basic successful get
+ CHECK_POINTEE_EQUALS(exess_get_boolean(&a_bool), true);
+ CHECK_POINTEE_EQUALS(exess_get_double(&a_decimal), 1.2);
+ CHECK_POINTEE_EQUALS(exess_get_double(&a_double), 3.4);
+ CHECK_POINTEE_EQUALS(exess_get_float(&a_float), 5.6f);
+ CHECK_POINTEE_EQUALS(exess_get_long(&a_long), 7);
+ CHECK_POINTEE_EQUALS(exess_get_int(&a_int), 8);
+ CHECK_POINTEE_EQUALS(exess_get_short(&a_short), 9);
+ CHECK_POINTEE_EQUALS(exess_get_byte(&a_byte), 10);
+ CHECK_POINTEE_EQUALS(exess_get_ulong(&a_ulong), 11u);
+ CHECK_POINTEE_EQUALS(exess_get_uint(&a_uint), 12u);
+ CHECK_POINTEE_EQUALS(exess_get_ushort(&a_ushort), 13u);
+ CHECK_POINTEE_EQUALS(exess_get_ubyte(&a_ubyte), 14u);
+ assert(!memcmp(exess_get_duration(&a_duration), &duration, sizeof(duration)));
+ assert(!memcmp(exess_get_datetime(&a_datetime), &datetime, sizeof(datetime)));
+ assert(!memcmp(exess_get_time(&a_time), &time, sizeof(time)));
+ assert(!memcmp(exess_get_date(&a_date), &date, sizeof(date)));
+ assert(exess_get_blob(&a_hex)->size == sizeof(blob_data));
+ assert(exess_get_blob(&a_hex)->data == blob_data);
+ assert(exess_get_blob(&a_base64)->size == sizeof(blob_data));
+ assert(exess_get_blob(&a_base64)->data == blob_data);
+
+ // Unsuccessful get
+ assert(!exess_get_boolean(&a_int));
+ assert(!exess_get_double(&a_int));
+ assert(!exess_get_float(&a_int));
+ assert(!exess_get_long(&a_bool));
+ assert(!exess_get_int(&a_bool));
+ assert(!exess_get_short(&a_int));
+ assert(!exess_get_byte(&a_int));
+ assert(!exess_get_ulong(&a_int));
+ assert(!exess_get_uint(&a_int));
+ assert(!exess_get_ushort(&a_int));
+ assert(!exess_get_ubyte(&a_int));
+ assert(!exess_get_duration(&a_int));
+ assert(!exess_get_datetime(&a_int));
+ assert(!exess_get_time(&a_int));
+ assert(!exess_get_date(&a_int));
+ assert(!exess_get_blob(&a_int));
+}
+
+int
+main(void)
+{
+ test_read_variant();
+ test_variant_string_length();
+ test_write_variant();
+ test_make_get();
+
+ return 0;
+}
diff --git a/subprojects/exess/test/time_test_utils.h b/subprojects/exess/test/time_test_utils.h
new file mode 100644
index 00000000..00993207
--- /dev/null
+++ b/subprojects/exess/test/time_test_utils.h
@@ -0,0 +1,52 @@
+/*
+ Copyright 2011-2021 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "int_test_data.h"
+
+#include "exess/exess.h"
+
+#include <stdint.h>
+
+#define INIT_ZONE(hour, minute) \
+ { \
+ 4 * (hour) + (minute) / 15 \
+ }
+
+static inline ExessTimezone
+random_timezone(uint32_t* rng)
+{
+ *rng = lcg32(*rng);
+
+ const int8_t hour = (int8_t)((*rng % 27) - 13);
+
+ *rng = lcg32(*rng);
+
+ const int8_t minute = (int8_t)((hour < 0 ? -1 : 1) * (int32_t)(*rng % 60));
+
+ const ExessTimezone zone = {(int8_t)(4 * hour + minute / 15)};
+ return zone;
+}
+
+static inline bool
+timezone_matches(const ExessTimezone zone,
+ const int8_t expected_hour,
+ const int8_t expected_minute,
+ const bool expected_is_present)
+
+{
+ return (!expected_is_present && zone.quarter_hours == EXESS_LOCAL) ||
+ zone.quarter_hours == 4 * expected_hour + expected_minute / 15;
+}