Skip to content

IPv4 ↔ IPv6 Converter — C source

Convert between IPv4 and IPv6 addresses both ways. Parse and validate addresses, expand and compress IPv6 to its canonical RFC 5952 form, map an IPv4 into IPv4-mapped and IPv4-compatible IPv6 (or any custom /96 prefix), and extract an embedded IPv4 back out.

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

/*
 * ip-converter — IPv4 ↔ IPv6 conversion (polyglot showcase: C)
 *
 * Language: C (C11, standard library only)
 * Source:   CosmoDev polyglot showcase port of the ip-converter tool,
 *           ported from src/lib/ip-converter.ts (the canonical TypeScript
 *           implementation).
 * License:  display source — part of CosmoDev's polyglot tool pages.
 *
 * Pure, deterministic IPv4/IPv6 address conversion logic. Every parse
 * function returns false (or writes nothing) on invalid input rather than
 * aborting, so the UI can show a graceful error. IPv6 text follows RFC 5952:
 * lowercase hex, no leading zeros, the single longest run of zero groups
 * collapsed to "::", and a dotted-decimal tail only for IPv4-mapped
 * ("::ffff:") addresses.
 *
 * C has no Option/Result type, so parse functions write through out-params
 * and return bool; string renderers draw into caller-provided buffers whose
 * required sizes are named by the IPCONV_*_MAX constants. The TS regex
 * guards are hand-rolled as length + charset checks, mirroring the Rust
 * port. Public functions carry the ipconv_ prefix (C has no namespaces).
 */

#include <ctype.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define IPCONV_IPV4_STRING_MAX 16    /* "255.255.255.255" + NUL */
#define IPCONV_IPV6_STRING_MAX 46    /* covers canonical + expanded forms */
#define IPCONV_IPV6_EXPANDED_MAX 40  /* 8*4 hex digits + 7 colons + NUL */

/* Embedding family for placing an IPv4 quad inside an IPv6 address. */
typedef enum ipconv_embed_mode {
    /* ::ffff:a.b.c.d — the modern, non-deprecated IPv4-mapped form (default). */
    IPCONV_EMBED_MAPPED,
    /* ::a.b.c.d — the deprecated IPv4-compatible form. */
    IPCONV_EMBED_COMPATIBLE
} ipconv_embed_mode;

/* Options for embedding an IPv4 octet quad into an IPv6 address. */
typedef struct ipconv_ipv4_to_ipv6_options {
    /* Embedding family. Ignored when prefix is non-NULL. */
    ipconv_embed_mode mode;
    /* Optional custom high-96-bit prefix (a valid IPv6 string; its first six
     * groups are used and its low 32 bits are overwritten by the IPv4), e.g.
     * "64:ff9b::" yields a NAT64-style "64:ff9b::a.b.c.d". Overrides mode. */
    const char *prefix;
} ipconv_ipv4_to_ipv6_options;

/* A valid IPv6 group token: 1-4 hex digits, no sign, no underscores.
 * (Equivalent to the TS /^[0-9a-fA-F]{1,4}$/ regex.) */
static bool is_hex_group(const char *s)
{
    size_t len = strlen(s);
    if (len < 1 || len > 4)
        return false;
    for (size_t i = 0; i < len; i++) {
        char c = s[i];
        bool hex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') ||
                   (c >= 'A' && c <= 'F');
        if (!hex)
            return false;
    }
    return true;
}

/* A valid IPv4 octet token: 1-3 decimal digits. Range is enforced separately.
 * (Equivalent to the TS /^\d{1,3}$/ regex.) */
static bool is_dec3(const char *s)
{
    size_t len = strlen(s);
    if (len < 1 || len > 3)
        return false;
    for (size_t i = 0; i < len; i++)
        if (s[i] < '0' || s[i] > '9')
            return false;
    return true;
}

/* Locate s with surrounding whitespace removed, without copying. Mirrors
 * Python's str.strip() / Rust's str::trim(). */
static void trimmed_span(const char *s, const char **start, size_t *len)
{
    const char *b = s;
    while (isspace((unsigned char)*b))
        b++;
    const char *e = s + strlen(s);
    while (e > b && isspace((unsigned char)e[-1]))
        e--;
    *start = b;
    *len = (size_t)(e - b);
}

/*
 * Parse a dotted-decimal IPv4 string into four octets, validating each is
 * 0-255. Returns false for anything that is not exactly four numeric octets
 * in range.
 */
