447 lines
10 KiB
C
447 lines
10 KiB
C
#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;
|
||
|
|
}
|