#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; }