bool ipconv_parse_ipv4(const char *s, uint8_t out[4])
{
    const char *t;
    size_t tlen;
    trimmed_span(s, &t, &tlen);
    if (tlen == 0 || tlen >= 64)
        return false; /* a valid quad is at most 15 chars; bound the scan */

    int n = 0;
    size_t start = 0;
    char tok[16];
    for (size_t i = 0; i <= tlen; i++) {
        if (i != tlen && t[i] != '.')
            continue;
        size_t dlen = i - start;
        if (dlen == 0 || dlen >= sizeof tok)
            return false;
        memcpy(tok, t + start, dlen);
        tok[dlen] = '\0';
        if (!is_dec3(tok))
            return false;
        /* is_dec3 guarantees digits only, so base-10 conversion cannot fail. */
        unsigned long v = strtoul(tok, NULL, 10);
        if (v > 255)
            return false;
        if (n >= 4)
            return false;
        out[n++] = (uint8_t)v;
        start = i + 1;
    }
    return n == 4;
}

/* Render four octets as "a.b.c.d". (The octet type is uint8_t, which already
 * guarantees range, but the signature keeps the symmetry with the other
 * ports.) */
void ipconv_ipv4_to_string(const uint8_t octets[4],
                           char out[IPCONV_IPV4_STRING_MAX])
{
    sprintf(out, "%u.%u.%u.%u", (unsigned)octets[0], (unsigned)octets[1],
            (unsigned)octets[2], (unsigned)octets[3]);
}

/* Count non-overlapping occurrences of "::" — used to enforce the
 * at-most-one-compression rule without a regex. */
static int count_double_colon(const char *s)
{
    int n = 0;
    const char *p = strstr(s, "::");
    while (p != NULL) {
        n++;
        p = strstr(p + 2, "::");
    }
    return n;
}

/* Parse colon-separated hex groups from the s[0..len) segment into out.
 * Returns the group count, or -1 on any invalid token. An empty segment
 * yields 0 tokens (mirrors the Rust empty-input split guard). */
static int parse_hex_groups(const char *s, size_t len, uint16_t *out, int cap)
{
    if (len == 0)
        return 0;
    int n = 0;
    size_t start = 0;
    char tok[16];
    for (size_t i = 0; i <= len; i++) {
        if (i != len && s[i] != ':')
            continue;
        size_t tlen = i - start;
        if (tlen == 0 || tlen >= sizeof tok)
            return -1;
        memcpy(tok, s + start, tlen);
        tok[tlen] = '\0';
        if (!is_hex_group(tok))
            return -1;
        if (n >= cap)
            return -1; /* more groups than any valid address can hold */
        out[n++] = (uint16_t)strtoul(tok, NULL, 16);
        start = i + 1;
    }
    return n;
}

/* Parse the colon-separated tokens of s[0..len) into out. The FINAL token may
 * instead be a dotted-decimal IPv4 quad, which contributes two groups (high
 * octet pair, low octet pair); every other token must be a plain hex group.
 * Returns the group count, or -1 on invalid input. */
static int parse_tail_groups(const char *s, size_t len, uint16_t *out, int cap)
{
    if (len == 0)
        return 0;
    /* First pass: count tokens so the final one is knowable. */
    size_t ntok = 1;
    for (size_t i = 0; i < len; i++)
        if (s[i] == ':')
            ntok++;

    int n = 0;
    size_t start = 0;
    size_t idx = 0;
    char tok[24];
    for (size_t i = 0; i <= len; i++) {
        if (i != len && s[i] != ':')
            continue;
        size_t tlen = i - start;
        if (tlen == 0 || tlen >= sizeof tok)
            return -1;
        memcpy(tok, s + start, tlen);
        tok[tlen] = '\0';
        if (idx == ntok - 1 && strchr(tok, '.') != NULL) {
            uint8_t oct[4];
            if (!ipconv_parse_ipv4(tok, oct))
                return -1;
            if (n + 2 > cap)
                return -1;
            out[n++] = (uint16_t)(((unsigned)oct[0] << 8) | oct[1]);
            out[n++] = (uint16_t)(((unsigned)oct[2] << 8) | oct[3]);
        } else {
            if (!is_hex_group(tok))
                return -1;
            if (n + 1 > cap)
                return -1;
            out[n++] = (uint16_t)strtoul(tok, NULL, 16);
        }
        idx++;
        start = i + 1;
    }
    return n;
}

/*
 * Parse an IPv6 string (with "::" compression, hex groups, and an optional
 * dotted-decimal IPv4 tail for mapped/compatible forms) into eight 16-bit
 * groups. Returns false on any malformed input — never crashes.
 */
