Skip to content

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 →