//===----------------------------------------------------------------------===// // Copyright © 2025-2026 Apple Inc. and the Containerization project authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// /// Represents an IPv6 network address conforming to RFC 5952 and RFC 4291. public struct IPv6Address: Sendable, Hashable, CustomStringConvertible, Equatable, Comparable { public let value: UInt128 public let zone: String? /// Creates an IPv6Address by parsing a string representation. /// /// Supports standard IPv6 formats including compressed notation (::), mixed IPv4 notation, and zone identifiers. /// /// - Parameter address: String representation of an IPv6 address /// - Throws: `AddressError` if the string is not a valid IPv6 address public init(_ address: String) throws { self = try Self.parse(address) } /// Creates an IPv6Address from 16 bytes. /// /// - Parameters: /// - bytes: 16-byte array in network byte order representing the IPv6 address /// - zone: Optional zone identifier (e.g., "eth0") /// - Throws: `AddressError.unableToParse` if the byte array length is not 16 @inlinable public init(_ bytes: [UInt8], zone: String? = nil) throws { guard bytes.count == 16 else { throw AddressError.unableToParse } // Build UInt128 value in chunks to avoid compiler complexity let hh = (UInt128(bytes[0]) << 120) | (UInt128(bytes[1]) << 112) | (UInt128(bytes[2]) << 104) | (UInt128(bytes[3]) << 96) let hl = (UInt128(bytes[4]) << 88) | (UInt128(bytes[5]) << 80) | (UInt128(bytes[6]) << 72) | (UInt128(bytes[7]) << 64) let lh = (UInt128(bytes[8]) << 56) | (UInt128(bytes[9]) << 48) | (UInt128(bytes[10]) << 40) | (UInt128(bytes[11]) << 32) let ll = (UInt128(bytes[12]) << 24) | (UInt128(bytes[13]) << 16) | (UInt128(bytes[14]) << 8) | UInt128(bytes[15]) self.value = hh | hl | lh | ll self.zone = zone } @inlinable public init(_ value: UInt128, zone: String? = nil) { self.value = value self.zone = zone } /// Canonical string representation following RFC 5952. public var description: String { // Convert UInt128 value to 16-bit groups let groups: [UInt16] = [ UInt16((value >> 112) & 0xFFFF), UInt16((value >> 96) & 0xFFFF), UInt16((value >> 80) & 0xFFFF), UInt16((value >> 64) & 0xFFFF), UInt16((value >> 48) & 0xFFFF), UInt16((value >> 32) & 0xFFFF), UInt16((value >> 16) & 0xFFFF), UInt16(value & 0xFFFF), ] // Find the longest run of consecutive zeros for :: compression var longestZeroStart = -1 var longestZeroLength = 0 var currentZeroStart = -1 var currentZeroLength = 0 for (index, group) in groups.enumerated() { if group == 0 { if currentZeroStart == -1 { currentZeroStart = index currentZeroLength = 1 } else { currentZeroLength += 1 } } else { if currentZeroLength > longestZeroLength { longestZeroStart = currentZeroStart longestZeroLength = currentZeroLength } currentZeroStart = -1 currentZeroLength = 0 } } if currentZeroLength > longestZeroLength { longestZeroStart = currentZeroStart longestZeroLength = currentZeroLength } let useCompression = longestZeroLength >= 2 var result = "" var index = 0 while index < 8 { if useCompression && index == longestZeroStart { if index == 0 { result += "::" } else { result += ":" } // Skip the compressed zeros index += longestZeroLength // If we compressed to the end, we're done if index >= 8 { break } } else { // Add the group in lowercase hex without leading zeros result += String(groups[index], radix: 16, uppercase: false) index += 1 // Add colon if not at the end if index < 8 { result += ":" } } } if let zone = zone { result += "%" + zone } return result } @inlinable public var bytes: [UInt8] { Self.bytes(self.value) } @usableFromInline internal static func bytes(_ value: UInt128) -> [UInt8] { var result = [UInt8](repeating: 0, count: 16) result[0] = UInt8((value >> 120) & 0xff) result[1] = UInt8((value >> 112) & 0xff) result[2] = UInt8((value >> 104) & 0xff) result[3] = UInt8((value >> 96) & 0xff) result[4] = UInt8((value >> 88) & 0xff) result[5] = UInt8((value >> 80) & 0xff) result[6] = UInt8((value >> 72) & 0xff) result[7] = UInt8((value >> 64) & 0xff) result[8] = UInt8((value >> 56) & 0xff) result[9] = UInt8((value >> 48) & 0xff) result[10] = UInt8((value >> 40) & 0xff) result[11] = UInt8((value >> 32) & 0xff) result[12] = UInt8((value >> 24) & 0xff) result[13] = UInt8((value >> 16) & 0xff) result[14] = UInt8((value >> 8) & 0xff) result[15] = UInt8(value & 0xff) return result } @available(macOS 26.0, *) @usableFromInline internal static func bytes(_ value: UInt128) -> InlineArray<16, UInt8> { let result: InlineArray<16, UInt8> = [ UInt8((value >> 120) & 0xff), UInt8((value >> 112) & 0xff), UInt8((value >> 104) & 0xff), UInt8((value >> 96) & 0xff), UInt8((value >> 88) & 0xff), UInt8((value >> 80) & 0xff), UInt8((value >> 72) & 0xff), UInt8((value >> 64) & 0xff), UInt8((value >> 56) & 0xff), UInt8((value >> 48) & 0xff), UInt8((value >> 40) & 0xff), UInt8((value >> 32) & 0xff), UInt8((value >> 24) & 0xff), UInt8((value >> 16) & 0xff), UInt8((value >> 8) & 0xff), UInt8(value & 0xff), ] return result } /// The unspecified IPv6 address (::) public static let unspecified = IPv6Address(0) /// The loopback IPv6 address (::1) public static let loopback = IPv6Address(1) // MARK: - Address Classification Methods /// Returns `true` if this is the unspecified address (::). @inlinable public var isUnspecified: Bool { value == 0 } /// Returns `true` if this is the loopback address (::1). @inlinable public var isLoopback: Bool { value == 1 } /// Returns `true` if this is a multicast address (ff00::/8). @inlinable public var isMulticast: Bool { (value >> 120) == 0xFF } /// Returns `true` if this is a link-local unicast address (fe80::/10). @inlinable public var isLinkLocal: Bool { (value >> 118) == 0x3FA // fe80::/10 = top 10 bits are 1111111010 } /// Returns `true` if this is a unique local address (fc00::/7). @inlinable public var isUniqueLocal: Bool { (value >> 121) == 0x7E // fc00::/7 = top 7 bits are 1111110 } /// Returns `true` if this is a global unicast address. @inlinable public var isGlobalUnicast: Bool { !isUnspecified && !isLoopback && !isMulticast && !isLinkLocal && !isUniqueLocal } /// Returns `true` if this is a documentation address (2001:db8::/32). @inlinable public var isDocumentation: Bool { (value >> 96) == 0x2001_0DB8 // 2001:db8::/32 } /// Compares two IPv6 addresses numerically, then by zone if values are equal. @inlinable public static func < (lhs: IPv6Address, rhs: IPv6Address) -> Bool { if lhs.value != rhs.value { return lhs.value < rhs.value } // Same value, compare zones lexicographically return (lhs.zone ?? "") < (rhs.zone ?? "") } } extension IPv6Address: Codable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let string = try container.decode(String.self) try self.init(string) } public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(description) } }