IPv4 ↔ IPv6 Converter — PHP 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 PHP implementation — the same logic the interactive tool runs, in a shareable, citable form.
<?php
// =============================================================================
// ip-converter — IPv4 ↔ IPv6 conversion (polyglot showcase: PHP)
// 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 rather than
// 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.
//
// Namespaced under CosmoDev\IpConverter to keep the global space clean. Uses
// only PHP's standard library (preg_match, explode/implode, dechex/hexdec).
// =============================================================================
namespace CosmoDev\IpConverter;
// One IPv6 group: 1-4 hex digits. (Namespace-level consts in PHP carry no
// visibility modifier; they are implicitly package-visible here.)
const HEX = '/^[0-9a-fA-F]{1,4}$/';
// One IPv4 octet token: 1-3 decimal digits (range checked separately).
const DEC3 = '/^\d{1,3}$/';
/**
* Options for embedding an IPv4 octet quad into an IPv6 address.
*/
final class Ipv4ToIpv6Options
{
/**
* Embedding family. 'mapped' (default) → `::ffff:a.b.c.d`;
* 'compatible' → `::a.b.c.d`. Ignored when $prefix is non-null.
*/
public ?string $mode = null;
/**
* 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.
*/
public ?string $prefix = null;
public function __construct(?string $mode = null, ?string $prefix = null)
{
$this->mode = $mode;
$this->prefix = $prefix;
}
}
/**
* True when $groups is exactly eight integers in the 16-bit range.
*/
function isValidIpv6Groups(array $groups): bool
{
if (count($groups) !== 8) {
return false;
}
foreach ($groups as $v) {
if (!is_int($v) || $v < 0 || $v > 0xffff) {
return false;
}
}
return true;
}
/**
* Lowercase hex for one 16-bit group, with no leading zeros.
*/
function hexGroup(int $v): string
{
return dechex($v);
}
/**
* 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.
*
* @return array<int>|null
*/
function parseIpv4(string $s): ?array
{
$parts = explode('.', trim($s));
if (count($parts) !== 4) {
return null;
}
$octets = [];
foreach ($parts as $p) {
if (!preg_match(DEC3, $p)) {
return null;
}
// DEC3 guarantees digits only; intval is safe and intentional here.
$n = intval($p, 10);
if ($n < 0 || $n > 255) {
return null;
}
$octets[] = $n;
}
return $octets;
}
/**
* Render four octets as `a.b.c.d`, or `''` if the octets are out of range.
*
* @param array<int> $octets
*/
function ipv4ToString(array $octets): string
{
if (count($octets) !== 4) {
return '';
}
foreach ($octets as $o) {
if (!is_int($o) || $o < 0 || $o > 255) {
return '';
}
}
return implode('.', $octets);
}
/**
* 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.
*
* @return array<int>|null
*/
function parseIpv6(string $s): ?array
{
$input = trim($s);
if ($input === '') {
return null;
}
// At most one `::` run is legal; reject ambiguous double-compression.
if (substr_count($input, '::') > 1) {
return null;
}
$dc = strpos($input, '::');
if ($dc !== false) {
$before = substr($input, 0, $dc);
$after = substr($input, $dc + 2);
$headTokens = $before === '' ? [] : explode(':', $before);
$tailTokens = $after === '' ? [] : explode(':', $after);
$head = [];
foreach ($headTokens as $g) {
if (!preg_match(HEX, $g)) {
return null;
}
$head[] = intval($g, 16);
}
$tail = [];
$tailCount = count($tailTokens);
for ($i = 0; $i < $tailCount; $i++) {
$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 === $tailCount - 1 && strpos($g, '.') !== false) {
$oct = parseIpv4($g);
if ($oct === null) {
return null;
}
$tail[] = ($oct[0] << 8) | $oct[1];
$tail[] = ($oct[2] << 8) | $oct[3];
} else {
if (!preg_match(HEX, $g)) {
return null;
}
$tail[] = intval($g, 16);
}
}
$total = count($head) + count($tail);
// `::` must elide at least one group.
if ($total >= 8) {
return null;
}
$zeroPad = array_fill(0, 8 - $total, 0);
return array_merge($head, $zeroPad, $tail);
}
// No compression: split on ':' and parse, allowing a dotted-quad only in
// the last slot. The result must be exactly eight groups.
$tokens = explode(':', $input);
$groups = [];
$tokenCount = count($tokens);
for ($i = 0; $i < $tokenCount; $i++) {
$g = $tokens[$i];
if ($i === $tokenCount - 1 && strpos($g, '.') !== false) {
$oct = parseIpv4($g);
if ($oct === null) {
return null;
}
$groups[] = ($oct[0] << 8) | $oct[1];
$groups[] = ($oct[2] << 8) | $oct[3];
} else {
if (!preg_match(HEX, $g)) {
return null;
}
$groups[] = intval($g, 16);
}
}
return count($groups) === 8 ? $groups : null;
}
/**
* True when the eight groups form an IPv4-mapped (`::ffff:`) address.
*
* @param array<int> $g
*/
function isMapped(array $g): bool
{
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.
*
* @param array<int> $g
*/
function isCompatible(array $g): bool
{
return $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 array<int> $groups
*/
function compressGroups(array $groups): string
{
$bestStart = -1;
$bestLen = 0;
$curStart = -1;
$curLen = 0;
// Track the longest run of consecutive zero groups. $bestStart records the
// first run of the longest length encountered (strict > keeps earliest).
for ($i = 0; $i < count($groups); $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 implode(':', array_map('dechex', $groups));
}
$before = implode(':', array_map('dechex', array_slice($groups, 0, $bestStart)));
$after = implode(':', array_map('dechex', array_slice($groups, $bestStart + $bestLen)));
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 array<int> $high
* @param array<int> $octets
*/
function renderWithEmbeddedTail(array $high, array $octets): string
{
$highStr = compressGroups($high);
$ipv4 = implode('.', $octets);
// str_ends_with is PHP 8+; we keep the suffix check explicit for clarity.
return substr($highStr, -2) === '::'
? $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 array<int> $groups
*/
function renderCanonical(array $groups): string
{
if (isMapped($groups)) {
$octets = [
($groups[6] >> 8) & 0xff,
$groups[6] & 0xff,
($groups[7] >> 8) & 0xff,
$groups[7] & 0xff,
];
return renderWithEmbeddedTail(array_slice($groups, 0, 6), $octets);
}
return compressGroups($groups);
}
/**
* Render eight groups as canonical compressed IPv6, or `''` if invalid.
*
* @param array<int> $groups
*/
function ipv6ToString(array $groups): string
{
if (!isValidIpv6Groups($groups)) {
return '';
}
return renderCanonical($groups);
}
/**
* Expand an IPv6 string to its full eight-group, four-hex-digit form; `''` if
* invalid.
*/
function expandIpv6(string $s): string
{
$g = parseIpv6($s);
if ($g === null) {
return '';
}
// str_pad left-pads each group to a fixed 4-digit width: 0000..ffff.
$parts = array_map(
fn(int $v): string => str_pad(dechex($v), 4, '0', STR_PAD_LEFT),
$g
);
return implode(':', $parts);
}
/**
* Compress an IPv6 string to its RFC 5952 canonical form; `''` if invalid.
*/
function compressIpv6(string $s): string
{
$g = parseIpv6($s);
if ($g === null) {
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
* non-null 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 array<int> $octets
* @param Ipv4ToIpv6Options|null $opts
*/
function ipv4ToIpv6(array $octets, ?Ipv4ToIpv6Options $opts = null): string
{
if (count($octets) !== 4) {
return '';
}
foreach ($octets as $o) {
if (!is_int($o) || $o < 0 || $o > 255) {
return '';
}
}
if ($opts !== null && $opts->prefix !== null) {
$p = parseIpv6($opts->prefix);
if ($p === null) {
return '';
}
return renderWithEmbeddedTail(array_slice($p, 0, 6), $octets);
}
if ($opts !== null && $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).
*/
function ipv6ToIpv4(string $s): ?string
{
$g = parseIpv6($s);
if ($g === null || !(isMapped($g) || isCompatible($g))) {
return null;
}
$octets = [
($g[6] >> 8) & 0xff,
$g[6] & 0xff,
($g[7] >> 8) & 0xff,
$g[7] & 0xff,
];
return implode('.', $octets);
}
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 →