Semver Checker — C source
Parse, compare, and validate Semantic Versioning 2.0.0 strings. Check which of two versions is greater (with full prerelease precedence), test whether a version satisfies an npm-style range (^, ~, comparators, hyphen, ||), and bump major/minor/patch/prerelease. Runs 100% client-side.
This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.
/* semver — Semantic Versioning 2.0.0: strict parse, precedence compare (incl. prerelease ordering), npm-style ranges (^ ~ comparators partials * AND ||) and bumping. Language: C (C11, stdlib only). Port of src/lib/semver.ts — same logic as this dir's python.py; hyphen ranges and build echo omitted for the 80-line budget (see python.py / go.go). */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct { long major, minor, patch; char pre[8][24]; int npre; } Semver;
typedef struct { long major, minor, patch; } Triple; /* minor/patch < 0 = absent/wildcard */
typedef struct { const char *op; Triple v; } Test; /* op: ">=" ">" "<" "<=" "=" */
static int dig(char c) { return c >= '0' && c <= '9'; }
static int digits_only(char *s) { while (*s) if (!dig(*s++)) return 0; return 1; }
static int cmp3(long a, long b) { return (a > b) - (a < b); }
static long parse_num(char *s, char **e) { if (!dig(*s) || (*s == '0' && dig(s[1]))) return -1; long n = 0; while (dig(*s)) n = n * 10 + (*s++ - '0'); *e = s; return n; }
static int ident_ok(char *s) { int d = 1; for (char *p = s; *p; p++) { if (!(dig(*p) || *p == '-' || (*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z'))) return 0; d = d && dig(*p); } return *s != 0 && !(d && s[0] == '0' && s[1] != 0); }
/* Parse strict "M.m.p[-pre][+build]"; a leading v/V is tolerated. Build metadata
is cut at '+' and dropped — it never affects precedence. Returns 0 on invalid. */
static int parse_semver(const char *input, Semver *o) {
char b[128], *s = b; strncpy(b, input, 127); b[127] = 0;
while (*s == ' ' || *s == '\t') s++;
if (*s == 'v' || *s == 'V') s++;
o->major = parse_num(s, &s); if (o->major < 0 || *s++ != '.') return 0;
o->minor = parse_num(s, &s); if (o->minor < 0 || *s++ != '.') return 0;
o->patch = parse_num(s, &s); if (o->patch < 0) return 0;
o->npre = 0;
char *plus = strchr(s, '+'); if (plus) *plus = 0;
if (*s != '-') return *s == 0;
if (s[1] == 0 || s[1] == '.' || strstr(s + 1, "..")) return 0;
for (char *t = strtok(s + 1, "."); t; t = strtok(NULL, "."))
if (!ident_ok(t) || o->npre >= 8) return 0; else strcpy(o->pre[o->npre++], t);
return 1;
}
static int ident_cmp(char *x, char *y) {
int xd = digits_only(x), yd = digits_only(y);
if (xd && yd) return cmp3(atol(x), atol(y)); /* numeric ids compare numerically */
if (xd || yd) return xd ? -1 : 1; /* numeric ids rank below alphanumeric */
return cmp3(strcmp(x, y), 0);
}
/* Compare by precedence: -1 a<b, 0 equal, 1 a>b. Build metadata is ignored. */
static int compare(Semver *a, Semver *b) {
int c = cmp3(a->major, b->major); if (!c) c = cmp3(a->minor, b->minor); if (!c) c = cmp3(a->patch, b->patch);
if (c) return c;
if (!a->npre || !b->npre) return cmp3(b->npre, a->npre); /* a release outranks any prerelease */
for (int i = 0; i < a->npre && i < b->npre; i++) if ((c = ident_cmp(a->pre[i], b->pre[i]))) return c;
return cmp3(a->npre, b->npre); /* equal shared ids: larger set wins */
}
static Test tests[8]; static int ntests;
static void add(const char *op, long M, long m, long p) { tests[ntests++] = (Test){op, (Triple){M, m, p}}; }
/* Parse a range partial: "1", "1.2", "1.2.3", "1.x", "*" — wildcards cascade down. */
static int parse_partial(char *s, Triple *t) {
while (*s == 'v' || *s == 'V') s++;
if (!*s || strchr("*xX", *s)) { *t = (Triple){-1, -1, -1}; return 1; }
long v[3] = {-1, -1, -1};
for (int i = 0; i < 3; i++) {
if (strchr("*xX", *s)) s++; /* wildcard field: trailing fields are dropped */
else { if (!dig(*s)) return 0; long n = 0; while (dig(*s)) n = n * 10 + (*s++ - '0'); v[i] = n; }
if (*s == '.') { s++; continue; }
if (*s) return 0; break;
}
*t = v[0] < 0 ? (Triple){-1, -1, -1} : v[1] < 0 ? (Triple){v[0], -1, -1} : (Triple){v[0], v[1], v[2]};
return 1;
}
/* Desugar one comparator (op + partial) into primitive >=/< tests. */
static void desugar(const char *op, Triple t) {
long M = t.major, m = t.minor < 0 ? 0 : t.minor, p = t.patch < 0 ? 0 : t.patch;
if (M < 0) return; /* wildcard matches everything */
if (*op == '^') { add(">=", M, m, p); /* never break the left-most non-zero part */
if (M > 0 || t.minor < 0) add("<", M + 1, 0, 0);
else if (t.minor > 0 || t.patch < 0) add("<", 0, t.minor + 1, 0);
else add("<", 0, 0, t.patch + 1); }
else if (*op == '~') { add(">=", M, m, p); if (t.minor < 0) add("<", M + 1, 0, 0); else add("<", M, t.minor + 1, 0); }
else if (*op == 'b' || *op == '=') { /* bare "1.2" acts as >=1.2.0 <1.3.0 */
if (t.minor < 0) { add(">=", M, 0, 0); add("<", M + 1, 0, 0); }
else if (t.patch < 0) { add(">=", M, t.minor, 0); add("<", M, t.minor + 1, 0); }
else add("=", M, t.minor, t.patch); }
else if (!strcmp(op, ">=")) add(">=", M, m, p);
else if (!strcmp(op, ">")) { if (t.minor < 0) add(">=", M + 1, 0, 0); else if (t.patch < 0) add(">=", M, t.minor + 1, 0); else add(">", M, t.minor, t.patch); }
else if (!strcmp(op, "<=")) { if (t.minor < 0) add("<", M + 1, 0, 0); else if (t.patch < 0) add("<", M, t.minor + 1, 0); else add("<=", M, t.minor, t.patch); }
else if (!strcmp(op, "<")) { if (t.minor < 0) add("<", M, 0, 0); else if (t.patch < 0) add("<", M, t.minor, 0); else add("<", M, t.minor, t.patch); }
}
/* Evaluate one AND-clause (already split from ||): every comparator must hold. */
static int clause(char *cl, Semver *v) {
ntests = 0; char *tok[16]; int n = 0, ok = 1;
for (char *t = strtok(cl, " \t"); t; t = strtok(NULL, " \t")) tok[n++] = t;
for (int i = 0; ok && i < n; i++) {
Triple pt; const char *op = "bare"; char *r = tok[i];
if (*r == '^' || *r == '~') { op = *r == '^' ? "^" : "~"; r++; }
else if (r[0] == '>' && r[1] == '=') { op = ">="; r += 2; }
else if (r[0] == '<' && r[1] == '=') { op = "<="; r += 2; }
else if (*r == '>' || *r == '<' || *r == '=') { op = *r == '>' ? ">" : *r == '<' ? "<" : "="; r++; }
ok = parse_partial(r, &pt); if (ok) desugar(op, pt);
}
for (int i = 0; ok && i < ntests; i++) { Test *t = &tests[i]; Semver w = {t->v.major, t->v.minor, t->v.patch, {{0}}, 0}; int c = compare(v, &w);
ok = !strcmp(t->op, ">") ? c > 0 : !strcmp(t->op, ">=") ? c >= 0 : !strcmp(t->op, "<") ? c < 0 : !strcmp(t->op, "<=") ? c <= 0 : c == 0; }
return ok; /* n == 0 ("*" or empty clause) -> true */
}
/* Does `version` satisfy the npm-style `range`? An invalid version yields 0. */
static int satisfies(const char *version, const char *range) {
Semver v; if (!parse_semver(version, &v)) return 0;
char buf[256]; strncpy(buf, range, 255); buf[255] = 0; char *or = buf;
while (or) { char *end = strstr(or, "||"); if (end) *end = 0; if (clause(or, &v)) return 1; or = end ? end + 2 : NULL; }
return 0;
}
/* Bump by kind; the prerelease bump increments a trailing numeric id (or
appends -0 / .1). Invalid input is returned unchanged. */
static void bump(const char *v, const char *kind, char *out) {
Semver s; if (!parse_semver(v, &s)) { strcpy(out, v); return; }
if (!strcmp(kind, "major")) sprintf(out, "%ld.0.0", s.major + 1);
else if (!strcmp(kind, "minor")) sprintf(out, "%ld.%ld.0", s.major, s.minor + 1);
else if (!strcmp(kind, "patch")) sprintf(out, "%ld.%ld.%ld", s.major, s.minor, s.patch + 1);
else if (s.npre == 0) sprintf(out, "%ld.%ld.%ld-0", s.major, s.minor, s.patch + 1);
else { char tail[32]; char *last = s.pre[s.npre - 1];
if (digits_only(last)) sprintf(tail, "%ld", atol(last) + 1); else sprintf(tail, "%s.1", last);
sprintf(out, "%ld.%ld.%ld-", s.major, s.minor, s.patch);
for (int i = 0; i < s.npre - 1; i++) { strcat(out, s.pre[i]); strcat(out, "."); }
strcat(out, tail); }
}
int main(void) {
Semver a, b; char out[64];
parse_semver("1.2.3-alpha.1+build.5", &a);
printf("parse 1.2.3-alpha.1+build.5 -> %ld.%ld.%ld-%s.%s\n", a.major, a.minor, a.patch, a.pre[0], a.pre[1]);
const char *chain[] = {"1.0.0-alpha", "1.0.0-alpha.1", "1.0.0-beta", "1.0.0-beta.11", "1.0.0-rc.1", "1.0.0"};
for (size_t i = 0; i + 1 < sizeof chain / sizeof *chain; i++) { parse_semver(chain[i], &a); parse_semver(chain[i + 1], &b); printf("compare %s %c %s\n", chain[i], compare(&a, &b) < 0 ? '<' : '!', chain[i + 1]); }
const char *vr[][2] = {{"1.2.3", "^1.2.0"}, {"1.9.0", "~1.2.0"}, {"0.2.5", "^0.2.3"}, {"0.3.0", "^0.2.3"}, {"3.1.0", ">=1.0.0 <2.0.0 || >=3.0.0"}, {"1.2.3", "*"}};
for (size_t i = 0; i < sizeof vr / sizeof *vr; i++) printf("satisfies %-7s %-26s -> %s\n", vr[i][0], vr[i][1], satisfies(vr[i][0], vr[i][1]) ? "true" : "false");
bump("1.2.3", "minor", out); printf("bump 1.2.3 minor -> %s\n", out);
bump("1.2.3-beta.2", "prerelease", out); printf("bump 1.2.3-beta.2 pre -> %s\n", out);
return 0;
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →