IPv4 ↔ IPv6 Converter — Java 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 Java implementation — the same logic the interactive tool runs, in a shareable, citable form.
// ip-converter — IPv4 ↔ IPv6 conversion (polyglot showcase: Java)
//
// Language: Java (17, 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 Optional.empty() (or Optional.empty() 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.
//
// Octets and 16-bit groups are plain ints confined to 0..255 / 0..0xFFFF by
// the parsers (Java has no unsigned byte/short arithmetic), mirroring the
// Python port. The TS regex guards are hand-rolled as length + charset
// checks. Note String.split needs limit -1 to keep trailing empty tokens,
// which this parser relies on.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public final class IpConverter {
private IpConverter() {
}
/** Embedding family for placing an IPv4 quad inside an IPv6 address. */
public enum EmbedMode {
/** ::ffff:a.b.c.d — the modern, non-deprecated IPv4-mapped form (default). */
MAPPED,
/** ::a.b.c.d — the deprecated IPv4-compatible form. */
COMPATIBLE
}
/**
* Options for embedding an IPv4 octet quad into an IPv6 address. A null
* mode means MAPPED; a non-null prefix overrides the mode.
*/
public record Ipv4ToIpv6Options(EmbedMode mode, String prefix) {
/** Default: the IPv4-mapped form with no custom prefix. */
public Ipv4ToIpv6Options {
if (mode == null) {
mode = EmbedMode.MAPPED;
}
}
public static Ipv4ToIpv6Options defaults() {
return new Ipv4ToIpv6Options(EmbedMode.MAPPED, null);
}
public static Ipv4ToIpv6Options compatible() {
return new Ipv4ToIpv6Options(EmbedMode.COMPATIBLE, null);
}
public static Ipv4ToIpv6Options withPrefix(String prefix) {
return new Ipv4ToIpv6Options(EmbedMode.MAPPED, prefix);
}
}
/**
* A valid IPv6 group token: 1-4 hex digits, no sign, no underscores.
* (Equivalent to the TS /^[0-9a-fA-F]{1,4}$/ regex.)
*/
private static boolean isHexGroup(String s) {
int len = s.length();
if (len < 1 || len > 4) {
return false;
}
for (int i = 0; i < len; i++) {
if (!isAsciiHexDigit(s.charAt(i))) {
return false;
}
}
return true;
}
private static boolean isAsciiHexDigit(char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
/**
* A valid IPv4 octet token: 1-3 decimal digits. Range is enforced
* separately. (Equivalent to the TS /^\d{1,3}$/ regex.)
*/
private static boolean isDec3(String s) {
int len = s.length();
if (len < 1 || len > 3) {
return false;
}
for (int i = 0; i < len; i++) {
char c = s.charAt(i);
if (c < '0' || c > '9') {
return false;
}
}
return true;
}
/** Count non-overlapping occurrences of "::" — used to enforce the
* at-most-one-compression rule without a regex. */
private static int countDoubleColon(String s) {
int count = 0;
int i = s.indexOf("::");
while (i >= 0) {
count++;
i = s.indexOf("::", i + 2);
}
return count;
}
/**
* Parse a dotted-decimal IPv4 string into four octets, validating each is
* 0-255. Returns Optional.empty() for anything that is not exactly four
* numeric octets in range.
*/
public static Optional<int[]> parseIpv4(String s) {
// split(".", -1) keeps trailing empty tokens, mirroring the Rust/Python
// split semantics this parser relies on.
String[] parts = s.trim().split("\\.", -1);
if (parts.length != 4) {
return Optional.empty();
}
int[] octets = new int[4];
for (int i = 0; i < 4; i++) {
if (!isDec3(parts[i])) {
return Optional.empty();
}
// isDec3 guarantees digits only, so the base-10 parse cannot fail.
int n = Integer.parseInt(parts[i]);
if (n > 255) {
return Optional.empty();
}
octets[i] = n;
}
return Optional.of(octets);
}
/**
* Render four octets as "a.b.c.d". (The parser already confines each
* octet to 0..255, but the signature keeps the symmetry with the other
* ports.)
*/
public static String ipv4ToString(int[] octets) {
return octets[0] + "." + octets[1] + "." + octets[2] + "." + octets[3];
}
/**
* 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 Optional.empty() on any malformed input — never throws.
*/
public static Optional<int[]> parseIpv6(String s) {
String input = s.trim();
if (input.isEmpty()) {
return Optional.empty();
}
// At most one "::" run is legal; reject ambiguous double-compression.
if (countDoubleColon(input) > 1) {
return Optional.empty();
}
// Branch on the position of "::" (if any). The two arms mirror each
// other: split into tokens, validate each, allow a dotted-quad only in
// the final slot, then assemble exactly eight groups.
int dc = input.indexOf("::");
if (dc >= 0) {
String before = input.substring(0, dc);
String after = input.substring(dc + 2);
String[] headTokens = before.isEmpty() ? new String[0] : before.split(":", -1);
String[] tailTokens = after.isEmpty() ? new String[0] : after.split(":", -1);
List<Integer> head = new ArrayList<>();
for (String g : headTokens) {
if (!isHexGroup(g)) {
return Optional.empty();
}
head.add(Integer.parseInt(g, 16));
}
List<Integer> tail = new ArrayList<>();
for (int i = 0; i < tailTokens.length; i++) {
String 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.indexOf('.') >= 0) {
int[] oct = parseIpv4(g).orElse(null);
if (oct == null) {
return Optional.empty();
}
tail.add((oct[0] << 8) | oct[1]);
tail.add((oct[2] << 8) | oct[3]);
} else {
if (!isHexGroup(g)) {
return Optional.empty();
}
tail.add(Integer.parseInt(g, 16));
}
}
int total = head.size() + tail.size();
if (total >= 8) {
return Optional.empty(); // "::" must elide at least one group.
}
int[] groups = new int[8];
for (int k = 0; k < head.size(); k++) {
groups[k] = head.get(k);
}
// The middle [head.size() .. 8 - tail.size()] stays zero — that is
// the elided run "::" stands in for.
for (int k = 0; k < tail.size(); k++) {
groups[8 - tail.size() + k] = tail.get(k);
}
return Optional.of(groups);
}
// No compression: split on ':' and parse, allowing a dotted-quad only
// in the last slot. The result must be exactly eight groups.
String[] tokens = input.split(":", -1);
int[] groups = new int[8];
int n = 0;
for (int i = 0; i < tokens.length; i++) {
String g = tokens[i];
if (i == tokens.length - 1 && g.indexOf('.') >= 0) {
int[] oct = parseIpv4(g).orElse(null);
if (oct == null) {
return Optional.empty();
}
if (n + 2 > 8) {
return Optional.empty();
}
groups[n] = (oct[0] << 8) | oct[1];
groups[n + 1] = (oct[2] << 8) | oct[3];
n += 2;
} else {
if (!isHexGroup(g)) {
return Optional.empty();
}
if (n + 1 > 8) {
return Optional.empty();
}
groups[n] = Integer.parseInt(g, 16);
n += 1;
}
}
if (n != 8) {
return Optional.empty();
}
return Optional.of(groups);
}
/** True when the eight groups form an IPv4-mapped ("::ffff:") address. */
private static boolean isMapped(int[] g) {
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. */
private static boolean isCompatible(int[] g) {
return g[0] == 0 && g[1] == 0 && g[2] == 0 && g[3] == 0 && g[4] == 0 && g[5] == 0;
}
/** Lowercase hex for one 16-bit group, with no leading zeros. */
private static String hexGroup(int v) {
return Integer.toHexString(v);
}
/** Same group rendered with a fixed 4-digit width: 0000..ffff. */
private static String hexGroupPadded(int v) {
String h = Integer.toHexString(v);
return "0".repeat(4 - h.length()) + h;
}
/** The slice [from, to) as colon-separated lowercase hex. */
private static String renderGroups(int[] groups, int from, int to) {
StringBuilder sb = new StringBuilder();
for (int i = from; i < to; i++) {
if (i > from) {
sb.append(':');
}
sb.append(hexGroup(groups[i]));
}
return sb.toString();
}
/**
* 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. Works over any group span (8 for a whole address, 6 for
* the high part of an embedded-IPv4 render). Does not emit dotted-decimal;
* call {@link #renderCanonical(int[])} for that.
*/
private static String compressGroups(int[] groups) {
int 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 (strict > keeps earliest).
for (int 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 renderGroups(groups, 0, groups.length);
}
return renderGroups(groups, 0, bestStart) + "::"
+ renderGroups(groups, bestStart + bestLen, groups.length);
}
/**
* 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".
*/
private static String renderWithEmbeddedTail(int[] high, int[] octets) {
String highStr = compressGroups(high);
String ipv4 = ipv4ToString(octets);
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 {@link #ipv6ToIpv4(String)}; on-demand compatible
* generation via {@link #ipv4ToIpv6(int[], Ipv4ToIpv6Options)} is
* untouched.
*/
private static String renderCanonical(int[] groups) {
if (isMapped(groups)) {
int[] octets = {
(groups[6] >> 8) & 0xFF, groups[6] & 0xFF,
(groups[7] >> 8) & 0xFF, groups[7] & 0xFF,
};
return renderWithEmbeddedTail(Arrays.copyOfRange(groups, 0, 6), octets);
}
return compressGroups(groups);
}
/** Render eight groups as canonical compressed IPv6. */
public static String ipv6ToString(int[] groups) {
return renderCanonical(groups);
}
/**
* Expand an IPv6 string to its full eight-group, four-hex-digit form;
* Optional.empty() if invalid.
*/
public static Optional<String> expandIpv6(String s) {
int[] g = parseIpv6(s).orElse(null);
if (g == null) {
return Optional.empty();
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 8; i++) {
if (i > 0) {
sb.append(':');
}
sb.append(hexGroupPadded(g[i]));
}
return Optional.of(sb.toString());
}
/** Compress an IPv6 string to its RFC 5952 canonical form;
* Optional.empty() if invalid. */
public static Optional<String> compressIpv6(String s) {
int[] g = parseIpv6(s).orElse(null);
if (g == null) {
return Optional.empty();
}
return Optional.of(renderCanonical(g));
}
/**
* Embed an IPv4 octet quad into an IPv6 address. By default produces the
* IPv4-mapped form "::ffff:a.b.c.d"; {@link EmbedMode#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 Optional.empty()
* for an invalid octet count or prefix.
*/
public static Optional<String> ipv4ToIpv6(int[] octets, Ipv4ToIpv6Options opts) {
if (octets.length != 4) {
return Optional.empty();
}
Ipv4ToIpv6Options o = opts == null ? Ipv4ToIpv6Options.defaults() : opts;
if (o.prefix() != null) {
int[] p = parseIpv6(o.prefix()).orElse(null);
if (p == null) {
return Optional.empty();
}
return Optional.of(renderWithEmbeddedTail(Arrays.copyOfRange(p, 0, 6), octets));
}
if (o.mode() == EmbedMode.COMPATIBLE) {
return Optional.of(renderWithEmbeddedTail(new int[] {0, 0, 0, 0, 0, 0}, octets));
}
return Optional.of(renderWithEmbeddedTail(new int[] {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
* Optional.empty() when the address carries no embedded IPv4 (or is
* unparseable).
*/
public static Optional<String> ipv6ToIpv4(String s) {
int[] g = parseIpv6(s).orElse(null);
if (g == null || !(isMapped(g) || isCompatible(g))) {
return Optional.empty();
}
int[] octets = {
(g[6] >> 8) & 0xFF, g[6] & 0xFF,
(g[7] >> 8) & 0xFF, g[7] & 0xFF,
};
return Optional.of(ipv4ToString(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 →