bool ipconv_parse_ipv6(const char *s, uint16_t out[8])
{
    const char *t;
    size_t inlen;
    trimmed_span(s, &t, &inlen);
    if (inlen == 0 || inlen >= 64)
        return false; /* longest valid IPv6 text is 45 chars */
    char input[64];
    memcpy(input, t, inlen);
    input[inlen] = '\0';

    /* At most one "::" run is legal; reject ambiguous double-compression. */
    if (count_double_colon(input) > 1)
        return false;

    uint16_t head[8];
    uint16_t tail[8];
    const char *dc = strstr(input, "::");

    if (dc != NULL) {
        size_t blen = (size_t)(dc - input);
        size_t alen = inlen - blen - 2;
        int hcount = parse_hex_groups(input, blen, head, 8);
        if (hcount < 0)
            return false;
        int tcount = parse_tail_groups(dc + 2, alen, tail, 8);
        if (tcount < 0)
            return false;

        int total = hcount + tcount;
        if (total >= 8)
            return false; /* "::" must elide at least one group. */
        /* Zero everything first — the middle [hcount .. 8 - tcount] stays
         * zero, the elided run "::" stands in for. (The Rust original gets
         * this for free from [0u16; 8] initialization.) */
        memset(out, 0, sizeof(uint16_t) * 8);
        for (int k = 0; k < hcount; k++)
            out[k] = head[k];
        /* The middle [hcount .. 8 - tcount] stays zero — that is the elided
         * run "::" stands in for. */
        for (int k = 0; k < tcount; k++)
            out[8 - tcount + k] = tail[k];
        return true;
    }

    /* No compression: split on ':' and parse, allowing a dotted-quad only in
     * the last slot. The result must be exactly eight groups. */
    int n = parse_tail_groups(input, inlen, tail, 8);
    if (n != 8)
        return false;
    memcpy(out, tail, sizeof(uint16_t) * 8);
    return true;
}

/* True when the eight groups form an IPv4-mapped ("::ffff:") address. */
static bool is_mapped(const uint16_t g[8])
{
    return g[0] == 0 && g[1] == 0 && g[2] == 0 && g[3] == 0 && g[4] == 0 &&
           g[5] == 0xFFFF;
}

/* True when the eight groups form an IPv4-compatible ("::") address. */
static bool is_compatible(const uint16_t g[8])
{
    return g[0] == 0 && g[1] == 0 && g[2] == 0 && g[3] == 0 && g[4] == 0 &&
           g[5] == 0;
}

/* Append lowercase hex for one 16-bit group (no leading zeros) at p and
 * return the number of characters written. */
static size_t append_hex(uint16_t v, char *p)
{
    char tmp[5];
    int n = sprintf(tmp, "%x", (unsigned)v);
    memcpy(p, tmp, (size_t)n);
    return (size_t)n;
}

/* RFC 5952 compression over `count` groups (8 for a whole address, 6 for the
 * high part of an embedded-IPv4 render). Collapse the longest run (>= 2) of
 * zero groups into "::" — first run wins on ties — and strip leading zeros.
 * NUL-terminates out and returns the number of characters written. */
static size_t compress_groups_n(const uint16_t *groups, size_t count, char *out)
{
    size_t best_start = 0, best_len = 0, cur_start = 0, cur_len = 0;
    /* Track the longest run of consecutive zero groups. best_start records
     * the first run of the longest length (strict > keeps earliest). */
    for (size_t i = 0; i < count; i++) {
        if (groups[i] == 0) {
            if (cur_len == 0)
                cur_start = i;
            cur_len++;
            if (cur_len > best_len) {
                best_len = cur_len;
                best_start = cur_start;
            }
        } else {
            cur_len = 0;
        }
    }

    char *p = out;
    if (best_len < 2) {
        for (size_t i = 0; i < count; i++) {
            if (i > 0)
                *p++ = ':';
            p += append_hex(groups[i], p);
        }
    } else {
        for (size_t i = 0; i < best_start; i++) {
            if (i > 0)
                *p++ = ':';
            p += append_hex(groups[i], p);
        }
        *p++ = ':';
        *p++ = ':';
        for (size_t i = best_start + best_len; i < count; i++) {
            if (i > best_start + best_len)
                *p++ = ':';
            p += append_hex(groups[i], p);
        }
    }
    *p = '\0';
    return (size_t)(p - out);
}

/* Render a compressed high part followed by a dotted-decimal IPv4 tail. When
 * the high part already ends in "::" (its zero run reaches the boundary) the
 * IPv4 attaches directly; otherwise a single ":" separates them — so
 * "::ffff:" → "::ffff:a.b.c.d" and "::" → "::a.b.c.d". */
static void render_with_embedded_tail(const uint16_t high[6],
                                      const uint8_t octets[4], char *out)
{
    char high_str[6 * 4 + 5 + 1]; /* six 4-digit groups + 5 colons + NUL */
    compress_groups_n(high, 6, high_str);
    size_t n = strlen(high_str);
    bool bare = n >= 2 && high_str[n - 1] == ':' && high_str[n - 2] == ':';
    sprintf(out, "%s%s%u.%u.%u.%u", high_str, bare ? "" : ":",
            (unsigned)octets[0], (unsigned)octets[1], (unsigned)octets[2],
            (unsigned)octets[3]);
}

