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 →