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 →