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 →