diff options
Diffstat (limited to 'subprojects')
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(©, &orig); + CHECK_HEXEQ("123456789ABCDEF01", ©); +} + +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; +} |