Skip to content

Credit Card Validator — C source

Validate credit card numbers with the Luhn checksum and detect the issuing brand (Visa, Mastercard, Amex, Discover, Diners, JCB, UnionPay, Maestro). 100% client-side - nothing is transmitted.

This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.

/*
 * credit-card-validator — Luhn checksum + brand detection for card numbers.
 *
 * Language: C (C11, standard library only)
 * Source:   CosmoDev polyglot showcase port of the Credit Card Validator tool,
 *           ported from src/lib/creditCard.ts (the canonical TypeScript lib) and
 *           held in lock-step with cli/credit-card-validator/credit-card-validator.go.
 * License:  display source — part of CosmoDev's polyglot tool pages.
 *
 * Pure credit-card validation with no I/O beyond the demo main: sanitize to
 * digits, run the Luhn checksum, detect the issuing brand by prefix + length,
 * and group the number for display. Never crashes on malformed input; mirrors
 * the TypeScript contract.
 *
 * C11 has no stdlib regex (POSIX <regex.h> is the usual ecosystem equivalent),
 * so — mirroring rust.rs — the brand prefixes are matched by hand on the byte
 * string instead of regexes. All sanitized digits are ASCII, so byte indexing
 * is correct. Heap strings returned by validate_card() are freed with
 * card_info_free().
 */

