Skip to content

IPv4 ↔ IPv6 Converter — Swift 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 Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.

// ip-converter — IPv4 ↔ IPv6 conversion (polyglot showcase: Swift)
//
// Language: Swift (5.9, standard library only — no Foundation needed)
// 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 nil (or nil for the string renderers) on invalid input
// rather than trapping, 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.
//
// The types stay precise the way the Rust port's do: octets are UInt8, IPv6
// 16-bit groups are UInt16. Everything below uses only the Swift standard
// library (split(omittingEmptySubsequences: false) keeps empty tokens, and
// firstRange(of:) powers the "::" scan), mirroring the hand-rolled Rust
// checks.

/// Embedding family for placing an IPv4 quad inside an IPv6 address.
enum EmbedMode {
    /// `::ffff:a.b.c.d` — the modern, non-deprecated IPv4-mapped form (default).
    case mapped
    /// `::a.b.c.d` — the deprecated IPv4-compatible form.
    case compatible
}

/// Options for embedding an IPv4 octet quad into an IPv6 address.
struct Ipv4ToIpv6Options {
    /// Embedding family. Ignored when `prefix` is non-`nil`.
    var mode: EmbedMode = .mapped
    /// 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`.
    var prefix: String? = nil
}

/// Pure IPv4/IPv6 address conversion logic, ported from src/lib/ip-converter.ts.
enum IpConverter {

