From 4b3ad9b6f872c9e31954f5da909c4d8aba804d94 Mon Sep 17 00:00:00 2001 From: SoraKatadzuma Date: Sat, 12 Sep 2026 10:47:02 -0500 Subject: [PATCH] Initial commit --- .editorconfig | 9 + .gitignore | 3 + chinookfile | 32 +++ include/malunal/version.h | 94 ++++++++ sources/version.c | 446 +++++++++++++++++++++++++++++++++++++ tests/version.c | 455 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 1039 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 chinookfile create mode 100644 include/malunal/version.h create mode 100644 sources/version.c create mode 100644 tests/version.c diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c6c8b36 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2744b01 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.chinook/ +.environ/ +.vscode/ diff --git a/chinookfile b/chinookfile new file mode 100644 index 0000000..92d2317 --- /dev/null +++ b/chinookfile @@ -0,0 +1,32 @@ +name: version +gpid: malunal +semv: 1.0.0 + +requires: +- remote: git@git.erasit.com:malunal/microtest + branch: v1.0.0 +- remote: git@git.erasit.com:malunal/strview + branch: v1.0.0 +- remote: git@git.erasit.com:malunal/types + branch: v1.0.0 + +targets: +- name: malunal.version + type: archive + deps: + - malunal.strview + - malunal.types + srcs: + - ./sources/version.c + +tests: +- name: malunal.version.tests + type: program + deps: + - malunal.microtest + - malunal.version + srcs: + - ./tests/version.c + +exports: +- malunal.version diff --git a/include/malunal/version.h b/include/malunal/version.h new file mode 100644 index 0000000..0df2eff --- /dev/null +++ b/include/malunal/version.h @@ -0,0 +1,94 @@ +/** + * @file version.h + * @brief Contains the structure and functions for a semantic version. + * @author John Christman (sorakatadzuma@gmail.com) + * @copyright Malunal Studios, LLC. + */ +#include "malunal/strview.h" +#include "malunal/types/error.h" + +#ifndef MALUNAL_VERSION_HEADER +#define MALUNAL_VERSION_HEADER + + +/** + * @brief Imports the @c version_t error domain for error checking. + * @details The @c version_t error domain is specific for versions to indicate + * an issue with the parsing of a semantic version string into the + * @c version_t object. + */ +extern +const error_domain_t +ERROR_DOMAIN_VERSION_T; + + +/** + * @brief Contains information for a semantic version. + * @details Allows a semantic version to be parsed from some string and its + * content stored in this structure. + */ +define_struct(version) { + malunal_size_t major; + malunal_size_t minor; + malunal_size_t patch; + strview_t preview; + strview_t build; +}; + + +/** + * @brief Defines a set of error codes for @c version_t. + * @details + */ +typedef enum { + VERSION_ERROR_NULL_INPUT, + VERSION_ERROR_INVALID_NUMBER, + VERSION_ERROR_UNEXPECTED_CHAR, + VERSION_ERROR_MISSING_PART, + VERSION_ERROR_LEADING_ZERO, + VERSION_ERROR_VALUE_OVERFLOW, +} version_error_t; + + +/** + * @brief Parses a version string into a @c version_t object. + * @param verstr The string containing the semantic version to be parsed. + * @param outver A pointer to the version to populate with the result. + * @returns An error if the semantic version string could not be parsed into the + * @c version_t object. + */ +error_t +parse_version( + strview_iptr_t verstr, + version_mptr_t outver +); + +/** + * @brief Compares one @c version_t to another. + * @param self The @c version_t to compare against. + * @param other The @c version_t to compare with. + * @retval -1 if self is less than other. + * @retval 0 if self is equal to other. + * @retval 1 if self is more than other. + */ +malunal_int32_t +version_compare( + version_iptr_t self, + version_iptr_t other +); + +/** + * @brief Converts the provided @c version_t to a string. + * @param self A pointer to the version object to convert to a string. + * @param buffer A pointer to where to write the semantic version string. + * @param length The length of the buffer which will receive the string. + * @returns The length of the version written or -1 if the buffer was too small. + */ +malunal_int32_t +version_string( + version_iptr_t self, + malunal_str_t buffer, + malunal_size_t length +); + +#endif /* MALUNAL_VERSION_HEADER */ diff --git a/sources/version.c b/sources/version.c new file mode 100644 index 0000000..d403e96 --- /dev/null +++ b/sources/version.c @@ -0,0 +1,446 @@ +#include +#include +#include +#include +#include +#include +#include "malunal/version.h" + + +static +malunal_cstr_t +describe(malunal_int32_t code) { + switch (code) { + case VERSION_ERROR_NULL_INPUT: + return "Version input or output pointer was null."; + case VERSION_ERROR_INVALID_NUMBER: + return "Expected a numeric version component but found none, or trailing " + "characters remained after it."; + case VERSION_ERROR_UNEXPECTED_CHAR: + return "An unexpected character was encountered while parsing the version."; + case VERSION_ERROR_MISSING_PART: + return "The version string was missing one of its required parts."; + case VERSION_ERROR_LEADING_ZERO: + return "A numeric version component had a disallowed leading zero."; + case VERSION_ERROR_VALUE_OVERFLOW: + return "A numeric version component was too large to represent."; + } + + return "Unknown version error"; +} + +const error_domain_t ERROR_DOMAIN_VERSION_T = { + .describe = &describe, + .name = "malunal.version.error" +}; + + +static +error_t +parse_core_number( + strview_mptr_t verstr, + malunal_size_t* outval +) { + if (!isdigit((unsigned char)*verstr->beg)) + return (error_t) { + .domain = &ERROR_DOMAIN_VERSION_T, + .code = VERSION_ERROR_INVALID_NUMBER + }; + + malunal_cstr_t start = verstr->beg; + while (isdigit((unsigned char)*verstr->beg)) + verstr->beg++; + + if (*verstr->beg != '.') + return (error_t) { + .domain = &ERROR_DOMAIN_VERSION_T, + .code = !( + strview_length(verstr) == 0 || + *verstr->beg == '-' || + *verstr->beg == '+' || + *verstr->beg == '.' + ) ? VERSION_ERROR_UNEXPECTED_CHAR + : VERSION_ERROR_MISSING_PART + }; + + if (verstr->beg - start > 1 && start[0] == '0') + return (error_t) { + .domain = &ERROR_DOMAIN_VERSION_T, + .code = VERSION_ERROR_LEADING_ZERO + }; + + strview_t walked = { start, verstr->beg }; + malunal_int64_t value = 0; + error_t result = strview_to_int64(&walked, &value); + if (result.domain == &ERROR_DOMAIN_STRVIEW_T) { + if (result.code == STRVIEW_ERROR_OUT_OF_RANGE) + return (error_t) { + .domain = &ERROR_DOMAIN_VERSION_T, + .code = VERSION_ERROR_VALUE_OVERFLOW + }; + + if (result.code == STRVIEW_ERROR_UNCONSUMED) + return (error_t) { + .domain = &ERROR_DOMAIN_VERSION_T, + .code = VERSION_ERROR_INVALID_NUMBER + }; + } + + *outval = (malunal_size_t)value; + return NO_ERROR; +} + +static +error_t +parse_patch_number( + strview_mptr_t verstr, + malunal_size_t* outval +) { + if (!isdigit((unsigned char)*verstr->beg)) + return (error_t) { + .domain = &ERROR_DOMAIN_VERSION_T, + .code = VERSION_ERROR_INVALID_NUMBER + }; + + malunal_cstr_t start = verstr->beg; + while (isdigit((unsigned char)*verstr->beg)) + verstr->beg++; + + if ( + strview_length(verstr) != 0 && + *verstr->beg != '-' && + *verstr->beg != '+' + ) return (error_t) { + .domain = &ERROR_DOMAIN_VERSION_T, + .code = VERSION_ERROR_UNEXPECTED_CHAR + }; + + if (verstr->beg - start > 1 && start[0] == '0') + return (error_t) { + .domain = &ERROR_DOMAIN_VERSION_T, + .code = VERSION_ERROR_LEADING_ZERO + }; + + strview_t walked = { start, verstr->beg }; + malunal_int64_t value = 0; + error_t result = strview_to_int64(&walked, &value); + if (result.domain == &ERROR_DOMAIN_STRVIEW_T) { + if (result.code == STRVIEW_ERROR_OUT_OF_RANGE) + return (error_t) { + .domain = &ERROR_DOMAIN_VERSION_T, + .code = VERSION_ERROR_VALUE_OVERFLOW + }; + + if (result.code == STRVIEW_ERROR_UNCONSUMED) + return (error_t) { + .domain = &ERROR_DOMAIN_VERSION_T, + .code = VERSION_ERROR_INVALID_NUMBER + }; + } + + *outval = (malunal_size_t)value; + return NO_ERROR; +} + +static +error_t +parse_preview_string( + strview_mptr_t verstr, + strview_mptr_t outstr +) { + if (*verstr->beg != '-') + return NO_ERROR; + + malunal_cstr_t start = ++verstr->beg; + if (strview_length(verstr) == 0 || *verstr->beg == '+') + return (error_t) { + .domain = &ERROR_DOMAIN_VERSION_T, + .code = VERSION_ERROR_MISSING_PART + }; + + while (strview_length(verstr) && *verstr->beg != '+') + verstr->beg++; + + *outstr = (strview_t) { + .beg = start, + .end = verstr->beg + }; + return NO_ERROR; +} + +static +error_t +parse_build_string( + strview_mptr_t verstr, + strview_mptr_t outstr +) { + if (*verstr->beg != '+') + return NO_ERROR; + + malunal_cstr_t start = ++verstr->beg; + if (strview_length(verstr) == 0) + return (error_t) { + .domain = &ERROR_DOMAIN_VERSION_T, + .code = VERSION_ERROR_MISSING_PART + }; + + while (strview_length(verstr) != 0) + verstr->beg++; + + *outstr = (strview_t) { + .beg = start, + .end = verstr->beg + }; + + return NO_ERROR; +} + +error_t +parse_version( + strview_iptr_t verstr, + version_mptr_t out +) { + if (verstr == null) + return (error_t) { + .domain = &ERROR_DOMAIN_VERSION_T, + .code = VERSION_ERROR_NULL_INPUT + }; + + if (out == null) + return (error_t) { + .domain = &ERROR_DOMAIN_VERSION_T, + .code = VERSION_ERROR_NULL_INPUT + }; + + memset(out, 0, sizeof(version_t)); + if (verstr->beg == null) + return NO_ERROR; + + error_t result; + strview_t tempstr = { + .beg = verstr->beg, + .end = verstr->end + }; + + result = parse_core_number(&tempstr, &out->major); + if (result.domain != null) + return result; + + tempstr.beg++; + result = parse_core_number(&tempstr, &out->minor); + if (result.domain != null) + return result; + + tempstr.beg++; + result = parse_patch_number(&tempstr, &out->patch); + if (result.domain != null) + return result; + + result = parse_preview_string(&tempstr, &out->preview); + if (result.domain != null) + return result; + + result = parse_build_string(&tempstr, &out->build); + if (result.domain != null) + return result; + + if (strview_length(&tempstr) != 0) + return (error_t) { + .domain = &ERROR_DOMAIN_VERSION_T, + .code = VERSION_ERROR_UNEXPECTED_CHAR + }; + + return NO_ERROR; +} + +static +malunal_bool_t +identifier_is_numeric(strview_iptr_t id) { + if (strview_length(id) == 0) + return false; + + malunal_cstr_t p; + for (p = id->beg; p < id->end; p++) + if (!isdigit(*p)) + return false; + return true; +} + +static +malunal_int32_t +compare_numeric_identifier( + strview_iptr_t a, + strview_iptr_t b +) { + malunal_size_t alen = strview_length(a); + malunal_size_t blen = strview_length(b); + malunal_cstr_t ap = a->beg; + malunal_cstr_t bp = b->beg; + + // Ignore leading zeros so two differently-padded numbers compare fairly. + while (alen > 1 && *ap == '0') { ap++; alen--; } + while (blen > 1 && *bp == '0') { bp++; blen--; } + + if (alen != blen) return alen < blen ? -1 : 1; + if (alen == 0) return 0; + + malunal_int32_t cmp = strncmp(ap, bp, alen); + return cmp < 0 ? -1 : (cmp > 0 ? 1 : 0); +} + +static +malunal_int32_t +compare_alphanumeric_identifier( + strview_iptr_t a, + strview_iptr_t b +) { + malunal_size_t alen = strview_length(a); + malunal_size_t blen = strview_length(b); + malunal_size_t minlen = alen < blen ? alen : blen; + malunal_int32_t cmp = minlen > 0 + ? strncmp(a->beg, b->beg, (size_t)minlen) + : 0; + + if (cmp != 0) + return cmp < 0 ? -1 : 1; + + return alen != blen + ? alen < blen ? -1 : 1 + : 0; +} + +static +malunal_int32_t +compare_preview_identifier( + strview_iptr_t a, + strview_iptr_t b +) { + malunal_bool_t anum = identifier_is_numeric(a); + malunal_bool_t bnum = identifier_is_numeric(b); + if (anum && bnum) + return compare_numeric_identifier(a, b); + + // Per semver, numeric identifiers always have lower precedence than + // alphanumeric ones. + return anum == bnum + ? compare_alphanumeric_identifier(a, b) + : anum ? -1 : 1; +} + +static +malunal_int32_t +compare_preview( + strview_iptr_t self, + strview_iptr_t other +) { + strview_t sremain = *self; + strview_t oremain = *other; + + for (;;) { + malunal_bool_t sdone = strview_length(&sremain) == 0; + malunal_bool_t odone = strview_length(&oremain) == 0; + if (sdone && odone) + return 0; + + // A pre-release with fewer identifiers has lower precedence, as long as + // all the preceding identifiers matched. + if (sdone) return -1; + if (odone) return 1; + + malunal_cstr_t send = sremain.beg; + while (send < sremain.end && *send != '.') + send++; + strview_t sid = { sremain.beg, send }; + + malunal_cstr_t oend = oremain.beg; + while (oend < oremain.end && *oend != '.') + oend++; + strview_t oid = { oremain.beg, oend }; + + malunal_int32_t cmp = compare_preview_identifier(&sid, &oid); + if (cmp != 0) + return cmp; + + sremain.beg = send < sremain.end ? send + 1 : send; + oremain.beg = oend < oremain.end ? oend + 1 : oend; + } +} + +malunal_int32_t +version_compare( + version_iptr_t self, + version_iptr_t other +) { + if (self == other) return 0; + if (self == null) return -1; + if (other == null) return 1; + + if (self->major != other->major) return self->major < other->major ? -1 : 1; + if (self->minor != other->minor) return self->minor < other->minor ? -1 : 1; + if (self->patch != other->patch) return self->patch < other->patch ? -1 : 1; + + malunal_bool_t self_has_preview = strview_length(&self->preview) != 0; + malunal_bool_t other_has_preview = strview_length(&other->preview) != 0; + if (self_has_preview != other_has_preview) + return self_has_preview ? -1 : 1; + + return self_has_preview + ? compare_preview(&self->preview, &other->preview) + : 0; +} + +malunal_int32_t +version_string( + version_iptr_t self, + malunal_str_t buffer, + malunal_size_t length +) { + if (self == null || buffer == null || length == 0) + return -1; + + malunal_size_t written = snprintf( + buffer, + length, + "%llu.%llu.%llu", + self->major, + self->minor, + self->patch + ); + + if (written < 0 || written >= length) + return -1; + + malunal_size_t preview_len = + strview_length(&self->preview); + if (preview_len > 0) { + malunal_int32_t n = snprintf( + buffer + written, + length - written, + "-%.*s", + preview_len, + self->preview.beg + ); + + if (n < 0 || (written + n) >= length) + return -1; + written += n; + } + + malunal_size_t build_len = + strview_length(&self->build); + if (build_len > 0) { + malunal_int32_t n = snprintf( + buffer + written, + length - written, + "+%.*s", + build_len, + self->build.beg + ); + + if (n < 0 || (written + n) >= length) + return -1; + written += n; + } + + return written; +} diff --git a/tests/version.c b/tests/version.c new file mode 100644 index 0000000..9d0d0ca --- /dev/null +++ b/tests/version.c @@ -0,0 +1,455 @@ +#include "malunal/microtest.h" +#include "malunal/version.h" + +/* ------------------------------------------------------------------------- + * parse_version(): successful parses + * ---------------------------------------------------------------------- */ + +MICROTEST(parse_version, basic_triple_succeeds) { + strview_t view = strview_from_cstr("1.2.3"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_EQ(ver.major, (malunal_size_t)1); + MICROTEST_EXPECT_EQ(ver.minor, (malunal_size_t)2); + MICROTEST_EXPECT_EQ(ver.patch, (malunal_size_t)3); + MICROTEST_EXPECT_EQ(strview_length(&ver.preview), (malunal_size_t)0); + MICROTEST_EXPECT_EQ(strview_length(&ver.build), (malunal_size_t)0); +} + +MICROTEST(parse_version, all_zero_succeeds) { + strview_t view = strview_from_cstr("0.0.0"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_EQ(ver.major, (malunal_size_t)0); + MICROTEST_EXPECT_EQ(ver.minor, (malunal_size_t)0); + MICROTEST_EXPECT_EQ(ver.patch, (malunal_size_t)0); +} + +MICROTEST(parse_version, multi_digit_components_succeed) { + strview_t view = strview_from_cstr("123.456.789"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_EQ(ver.major, (malunal_size_t)123); + MICROTEST_EXPECT_EQ(ver.minor, (malunal_size_t)456); + MICROTEST_EXPECT_EQ(ver.patch, (malunal_size_t)789); +} + +MICROTEST(parse_version, preview_only_succeeds) { + strview_t view = strview_from_cstr("1.2.3-alpha"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_EQ(strview_length(&ver.preview), (malunal_size_t)5); + MICROTEST_EXPECT_TRUE(memcmp(ver.preview.beg, "alpha", 5) == 0); + MICROTEST_EXPECT_EQ(strview_length(&ver.build), (malunal_size_t)0); +} + +MICROTEST(parse_version, dotted_preview_succeeds) { + strview_t view = strview_from_cstr("1.2.3-alpha.1"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_EQ(strview_length(&ver.preview), (malunal_size_t)7); + MICROTEST_EXPECT_TRUE(memcmp(ver.preview.beg, "alpha.1", 7) == 0); +} + +MICROTEST(parse_version, build_only_succeeds) { + strview_t view = strview_from_cstr("1.2.3+build.5"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_EQ(strview_length(&ver.preview), (malunal_size_t)0); + MICROTEST_EXPECT_EQ(strview_length(&ver.build), (malunal_size_t)7); + MICROTEST_EXPECT_TRUE(memcmp(ver.build.beg, "build.5", 7) == 0); +} + +MICROTEST(parse_version, preview_and_build_succeed) { + strview_t view = strview_from_cstr("1.2.3-alpha.1+build.5"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_EQ(ver.major, (malunal_size_t)1); + MICROTEST_EXPECT_EQ(ver.minor, (malunal_size_t)2); + MICROTEST_EXPECT_EQ(ver.patch, (malunal_size_t)3); + MICROTEST_EXPECT_TRUE(memcmp(ver.preview.beg, "alpha.1", 7) == 0); + MICROTEST_EXPECT_TRUE(memcmp(ver.build.beg, "build.5", 7) == 0); +} + +MICROTEST(parse_version, single_digit_zero_component_is_not_leading_zero) { + /* A lone "0" is a valid component; the leading-zero rule only forbids + * *multiple* digits starting with zero (e.g. "01"). */ + strview_t view = strview_from_cstr("0.2.3"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_EQ(ver.major, (malunal_size_t)0); +} + +/* ------------------------------------------------------------------------- + * parse_version(): error cases + * ---------------------------------------------------------------------- */ + +MICROTEST(parse_version, null_verstr_reports_null_input) { + version_t ver; + error_t err = parse_version(null, &ver); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_NULL_INPUT); +} + +MICROTEST(parse_version, null_out_reports_null_input) { + strview_t view = strview_from_cstr("1.2.3"); + error_t err = parse_version(&view, null); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_NULL_INPUT); +} + +MICROTEST(parse_version, null_backing_data_succeeds_as_zero_version) { + strview_t view = { null, null }; + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NULL(err.domain); + MICROTEST_EXPECT_EQ(ver.major, (malunal_size_t)0); + MICROTEST_EXPECT_EQ(ver.minor, (malunal_size_t)0); + MICROTEST_EXPECT_EQ(ver.patch, (malunal_size_t)0); +} + +MICROTEST(parse_version, leading_zero_in_major_fails) { + strview_t view = strview_from_cstr("01.2.3"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_LEADING_ZERO); +} + +MICROTEST(parse_version, leading_zero_in_minor_fails) { + strview_t view = strview_from_cstr("1.02.3"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_LEADING_ZERO); +} + +MICROTEST(parse_version, leading_zero_in_patch_fails) { + strview_t view = strview_from_cstr("1.2.03"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_LEADING_ZERO); +} + +MICROTEST(parse_version, missing_minor_and_patch_fails) { + strview_t view = strview_from_cstr("1.2"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_MISSING_PART); +} + +MICROTEST(parse_version, missing_patch_digits_fails) { + strview_t view = strview_from_cstr("1.2."); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_INVALID_NUMBER); +} + +MICROTEST(parse_version, fourth_numeric_component_fails) { + strview_t view = strview_from_cstr("1.2.3.4"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_UNEXPECTED_CHAR); +} + +MICROTEST(parse_version, empty_preview_fails) { + strview_t view = strview_from_cstr("1.2.3-"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_MISSING_PART); +} + +MICROTEST(parse_version, empty_build_fails) { + strview_t view = strview_from_cstr("1.2.3+"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_MISSING_PART); +} + +MICROTEST(parse_version, negative_major_fails) { + strview_t view = strview_from_cstr("-1.2.3"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_INVALID_NUMBER); +} + +MICROTEST(parse_version, non_numeric_components_fail) { + strview_t view = strview_from_cstr("a.b.c"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_INVALID_NUMBER); +} + +MICROTEST(parse_version, overflowing_major_fails) { + strview_t view = strview_from_cstr("99999999999999999999.2.3"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_VALUE_OVERFLOW); +} + +MICROTEST(parse_version, overflowing_patch_fails) { + strview_t view = strview_from_cstr("1.2.99999999999999999999"); + version_t ver; + error_t err = parse_version(&view, &ver); + + MICROTEST_EXPECT_NOT_NULL(err.domain); + MICROTEST_EXPECT_EQ(err.code, (malunal_int32_t)VERSION_ERROR_VALUE_OVERFLOW); +} + +/* ------------------------------------------------------------------------- + * version_compare(): core number precedence + * ---------------------------------------------------------------------- */ + +static +version_t +must_parse(malunal_cstr_t s) { + strview_t view = strview_from_cstr(s); + version_t ver; + parse_version(&view, &ver); + return ver; +} + +MICROTEST(version_compare, identical_versions_are_equal) { + version_t a = must_parse("1.2.3"); + version_t b = must_parse("1.2.3"); + + MICROTEST_EXPECT_EQ(version_compare(&a, &b), 0); +} + +MICROTEST(version_compare, is_reflexive) { + version_t a = must_parse("1.2.3"); + + MICROTEST_EXPECT_EQ(version_compare(&a, &a), 0); +} + +MICROTEST(version_compare, greater_major_wins) { + version_t a = must_parse("2.0.0"); + version_t b = must_parse("1.9.9"); + + MICROTEST_EXPECT_GT(version_compare(&a, &b), 0); + MICROTEST_EXPECT_LT(version_compare(&b, &a), 0); +} + +MICROTEST(version_compare, greater_minor_wins_when_major_equal) { + version_t a = must_parse("1.3.0"); + version_t b = must_parse("1.2.9"); + + MICROTEST_EXPECT_GT(version_compare(&a, &b), 0); +} + +MICROTEST(version_compare, greater_patch_wins_when_major_minor_equal) { + version_t a = must_parse("1.2.4"); + version_t b = must_parse("1.2.3"); + + MICROTEST_EXPECT_GT(version_compare(&a, &b), 0); +} + +MICROTEST(version_compare, numeric_comparison_not_lexical) { + /* 1.2.10 > 1.2.9 numerically, even though "10" < "9" lexically. */ + version_t a = must_parse("1.2.10"); + version_t b = must_parse("1.2.9"); + + MICROTEST_EXPECT_GT(version_compare(&a, &b), 0); +} + +/* ------------------------------------------------------------------------- + * version_compare(): pre-release precedence + * ---------------------------------------------------------------------- */ + +MICROTEST(version_compare, preview_has_lower_precedence_than_release) { + version_t preview = must_parse("1.0.0-alpha"); + version_t release = must_parse("1.0.0"); + + MICROTEST_EXPECT_LT(version_compare(&preview, &release), 0); + MICROTEST_EXPECT_GT(version_compare(&release, &preview), 0); +} + +MICROTEST(version_compare, more_preview_identifiers_has_higher_precedence) { + version_t shorter = must_parse("1.0.0-alpha"); + version_t longer = must_parse("1.0.0-alpha.1"); + + MICROTEST_EXPECT_LT(version_compare(&shorter, &longer), 0); +} + +MICROTEST(version_compare, numeric_identifier_has_lower_precedence_than_alphanumeric) { + version_t numeric = must_parse("1.0.0-alpha.1"); + version_t alpha = must_parse("1.0.0-alpha.beta"); + + MICROTEST_EXPECT_LT(version_compare(&numeric, &alpha), 0); +} + +MICROTEST(version_compare, numeric_preview_identifiers_compare_numerically) { + /* "10" > "2" numerically, even though "10" < "2" lexically. */ + version_t ten = must_parse("1.0.0-rc.10"); + version_t two = must_parse("1.0.0-rc.2"); + + MICROTEST_EXPECT_GT(version_compare(&ten, &two), 0); +} + +MICROTEST(version_compare, alphanumeric_preview_identifiers_compare_lexically) { + version_t a = must_parse("1.0.0-alpha"); + version_t b = must_parse("1.0.0-beta"); + + MICROTEST_EXPECT_LT(version_compare(&a, &b), 0); +} + +MICROTEST(version_compare, identical_previews_are_equal) { + version_t a = must_parse("1.0.0-alpha.1"); + version_t b = must_parse("1.0.0-alpha.1"); + + MICROTEST_EXPECT_EQ(version_compare(&a, &b), 0); +} + +/* ------------------------------------------------------------------------- + * version_compare(): build metadata is ignored + * ---------------------------------------------------------------------- */ + +MICROTEST(version_compare, differing_build_metadata_is_ignored) { + version_t a = must_parse("1.0.0+build.1"); + version_t b = must_parse("1.0.0+build.2"); + + MICROTEST_EXPECT_EQ(version_compare(&a, &b), 0); +} + +MICROTEST(version_compare, build_metadata_ignored_with_preview_present) { + version_t a = must_parse("1.0.0-alpha+build.1"); + version_t b = must_parse("1.0.0-alpha+build.2"); + + MICROTEST_EXPECT_EQ(version_compare(&a, &b), 0); +} + +/* ------------------------------------------------------------------------- + * version_string() + * ---------------------------------------------------------------------- */ + +MICROTEST(version_string, basic_triple_round_trips) { + version_t ver = must_parse("1.2.3"); + + malunal_char_t buffer[32]; + malunal_int32_t n = version_string(&ver, buffer, sizeof(buffer)); + + MICROTEST_EXPECT_GT(n, 0); + MICROTEST_EXPECT_TRUE(strcmp(buffer, "1.2.3") == 0); +} + +MICROTEST(version_string, includes_preview) { + version_t ver = must_parse("1.2.3-alpha.1"); + + malunal_char_t buffer[32]; + malunal_int32_t n = version_string(&ver, buffer, sizeof(buffer)); + + MICROTEST_EXPECT_GT(n, 0); + MICROTEST_EXPECT_TRUE(strcmp(buffer, "1.2.3-alpha.1") == 0); +} + +MICROTEST(version_string, includes_build) { + version_t ver = must_parse("1.2.3+build.5"); + + malunal_char_t buffer[32]; + malunal_int32_t n = version_string(&ver, buffer, sizeof(buffer)); + + MICROTEST_EXPECT_GT(n, 0); + MICROTEST_EXPECT_TRUE(strcmp(buffer, "1.2.3+build.5") == 0); +} + +MICROTEST(version_string, includes_preview_and_build) { + version_t ver = must_parse("1.2.3-alpha.1+build.5"); + + malunal_char_t buffer[32]; + malunal_int32_t n = version_string(&ver, buffer, sizeof(buffer)); + + MICROTEST_EXPECT_GT(n, 0); + MICROTEST_EXPECT_TRUE(strcmp(buffer, "1.2.3-alpha.1+build.5") == 0); +} + +MICROTEST(version_string, too_small_buffer_fails) { + version_t ver = must_parse("1.2.3-alpha.1+build.5"); + + malunal_char_t buffer[5]; + malunal_int32_t n = version_string(&ver, buffer, sizeof(buffer)); + + MICROTEST_EXPECT_EQ(n, -1); +} + +MICROTEST(version_string, exact_size_buffer_succeeds) { + /* "1.2.3" is 5 characters plus a null terminator. */ + version_t ver = must_parse("1.2.3"); + + malunal_char_t buffer[6]; + malunal_int32_t n = version_string(&ver, buffer, sizeof(buffer)); + + MICROTEST_EXPECT_EQ(n, 5); + MICROTEST_EXPECT_TRUE(strcmp(buffer, "1.2.3") == 0); +} + +MICROTEST(version_string, one_byte_too_small_fails) { + version_t ver = must_parse("1.2.3"); + + malunal_char_t buffer[5]; + malunal_int32_t n = version_string(&ver, buffer, sizeof(buffer)); + + MICROTEST_EXPECT_EQ(n, -1); +} + +MICROTEST(version_string, null_self_fails) { + malunal_char_t buffer[32]; + malunal_int32_t n = version_string(null, buffer, sizeof(buffer)); + + MICROTEST_EXPECT_EQ(n, -1); +} + +MICROTEST(version_string, null_buffer_fails) { + version_t ver = must_parse("1.2.3"); + malunal_int32_t n = version_string(&ver, null, 32); + + MICROTEST_EXPECT_EQ(n, -1); +} + +MICROTEST(version_string, zero_length_fails) { + version_t ver = must_parse("1.2.3"); + malunal_char_t buffer[32]; + malunal_int32_t n = version_string(&ver, buffer, 0); + + MICROTEST_EXPECT_EQ(n, -1); +} + +MICROTEST_MAIN()