Initial commit

This commit is contained in:
2026-09-12 10:47:02 -05:00
commit 4b3ad9b6f8
6 changed files with 1039 additions and 0 deletions
+446
View File
@@ -0,0 +1,446 @@
#include <ctype.h>
#include <errno.h>
#include <memory.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}