Skip to content

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 →