    /// 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 func isHexGroup(_ s: Substring) -> Bool {
        (1...4).contains(s.count) && s.allSatisfy(\.isHexDigit)
    }

    /// A valid IPv4 octet token: 1-3 decimal digits. Range is enforced separately.
    /// (Equivalent to the TS /^\d{1,3}$/ regex.)
    static func isDec3(_ s: Substring) -> Bool {
        (1...3).contains(s.count) && s.allSatisfy { $0 >= "0" && $0 <= "9" }
    }

    /// Numeric value of a validated hex-group token (1-4 hex digits), or nil.
    static func hexValue(_ tok: Substring) -> UInt16? {
        guard isHexGroup(tok) else { return nil }
        return UInt16(tok, radix: 16)
    }

    /// Remove surrounding whitespace without Foundation (String's
    /// trimmingCharacters lives there). Mirrors str.strip() / str::trim().
    static func trim(_ s: String) -> String {
        var view = s[...]
        while let first = view.first, first.isWhitespace { view = view.dropFirst() }
        while let last = view.last, last.isWhitespace { view = view.dropLast() }
        return String(view)
    }

    /// Count non-overlapping occurrences of `needle` — used to enforce the
    /// at-most-one "::" rule. (Substring.firstRange(of:) is stdlib, Swift 5.7+.)
    static func countOccurrences(of needle: String, in haystack: String) -> Int {
        var count = 0
        var rest = haystack[...]
        while let found = rest.firstRange(of: needle) {
            count += 1
            rest = rest[found.upperBound...]
        }
        return count
    }

    /// Parse a dotted-decimal IPv4 string into four octets, validating each is
    /// 0-255. Returns nil for anything that is not exactly four numeric octets
    /// in range.
    static func parseIpv4(_ s: String) -> [UInt8]? {
        let input = trim(s)
        let parts = input.split(separator: ".", omittingEmptySubsequences: false)
        guard parts.count == 4 else { return nil }
        var octets: [UInt8] = []
        for p in parts {
            guard isDec3(p) else { return nil }
            // isDec3 guarantees ASCII digits only, and UInt8 bounds the value
            // to 0-255 by construction.
            guard let n = UInt8(p) else { return nil }
            octets.append(n)
        }
        return octets
    }

    /// Render four octets as `a.b.c.d`. (The octet type is UInt8, which
    /// already guarantees range, but the signature keeps the symmetry with the
    /// other ports.)
    static func ipv4ToString(_ octets: [UInt8]) -> String {
        octets.map { String($0) }.joined(separator: ".")
    }

    /// 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 nil on any malformed input — never traps.
    static func parseIpv6(_ s: String) -> [UInt16]? {
        let input = trim(s)
        guard !input.isEmpty else { return nil }
        // At most one "::" run is legal; reject ambiguous double-compression.
        guard countOccurrences(of: "::", in: input) <= 1 else { return nil }

        // 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.
        if let dc = input.firstRange(of: "::") {
            let before = input[..<dc.lowerBound]
            let after = input[dc.upperBound...]
            let headTokens = before.isEmpty ? [] : before.split(separator: ":", omittingEmptySubsequences: false)
            let tailTokens = after.isEmpty ? [] : after.split(separator: ":", omittingEmptySubsequences: false)

            var head: [UInt16] = []
            for g in headTokens {
                guard let v = hexValue(g) else { return nil }
                head.append(v)
            }

            var tail: [UInt16] = []
            for (i, g) in tailTokens.enumerated() {
                // 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.count - 1 && g.contains(".") {
                    guard let oct = parseIpv4(String(g)) else { return nil }
                    tail.append((UInt16(oct[0]) << 8) | UInt16(oct[1]))
                    tail.append((UInt16(oct[2]) << 8) | UInt16(oct[3]))
                } else {
                    guard let v = hexValue(g) else { return nil }
                    tail.append(v)
                }
            }

            let total = head.count + tail.count
            // "::" must elide at least one group.
            guard total < 8 else { return nil }
            var groups = [UInt16](repeating: 0, count: 8)
            for (k, v) in head.enumerated() { groups[k] = v }
            // The middle [head.count ..< 8 - tail.count] stays zero — that is
            // the elided run "::" stands in for.
            for (k, v) in tail.enumerated() { groups[8 - tail.count + k] = v }
            return groups
        }

        // No compression: split on ':' and parse, allowing a dotted-quad only
        // in the last slot. The result must be exactly eight groups.
        let tokens = input.split(separator: ":", omittingEmptySubsequences: false)
        var groups: [UInt16] = []
        for (i, g) in tokens.enumerated() {
            if i == tokens.count - 1 && g.contains(".") {
                guard let oct = parseIpv4(String(g)) else { return nil }
                groups.append((UInt16(oct[0]) << 8) | UInt16(oct[1]))
                groups.append((UInt16(oct[2]) << 8) | UInt16(oct[3]))
            } else {
                guard let v = hexValue(g) else { return nil }
                groups.append(v)
            }
        }
        return groups.count == 8 ? groups : nil
    }

    /// True when the eight groups form an IPv4-mapped ("::ffff:") address.
    static func isMapped(_ g: [UInt16]) -> Bool {
        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 func isCompatible(_ g: [UInt16]) -> Bool {
        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.
    static func hexGroup(_ v: UInt16) -> String {
        String(v, radix: 16, uppercase: false)
    }

    /// Same group rendered with a fixed 4-digit width: 0000..ffff.
    static func hexGroupPadded(_ v: UInt16) -> String {
        let h = hexGroup(v)
        return String(repeating: "0", count: 4 - h.count) + h
    }

    /// 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 array (8 for a whole address, 6 for
    /// the high part of an embedded-IPv4 render). Does not emit dotted-decimal;
    /// call `renderCanonical(_:)` for that.
    static func compressGroups(_ groups: [UInt16]) -> String {
        var bestStart = -1
        var bestLen = 0
        var curStart = -1
        var curLen = 0
        // Track the longest run of consecutive zero groups. bestStart records
        // the first run of the longest length (strict > keeps earliest).
        for (i, v) in groups.enumerated() {
            if v == 0 {
                if curStart < 0 { curStart = i }
                curLen += 1
                if curLen > bestLen {
                    bestLen = curLen
                    bestStart = curStart
                }
            } else {
                curStart = -1
                curLen = 0
            }
        }

        func render(_ slice: ArraySlice<UInt16>) -> String {
            slice.map { hexGroup($0) }.joined(separator: ":")
        }

        if bestLen < 2 {
            return render(groups[...])
        }
        let start = groups.index(groups.startIndex, offsetBy: bestStart)
        let end = groups.index(groups.startIndex, offsetBy: bestStart + bestLen)
        return render(groups[..<start]) + "::" + render(groups[end...])
    }

    /// 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 func renderWithEmbeddedTail(_ high: [UInt16], _ octets: [UInt8]) -> String {
        let highStr = compressGroups(high)
        let ipv4 = ipv4ToString(octets)
        return highStr.hasSuffix("::") ? 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(_:options:)` is untouched.
    static func renderCanonical(_ groups: [UInt16]) -> String {
        guard isMapped(groups) else {
            return compressGroups(groups)
        }
        let octets: [UInt8] = [
            UInt8(groups[6] >> 8), UInt8(groups[6] & 0xFF),
            UInt8(groups[7] >> 8), UInt8(groups[7] & 0xFF),
        ]
        return renderWithEmbeddedTail(Array(groups[0..<6]), octets)
    }

    /// Render eight groups as canonical compressed IPv6.
    static func ipv6ToString(_ groups: [UInt16]) -> String {
        renderCanonical(groups)
    }

    /// Expand an IPv6 string to its full eight-group, four-hex-digit form;
    /// nil if invalid.
    static func expandIpv6(_ s: String) -> String? {
        guard let g = parseIpv6(s) else { return nil }
        return g.map { hexGroupPadded($0) }.joined(separator: ":")
    }

    /// Compress an IPv6 string to its RFC 5952 canonical form; nil if invalid.
    static func compressIpv6(_ s: String) -> String? {
        guard let g = parseIpv6(s) else { return nil }
        return renderCanonical(g)
    }

    /// Embed an IPv4 octet quad into an IPv6 address. By default produces the
    /// IPv4-mapped form "::ffff:a.b.c.d"; `.compatible` yields "::a.b.c.d"; a
    /// set `options.prefix` overrides both and places the IPv4 after any
    /// custom /96 prefix (e.g. "64:ff9b::a.b.c.d"). Returns nil for an invalid
    /// octet count or prefix.
    static func ipv4ToIpv6(
        _ octets: [UInt8],
        options: Ipv4ToIpv6Options = Ipv4ToIpv6Options()
    ) -> String? {
        guard octets.count == 4 else { return nil }

        if let prefix = options.prefix {
            guard let p = parseIpv6(prefix) else { return nil }
            return renderWithEmbeddedTail(Array(p[0..<6]), octets)
        }
        switch options.mode {
        case .compatible:
            return renderWithEmbeddedTail([0, 0, 0, 0, 0, 0], octets)
        case .mapped:
            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 nil
    /// when the address carries no embedded IPv4 (or is unparseable).
    static func ipv6ToIpv4(_ s: String) -> String? {
        guard let g = parseIpv6(s), isMapped(g) || isCompatible(g) else { return nil }
        let octets: [UInt8] = [
            UInt8(g[6] >> 8), UInt8(g[6] & 0xFF),
            UInt8(g[7] >> 8), UInt8(g[7] & 0xFF),
        ]
        return 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 →