Test iOS IP Literal Parsing: Address Families and Normalization

Test iOS IP Literal Parsing: Address Families and Normalization

Test iOS IP Literal Parsing: Address Families and Normalization

You update the connection settings on Windows and hand the task over to a cloud Mac for validation, only to find that the same device’s IPv6 address is treated as two separate targets: one entry spells out every zero group, while another uses a double colon. If the connection list, deduplication logic and access rules each compare raw strings, their results can diverge. The following Swift checks use system parsing functions and run entirely offline. No live server or working network connection is required.

Define the input contract first

This example accepts only IP address literals—not domain names, ports or complete URLs. IPv4 must use four decimal components; IPv6 may use standard forms supported by the system. Leading and trailing whitespace is rejected rather than silently trimmed, so the value shown in the interface cannot differ from the value used for the connection.

Square brackets around IPv6 addresses belong to the URL host representation, not to the address itself. Scope identifiers such as %en0 are also outside this parser’s contract. If you need to connect to a link-local address, accept the address and interface information explicitly at another layer. Do not simply strip everything from the percent sign onward and proceed with the connection.

Successful parsing means only that the text represents an address. It does not mean access to that address is permitted or that the device is online.

The documentation addresses in the examples are used only for local parsing. The script does not connect to them.

Store the address family and bytes

Create ip-contract.swift in the project directory on your cloud Mac. First confirm that xcrun swiftc --version prints compiler information, then add the following code. It avoids interfaces that might resolve hostnames, keeping DNS behavior out of literal validation.

import Darwin
import Foundation

struct IPAddress: Equatable {
    let family: Int32
    let bytes: [UInt8]
}

func parseIP(_ text: String) -> IPAddress? {
    guard !text.isEmpty,
          text.utf8.count <= 45,
          !text.utf8.contains(0) else {
        return nil
    }

    var v4 = in_addr()
    let result4 = text.withCString {
        inet_pton(AF_INET, $0, &v4)
    }
    if result4 == 1 {
        return IPAddress(
            family: AF_INET,
            bytes: withUnsafeBytes(of: &v4) { Array($0) }
        )
    }

    var v6 = in6_addr()
    let result6 = text.withCString {
        inet_pton(AF_INET6, $0, &v6)
    }
    if result6 == 1 {
        return IPAddress(
            family: AF_INET6,
            bytes: withUnsafeBytes(of: &v6) { Array($0) }
        )
    }

    return nil
}

func require(_ condition: Bool, _ message: String) {
    if !condition {
        fatalError(message)
    }
}

let good = [
    "192.0.2.1",
    "2001:db8::1",
    "2001:0DB8:0000:0000:0000:0000:0000:0001",
    "::",
    "::1",
    "::ffff:192.0.2.1"
]

for text in good {
    require(parseIP(text) != nil, "Rejected valid literal")
}

let bad = [
    "",
    " 192.0.2.1",
    "192.0.2.1\n",
    "192.0.2.256",
    "192.0.2",
    "192.0.02.1",
    "0xC0000201",
    "[2001:db8::1]",
    "fe80::1%en0",
    "2001:::1",
    "192.0.2.1\u{0}ignored"
]

for text in bad {
    require(parseIP(text) == nil, "Accepted invalid literal")
}

let short = parseIP("2001:db8::1")!
let expanded = parseIP(
    "2001:0DB8:0000:0000:0000:0000:0000:0001"
)!
require(short == expanded, "Equivalent IPv6 mismatch")
require(short.bytes.count == 16, "Invalid IPv6 length")

let v4 = parseIP("192.0.2.1")!
let mapped = parseIP("::ffff:192.0.2.1")!
require(v4.bytes.count == 4, "Invalid IPv4 length")
require(v4 != mapped, "Address families collapsed")

The length limit is a byte budget for this parsing contract, not a universal limit for all addresses with scope identifiers. The address bytes come directly from the system structures and retain network byte order. Do not reinterpret the entire buffer as a native integer for comparison.

Compile, run and verify failure handling

Compile the script into a standalone executable so it can run as part of a build task without differences in launch methods affecting interpreted execution:

set -eu
mkdir -p .ip-check
xcrun swiftc ip-contract.swift -o .ip-check/ip-contract
.ip-check/ip-contract

A successful run produces no output and exits with code zero. Any contract violation terminates the program with a nonzero status. These checks are explicit rather than using assert, because optimization settings can remove assertions.

Temporarily change the equality comparison to inequality, confirm that the task actually fails, then restore it. This negative test can reveal pipelines that ignore exit codes or save logs while still reporting success. Do not stop at confirming that the script “ran once.”

Keep normalization separate from access policy

Do not broaden equivalence implicitly

Compressed and fully expanded representations of the same IPv6 address should compare equal. However, IPv4 and IPv4-mapped IPv6 remain distinct address families in this model. If your application needs to treat them as the same logical target, add an explicit conversion and test the access rules before and after conversion. Removing a string prefix is not a substitute.

For display, you can use inet_ntop to produce a consistent textual representation. Deduplication and authorization should continue to use the parsed model. Do not treat the string format emitted by one system version as a permanent, cross-version storage contract.

Review domain names separately

A domain name failing this function does not mean the name is invalid; it means the name is outside this input type. If the product also supports domain names, provide a separate input path and, after resolution, check the set of addresses the application is actually about to connect to. Checking only the host text entered by the user does not account for changes in resolution results.

Whether loopback, private, link-local and similar addresses are allowed depends on the product’s purpose. A device debugging tool and a service that fetches public internet resources need different rules. They should not share a vaguely defined “safe address” boolean.

Preserve failure cases as regression tests

Once the parser is in shared source code, run the same test vectors in both macOS command-line checks and an iOS test target. The former is useful for quickly catching errors in a remote environment; the latter verifies behavior on the target system. A passing run on the host cannot replace validation on the target platform.

For every fix, add the original input, expected address family and expected result. For embedded null bytes in particular, preserve the original bytes, since log output may hide them. C strings end at a null byte; without the input guard, validation could check only a valid prefix.

Finally, verify three things: different textual forms resolve to the same address where appropriate, address families remain distinct, and invalid input fails before a connection is attempted. With these contracts stable, you have a repeatable set of checks to support future interface validation or connection policies.

Frequently asked questions

Do different IPv6 strings always identify different addresses?

No. Zero compression, leading zeros and hexadecimal letter case can differ. Parse first, then compare the address family and network-order bytes.

Does successful inet_pton parsing mean a connection is safe to allow?

No. Loopback, private, link-local and mapped addresses still need explicit access rules. Addresses returned by DNS also require separate validation.

Hand off your tasks to a dedicated cloud Mac

Rent a dedicated physical Mac mini and choose a region and billing period that fit your workflow. Once it’s ready, find your connection details in the console. If you need help, submit a support ticket there.

Choose a cloud Mac plan

Choose a region that fits your workflow

Review the configuration and billing period, then provision your dedicated physical Mac mini.