IPv4 ↔ IPv6 Converter — JavaScript 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 JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
// =============================================================================
// ip-converter - IPv4 ↔ IPv6 conversion (polyglot showcase: JavaScript)
// CosmoDev polyglot port of ip-converter, ported from src/lib/ip-converter.ts.
// Display source - part of CosmoDev's polyglot tool pages.
//
// Pure, deterministic IPv4/IPv6 address conversion logic. Every parse function
// returns `null` (or '' for the string renderers) on invalid input instead of
// throwing, 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.
//
// This is the ES module variant of the canonical TypeScript lib: same exports,
// same behavior, JSDoc in place of TS interfaces. Stdlib only.
// =============================================================================
/**
* @typedef {Object} Ipv4ToIpv6Options
* @property {'mapped'|'compatible'} [mode] - Embedding family. `mapped`
* (default) → `::ffff:a.b.c.d`; `compatible` → `::a.b.c.d`. Ignored when
* `prefix` is supplied.
* @property {string} [prefix] - Optional custom high-96-bit prefix (a valid
* IPv6 string; its first 6 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`.
*/
// 1-4 hex digits (a single IPv6 group, pre-`::`/post-`::` token).
const HEX = /^[0-9a-fA-F]{1,4}$/;
// 1-3 decimal digits (an IPv4 octet, before the 0-255 range check).
const DEC3 = /^\d{1,3}$/;
/**
* Lowercase hex for one 16-bit group, with no leading zeros.
* @param {number} v
* @returns {string}
*/
const hexGroup = (v) => v.toString(16);
/**
* Parse a dotted-decimal IPv4 string into four octets, validating each is 0-255.
* Returns `null` for anything that is not exactly four numeric octets in range.
* @param {string} s
* @returns {number[] | null}
*/
export function parseIpv4(s) {
const parts = s.trim().split('.');
if (parts.length !== 4) return null;
/** @type {number[]} */
const octets = [];
for (const p of parts) {
if (!DEC3.test(p)) return null;
const n = Number(p);
if (n < 0 || n > 255) return null;
octets.push(n);
}
return octets;
}
/**
* Render four octets as `a.b.c.d`, or `''` if the octets are out of range.
* @param {number[]} octets
* @returns {string}
*/
export function ipv4ToString(octets) {
if (
!Array.isArray(octets) ||
octets.length !== 4 ||
!octets.every((o) => Number.isInteger(o) && o >= 0 && o <= 255)
) {
return '';
}
return octets.join('.');
}
/**
* 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 `null` on any malformed input - never throws.
* @param {string} s
* @returns {number[] | null}
*/
export function parseIpv6(s) {
const input = s.trim();
if (!input) return null;
// At most one `::` run is legal; reject ambiguous double-compression.
if ((input.match(/::/g) || []).length > 1) return null;
const dc = input.indexOf('::');
if (dc >= 0) {
const before = input.slice(0, dc);
const after = input.slice(dc + 2);
const headTokens = before === '' ? [] : before.split(':');
const tailTokens = after === '' ? [] : after.split(':');
const head = [];
for (const g of headTokens) {
if (!HEX.test(g)) return null;
head.push(parseInt(g, 16));
}
const tail = [];
for (let i = 0; i < tailTokens.length; i++) {
const g = tailTokens[i];
// A dotted-quad IPv4 tail is permitted only in the final slot, where it
// contributes two groups (high octet pair, low octet pair).
if (i === tailTokens.length - 1 && g.includes('.')) {
const oct = parseIpv4(g);
if (!oct) return null;
tail.push((oct[0] << 8) | oct[1], (oct[2] << 8) | oct[3]);
} else {
if (!HEX.test(g)) return null;
tail.push(parseInt(g, 16));
}
}
const total = head.length + tail.length;
if (total >= 8) return null; // `::` must elide at least one group
return [...head, ...new Array(8 - total).fill(0), ...tail];
}
// No compression: split on ':' and parse, allowing a dotted-quad only in the
// last slot. The result must be exactly eight groups.
const tokens = input.split(':');
const groups = [];
for (let i = 0; i < tokens.length; i++) {
const g = tokens[i];
if (i === tokens.length - 1 && g.includes('.')) {
const oct = parseIpv4(g);
if (!oct) return null;
groups.push((oct[0] << 8) | oct[1], (oct[2] << 8) | oct[3]);
} else {
if (!HEX.test(g)) return null;
groups.push(parseInt(g, 16));
}
}
return groups.length === 8 ? groups : null;
}
/**
* True when the eight groups form an IPv4-mapped (`::ffff:`) address.
* @param {number[]} g
* @returns {boolean}
*/
const isMapped = (g) =>
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.
* @param {number[]} g
* @returns {boolean}
*/
const isCompatible = (g) =>
g[0] === 0 && g[1] === 0 && g[2] === 0 && g[3] === 0 && g[4] === 0 && g[5] === 0;
/**
* Collapse the longest run (length ≥ 2) of zero groups into `::` (first run
* wins on ties) and strip leading zeros - RFC 5952 canonical text for pure-hex
* IPv6. Does not emit dotted-decimal; call `renderCanonical` for that.
* @param {number[]} groups
* @returns {string}
*/
function compressGroups(groups) {
let bestStart = -1;
let bestLen = 0;
let curStart = -1;
let curLen = 0;
// Track the longest run of consecutive zero groups. `bestStart` records the
// first run of the longest length encountered (strict `>` keeps earliest).
for (let i = 0; i < groups.length; i++) {
if (groups[i] === 0) {
if (curStart < 0) curStart = i;
curLen++;
if (curLen > bestLen) {
bestLen = curLen;
bestStart = curStart;
}
} else {
curStart = -1;
curLen = 0;
}
}
if (bestLen < 2) return groups.map(hexGroup).join(':');
const before = groups.slice(0, bestStart).map(hexGroup).join(':');
const after = groups.slice(bestStart + bestLen).map(hexGroup).join(':');
return `${before}::${after}`;
}
/**
* 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`.
* @param {number[]} high
* @param {number[]} octets
* @returns {string}
*/
function renderWithEmbeddedTail(high, octets) {
const highStr = compressGroups(high);
const ipv4 = octets.join('.');
return highStr.endsWith('::') ? `${highStr}${ipv4}` : `${highStr}:${ipv4}`;
}
/**
* 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
* `ipv6ToIpv4`; on-demand compatible generation via `ipv4ToIpv6` is untouched.
* @param {number[]} groups
* @returns {string}
*/
function renderCanonical(groups) {
if (isMapped(groups)) {
const octets = [
(groups[6] >> 8) & 0xff,
groups[6] & 0xff,
(groups[7] >> 8) & 0xff,
groups[7] & 0xff,
];
return renderWithEmbeddedTail(groups.slice(0, 6), octets);
}
return compressGroups(groups);
}
/**
* Render eight groups as canonical compressed IPv6, or `''` if invalid.
* @param {number[]} groups
* @returns {string}
*/
export function ipv6ToString(groups) {
if (!isValidIpv6Groups(groups)) return '';
return renderCanonical(groups);
}
/**
* Expand an IPv6 string to its full eight-group, four-hex-digit form; `''` if invalid.
* @param {string} s
* @returns {string}
*/
export function expandIpv6(s) {
const g = parseIpv6(s);
if (!g) return '';
return g.map((v) => v.toString(16).padStart(4, '0')).join(':');
}
/**
* Compress an IPv6 string to its RFC 5952 canonical form; `''` if invalid.
* @param {string} s
* @returns {string}
*/
export function compressIpv6(s) {
const g = parseIpv6(s);
if (!g) return '';
return renderCanonical(g);
}
/**
* Embed an IPv4 octet quad into an IPv6 address. By default produces the
* IPv4-mapped form `::ffff:a.b.c.d`; `mode: 'compatible'` yields `::a.b.c.d`;
* a `prefix` overrides both and places the IPv4 after any custom /96 prefix
* (e.g. `64:ff9b::a.b.c.d`). Returns `''` for invalid octets or prefix.
* @param {number[]} octets
* @param {Ipv4ToIpv6Options} [opts]
* @returns {string}
*/
export function ipv4ToIpv6(octets, opts) {
if (
!Array.isArray(octets) ||
octets.length !== 4 ||
!octets.every((o) => Number.isInteger(o) && o >= 0 && o <= 255)
) {
return '';
}
if (opts && opts.prefix !== undefined) {
const p = parseIpv6(opts.prefix);
if (!p) return '';
return renderWithEmbeddedTail(p.slice(0, 6), octets);
}
if (opts && opts.mode === 'compatible') {
return renderWithEmbeddedTail([0, 0, 0, 0, 0, 0], octets);
}
return renderWithEmbeddedTail([0, 0, 0, 0, 0, 0xffff], octets);
}
/**
* Extract the embedded IPv4 from an IPv4-mapped (`::ffff:a.b.c.d`) or
* IPv4-compatible (`::a.b.c.d`) address, returning dotted-decimal or `null`
* when the address carries no embedded IPv4 (or is unparseable).
* @param {string} s
* @returns {string | null}
*/
export function ipv6ToIpv4(s) {
const g = parseIpv6(s);
if (!g || !(isMapped(g) || isCompatible(g))) return null;
const octets = [
(g[6] >> 8) & 0xff,
g[6] & 0xff,
(g[7] >> 8) & 0xff,
g[7] & 0xff,
];
return octets.join('.');
}
/** True when `groups` is exactly eight integers in the 16-bit range. */
function isValidIpv6Groups(groups) {
return (
Array.isArray(groups) &&
groups.length === 8 &&
groups.every((v) => Number.isInteger(v) && v >= 0 && v <= 0xffff)
);
}
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 →