#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/*
 * Full validation result. `brand` is NULL when the issuer is unknown
 * (the C equivalent of TypeScript's `string | null`). `input` borrows the
 * caller's string; `sanitized` and `formatted` are heap-owned and must be
 * released with card_info_free().
 */
typedef struct {
    const char *input;
    char *sanitized;     /* digits only */
    bool valid;          /* digits present, sane length, and Luhn passes */
    bool luhn_valid;     /* Luhn checksum passes (requires >= 12 digits) */
    const char *brand;   /* static string, or NULL when unknown */
    char *formatted;     /* grouped per brand spacing rules */
} card_info;

/* ---- Small byte helpers (digits are ASCII, so byte compares are safe). ---- */

static bool is_digit_byte(char c) { return c >= '0' && c <= '9'; }

static bool in_range(char c, char lo, char hi) { return c >= lo && c <= hi; }

static bool starts_with(const char *s, const char *prefix) {
    return strncmp(s, prefix, strlen(prefix)) == 0;
}

static bool len_ok(const size_t *lengths, size_t count, size_t n) {
    for (size_t i = 0; i < count; i++) {
        if (lengths[i] == n) {
            return true;
        }
    }
    return false;
}

/* Keep ASCII digit bytes only. Returns a heap string; NULL only on OOM. */
static char *sanitize_digits(const char *input) {
    size_t n = strlen(input);
    char *out = malloc(n + 1);
    if (out == NULL) {
        return NULL;
    }
    size_t j = 0;
    for (size_t i = 0; i < n; i++) {
        if (is_digit_byte(input[i])) {
            out[j++] = input[i];
        }
    }
    out[j] = '\0';
    return out;
}

/* Luhn (mod-10) checksum. Empty or non-digit input returns false. */
static bool luhn_check(const char *digits) {
    if (digits == NULL || digits[0] == '\0') {
        return false;
    }
    unsigned total = 0;
    bool should_double = false;
    /* Walk right-to-left; double every second digit, collapsing 10-18 to its digit-sum. */
    for (size_t i = strlen(digits); i-- > 0;) {
        char ch = digits[i];
        if (!is_digit_byte(ch)) {
            return false; /* non-digit -> invalid, matching /^[0-9]*$/ */
        }
        unsigned d = (unsigned)(ch - '0');
        if (should_double) {
            d *= 2;
            if (d > 9) {
                d -= 9;
            }
        }
        total += d;
        should_double = !should_double;
    }
    return total % 10 == 0;
}

/* ---- Brand prefix matchers (hand-rolled equivalents of the TS regexes). ---- */

/* Mastercard: 5[1-5], or 2(2[2-9] | [3-6][0-9] | 7[01] | 720). */
static bool matches_mastercard(const char *s) {
    size_t n = strlen(s);
    if (n >= 2 && s[0] == '5' && in_range(s[1], '1', '5')) {
        return true;
    }
    if (n >= 2 && s[0] == '2') {
        if (s[1] == '2' && n >= 3 && in_range(s[2], '2', '9')) {
            return true; /* 22[2-9] */
        }
        if (in_range(s[1], '3', '6') && n >= 3 && is_digit_byte(s[2])) {
            return true; /* 2[3-6][0-9] */
        }
        if (s[1] == '7') {
            if (n >= 3 && (s[2] == '0' || s[2] == '1')) {
                return true; /* 27[01] */
            }
            if (starts_with(s, "2720")) {
                return true; /* explicit 720 range end */
            }
        }
    }
    return false;
}

/* Discover: 6011, 65, 64[4-9], or 622(12[6-9] | 1[3-9][0-9] | [2-8][0-9][0-9] | 9([01][0-9] | 2[0-5])). */
static bool matches_discover(const char *s) {
    size_t n = strlen(s);
    if (starts_with(s, "6011")) {
        return true;
    }
    if (n < 2 || s[0] != '6') {
        return false;
    }
    if (s[1] == '5') {
        return true; /* 65 */
    }
    if (s[1] == '4' && n >= 3 && in_range(s[2], '4', '9')) {
        return true; /* 64[4-9] */
    }
    /* The 622 inner group consumes indices 3, 4, 5. */
    if (starts_with(s, "622") && n >= 6) {
        char c3 = s[3], c4 = s[4], c5 = s[5];
        if (c3 == '1' && c4 == '2' && in_range(c5, '6', '9')) {
            return true; /* 622 12[6-9] */
        }
        if (c3 == '1' && in_range(c4, '3', '9') && is_digit_byte(c5)) {
            return true; /* 622 1[3-9][0-9] */
        }
        if (in_range(c3, '2', '8') && is_digit_byte(c4) && is_digit_byte(c5)) {
            return true; /* 622 [2-8][0-9][0-9] */
        }
        if (c3 == '9') {
            if ((c4 == '0' || c4 == '1') && is_digit_byte(c5)) {
                return true; /* 622 9[01][0-9] */
            }
            if (c4 == '2' && in_range(c5, '0', '5')) {
                return true; /* 622 92[0-5] */
            }
        }
    }
    return false;
}

/* Diners Club (incl. Carte Blanche / international): 36, 38, 39, 54, 55, 30[0-5], 3095. */
static bool matches_diners(const char *s) {
    static const char *const pairs[] = {"36", "38", "39", "54", "55"};
    for (size_t i = 0; i < sizeof(pairs) / sizeof(pairs[0]); i++) {
        if (starts_with(s, pairs[i])) {
            return true;
        }
    }
    size_t n = strlen(s);
    if (n >= 3 && s[0] == '3' && s[1] == '0' && in_range(s[2], '0', '5')) {
        return true; /* 30[0-5] */
    }
    if (starts_with(s, "3095")) {
        return true; /* separate alternative: the 0-5 range does not cover the '9' */
    }
    return false;
}

/* JCB: 35(2[89] | [3-8][0-9]) — the inner group needs indices 2 and 3. */
static bool matches_jcb(const char *s) {
    size_t n = strlen(s);
    if (n < 4 || s[0] != '3' || s[1] != '5') {
        return false;
    }
    char c2 = s[2], c3 = s[3];
    return (c2 == '2' && (c3 == '8' || c3 == '9')) ||
           (in_range(c2, '3', '8') && is_digit_byte(c3));
}

/* Maestro: 50, 56-58, or any leading 6 (catch-all for 6xxx issuers). */
static bool matches_maestro(const char *s) {
    if (s[0] == '\0') {
        return false;
    }
    size_t n = strlen(s);
    if (n >= 2 && s[0] == '5' && (s[1] == '0' || in_range(s[1], '6', '8'))) {
        return true;
    }
    return s[0] == '6';
}

/*
 * Detect the card brand by prefix + length. Returns a static brand string,
 * or NULL when unknown.
 *
 * Order matters: the first prefix+length match wins. Maestro is last because
 * its broad "6" / 5x ranges overlap narrower issuers (Discover, UnionPay, ...).
 */
static const char *detect_brand(const char *digits) {
    if (digits == NULL || digits[0] == '\0') {
        return NULL;
    }
    size_t n = strlen(digits);
    static const size_t visa_lens[] = {13, 16, 19};
    static const size_t discover_lens[] = {16, 19};
    static const size_t diners_lens[] = {14, 16};
    static const size_t jcb_lens[] = {16, 17, 18, 19};
    static const size_t unionpay_lens[] = {16, 17, 18, 19};
    static const size_t maestro_lens[] = {12, 13, 14, 15, 16, 17, 18, 19};

    if (digits[0] == '4' && len_ok(visa_lens, 3, n)) {
        return "Visa"; /* leading 4 */
    }
    if (matches_mastercard(digits) && n == 16) {
        return "Mastercard"; /* 51-55, or 2221-2720 */
    }
    if ((starts_with(digits, "34") || starts_with(digits, "37")) && n == 15) {
        return "American Express";
    }
    if (matches_discover(digits) && len_ok(discover_lens, 2, n)) {
        return "Discover";
    }
    if (matches_diners(digits) && len_ok(diners_lens, 2, n)) {
        return "Diners Club";
    }
    if (matches_jcb(digits) && len_ok(jcb_lens, 4, n)) {
        return "JCB"; /* 3528-3589 */
    }
    if (starts_with(digits, "62") && len_ok(unionpay_lens, 4, n)) {
        return "UnionPay";
    }
    if (matches_maestro(digits) && len_ok(maestro_lens, 8, n)) {
        return "Maestro";
    }
    return NULL;
}

/* Slice digits into runs of the given widths, joining with spaces.
 * Returns a heap string; NULL only on OOM. */
static char *group(const char *digits, const size_t *widths, size_t count) {
    size_t n = strlen(digits);
    char *out = malloc(2 * n + 2); /* worst case: a space between every digit */
    if (out == NULL) {
        return NULL;
    }
    size_t o = 0, i = 0;
    for (size_t k = 0; k < count; k++) {
        if (i >= n) {
            break;
        }
        size_t end = i + widths[k];
        if (end > n) {
            end = n;
        }
        for (size_t j = i; j < end; j++) {
            out[o++] = digits[j];
        }
        out[o++] = ' ';
        i += widths[k];
    }
    /* Any trailing remainder (e.g. a partially typed number) is appended as-is. */
    if (i < n) {
        for (size_t j = i; j < n; j++) {
            out[o++] = digits[j];
        }
        out[o++] = ' ';
    }
    if (o > 0 && out[o - 1] == ' ') {
        o--; /* drop the join's trailing space */
    }
    out[o] = '\0';
    return out;
}

/* Insert a space every 4 digits, omitting the trailing space
 * (equivalent to the TS regex `(.{4})(?=.)` replacement). */
static char *default_group(const char *digits) {
    size_t n = strlen(digits);
    char *out = malloc(n + n / 4 + 1);
    if (out == NULL) {
        return NULL;
    }
    size_t o = 0;
    for (size_t i = 0; i < n; i++) {
        if (i > 0 && i % 4 == 0) {
            out[o++] = ' ';
        }
        out[o++] = digits[i];
    }
    out[o] = '\0';
    return out;
}

/* Group the number per brand spacing rules. Heap string; NULL only on OOM. */
static char *format_card(const char *digits, const char *brand) {
    if (digits == NULL || digits[0] == '\0') {
        char *empty = malloc(1);
        if (empty != NULL) {
            empty[0] = '\0';
        }
        return empty;
    }
    static const size_t amex[] = {4, 6, 5};
    static const size_t diners14[] = {4, 6, 4};
    /* Amex uses a 4-6-5 grouping; Diners Club's 14-digit variant uses 4-6-4. */
    if (brand != NULL && strcmp(brand, "American Express") == 0) {
        return group(digits, amex, 3);
    }
    if (brand != NULL && strcmp(brand, "Diners Club") == 0 && strlen(digits) == 14) {
        return group(digits, diners14, 3);
    }
    return default_group(digits);
}

/* Validate a card number. Always returns a card_info; never crashes.
 * Heap fields are NULL only on OOM (the caller still calls card_info_free). */
card_info validate_card(const char *input) {
    card_info info;
    info.input = input;
    info.sanitized = sanitize_digits(input);
    info.brand = detect_brand(info.sanitized != NULL ? info.sanitized : "");
    /* Luhn is only meaningful once we have a plausible card length (>= 12 digits). */
    info.luhn_valid = info.sanitized != NULL && strlen(info.sanitized) >= 12 &&
                      luhn_check(info.sanitized);
    info.valid = info.luhn_valid;
    info.formatted = format_card(info.sanitized != NULL ? info.sanitized : "", info.brand);
    return info;
}

/* Release the heap-owned fields of a card_info. */
void card_info_free(card_info *info) {
    if (info == NULL) {
        return;
    }
    free(info->sanitized);
    free(info->formatted);
    info->sanitized = NULL;
    info->formatted = NULL;
}

int main(void) {
    /* Tiny showcase: validate a well-known test number (Visa). */
    card_info info = validate_card("4111 1111 1111 1111");
    printf("brand=%s valid=%s formatted=\"%s\"\n",
           info.brand != NULL ? info.brand : "None",
           info.valid ? "true" : "false",
           info.formatted != NULL ? info.formatted : "");
    card_info_free(&info);
    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 →