680845cb1c
Linux build / Determine Swift version (push) Waiting to run
Linux build / Linux compile check (push) Blocked by required conditions
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
271 lines
9.2 KiB
Swift
271 lines
9.2 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// 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)
|
|
}
|
|
}
|