/* Canonical RFC 5952 text for eight groups: a dotted-decimal tail for
 * IPv4-mapped ("::ffff:") addresses, otherwise pure compressed hex. The
 * deprecated IPv4-compatible range ("::/96") is NOT rendered dotted here —
 * that would mis-render the unspecified ("::") and loopback ("::1")
 * addresses as "::0.0.0.0" / "::0.0.0.1". Compatible extraction is still
 * available via ipconv_ipv6_to_ipv4; on-demand compatible generation via
 * ipconv_ipv4_to_ipv6 is untouched. */
static void render_canonical(const uint16_t groups[8], char *out)
{
    if (is_mapped(groups)) {
        uint8_t octets[4] = {
            (uint8_t)(groups[6] >> 8),
            (uint8_t)(groups[6] & 0xFF),
            (uint8_t)(groups[7] >> 8),
            (uint8_t)(groups[7] & 0xFF),
        };
        uint16_t high[6] = {groups[0], groups[1], groups[2],
                            groups[3], groups[4], groups[5]};
        render_with_embedded_tail(high, octets, out);
    } else {
        compress_groups_n(groups, 8, out);
    }
}

/* Render eight groups as canonical compressed IPv6. */
void ipconv_ipv6_to_string(const uint16_t groups[8],
                           char out[IPCONV_IPV6_STRING_MAX])
{
    render_canonical(groups, out);
}

/*
 * Expand an IPv6 string to its full eight-group, four-hex-digit form;
 * false if invalid.
 */
bool ipconv_expand_ipv6(const char *s, char out[IPCONV_IPV6_EXPANDED_MAX])
{
    uint16_t g[8];
    if (!ipconv_parse_ipv6(s, g))
        return false;
    char *p = out;
    for (int i = 0; i < 8; i++) {
        if (i > 0)
            *p++ = ':';
        /* Left-pad each group to a fixed 4-digit width: 0000..ffff. */
        p += sprintf(p, "%04x", (unsigned)g[i]);
    }
    *p = '\0';
    return true;
}

/* Compress an IPv6 string to its RFC 5952 canonical form; false if invalid. */
bool ipconv_compress_ipv6(const char *s, char out[IPCONV_IPV6_STRING_MAX])
{
    uint16_t g[8];
    if (!ipconv_parse_ipv6(s, g))
        return false;
    render_canonical(g, out);
    return true;
}

/*
 * Embed an IPv4 octet quad into an IPv6 address. By default produces the
 * IPv4-mapped form "::ffff:a.b.c.d"; mode = IPCONV_EMBED_COMPATIBLE yields
 * "::a.b.c.d"; a set prefix overrides both and places the IPv4 after any
 * custom /96 prefix (e.g. "64:ff9b::a.b.c.d"). Returns false for an invalid
 * prefix. (opts may be NULL for the default mapped form.)
 */
bool ipconv_ipv4_to_ipv6(const uint8_t octets[4],
                         const ipconv_ipv4_to_ipv6_options *opts,
                         char out[IPCONV_IPV6_STRING_MAX])
{
    if (opts != NULL && opts->prefix != NULL) {
        uint16_t p[8];
        if (!ipconv_parse_ipv6(opts->prefix, p))
            return false;
        uint16_t high[6];
        memcpy(high, p, sizeof high);
        render_with_embedded_tail(high, octets, out);
        return true;
    }
    if (opts != NULL && opts->mode == IPCONV_EMBED_COMPATIBLE) {
        uint16_t compatible[6] = {0, 0, 0, 0, 0, 0};
        render_with_embedded_tail(compatible, octets, out);
    } else {
        uint16_t mapped[6] = {0, 0, 0, 0, 0, 0xFFFF};
        render_with_embedded_tail(mapped, octets, out);
    }
    return true;
}

/*
 * Extract the embedded IPv4 from an IPv4-mapped ("::ffff:a.b.c.d") or
 * IPv4-compatible ("::a.b.c.d") address into dotted-decimal form; false when
 * the address carries no embedded IPv4 (or is unparseable).
 */
bool ipconv_ipv6_to_ipv4(const char *s, char out[IPCONV_IPV4_STRING_MAX])
{
    uint16_t g[8];
    if (!ipconv_parse_ipv6(s, g))
        return false;
    if (!(is_mapped(g) || is_compatible(g)))
        return false;
    sprintf(out, "%u.%u.%u.%u", (unsigned)(g[6] >> 8), (unsigned)(g[6] & 0xFF),
            (unsigned)(g[7] >> 8), (unsigned)(g[7] & 0xFF));
    return true;
}

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 →