mirror of
https://github.com/apple/container.git
synced 2026-11-08 06:53:14 +00:00
Use typesafe IP/CIDR parameters everywhere. (#448)
- Closes #445. - Adopts refined IPv4 and IPv6 types developed by Agam Dua <agamdua@users.noreply.github.com>. - For type safety and clarity, use IP and CIDR types where we were previously using String. Co-authored-by: Agam Dua <agamdua@users.noreply.github.com>
This commit is contained in:
1 parent
68694942ec
commit
9ba8267afb
36 files changed
+3687
-666
No files matched your search
@@ -49,19 +49,19 @@ public struct ContainerManager: Sendable {
|
||||
nonisolated(unsafe) private let reference: vmnet_network_ref
|
||||
|
||||
/// The IPv4 subnet of this network.
|
||||
public let subnet: CIDRAddress
|
||||
public let subnet: CIDRv4
|
||||
|
||||
/// The gateway address of this network.
|
||||
public var gateway: IPv4Address {
|
||||
/// The IPv4 gateway address of this network.
|
||||
public var ipv4Gateway: IPv4Address {
|
||||
subnet.gateway
|
||||
}
|
||||
|
||||
struct Allocator: Sendable {
|
||||
private let addressAllocator: any AddressAllocator<UInt32>
|
||||
private let cidr: CIDRAddress
|
||||
private let cidr: CIDRv4
|
||||
private var allocations: [String: UInt32]
|
||||
|
||||
init(cidr: CIDRAddress) throws {
|
||||
init(cidr: CIDRv4) throws {
|
||||
self.cidr = cidr
|
||||
self.allocations = .init()
|
||||
let size = Int(cidr.upper.value - cidr.lower.value - 3)
|
||||
@@ -71,14 +71,14 @@ public struct ContainerManager: Sendable {
|
||||
)
|
||||
}
|
||||
|
||||
mutating func allocate(_ id: String) throws -> String {
|
||||
mutating func allocate(_ id: String) throws -> CIDRv4 {
|
||||
if allocations[id] != nil {
|
||||
throw ContainerizationError(.exists, message: "allocation with id \(id) already exists")
|
||||
}
|
||||
let index = try addressAllocator.allocate()
|
||||
allocations[id] = index
|
||||
let ip = IPv4Address(fromValue: index)
|
||||
return try CIDRAddress(ip, prefixLength: cidr.prefixLength).description
|
||||
let ip = IPv4Address(index)
|
||||
return try CIDRv4(ip, prefix: cidr.prefix)
|
||||
}
|
||||
|
||||
mutating func release(_ id: String) throws {
|
||||
@@ -91,8 +91,8 @@ public struct ContainerManager: Sendable {
|
||||
|
||||
/// A network interface supporting the vmnet_network_ref.
|
||||
public struct Interface: Containerization.Interface, VZInterface, Sendable {
|
||||
public let address: String
|
||||
public let gateway: String?
|
||||
public let ipv4Address: CIDRv4
|
||||
public let ipv4Gateway: IPv4Address?
|
||||
public let macAddress: String?
|
||||
|
||||
// `reference` isn't used concurrently.
|
||||
@@ -100,12 +100,12 @@ public struct ContainerManager: Sendable {
|
||||
|
||||
public init(
|
||||
reference: vmnet_network_ref,
|
||||
address: String,
|
||||
gateway: String,
|
||||
ipv4Address: CIDRv4,
|
||||
ipv4Gateway: IPv4Address,
|
||||
macAddress: String? = nil
|
||||
) {
|
||||
self.address = address
|
||||
self.gateway = gateway
|
||||
self.ipv4Address = ipv4Address
|
||||
self.ipv4Gateway = ipv4Gateway
|
||||
self.macAddress = macAddress
|
||||
self.reference = reference
|
||||
}
|
||||
@@ -126,7 +126,7 @@ public struct ContainerManager: Sendable {
|
||||
|
||||
/// Creates a new network.
|
||||
/// - Parameter subnet: The subnet to use for this network.
|
||||
public init(subnet: String? = nil) throws {
|
||||
public init(subnet: CIDRv4? = nil) throws {
|
||||
var status: vmnet_return_t = .VMNET_FAILURE
|
||||
guard let config = vmnet_network_configuration_create(.VMNET_SHARED_MODE, &status) else {
|
||||
throw ContainerizationError(.unsupported, message: "failed to create vmnet config with status \(status)")
|
||||
@@ -135,7 +135,7 @@ public struct ContainerManager: Sendable {
|
||||
vmnet_network_configuration_disable_dhcp(config)
|
||||
|
||||
if let subnet {
|
||||
try Self.configureSubnet(config, subnet: try CIDRAddress(subnet))
|
||||
try Self.configureSubnet(config, subnet: subnet)
|
||||
}
|
||||
|
||||
guard let ref = vmnet_network_create(config, &status), status == .VMNET_SUCCESS else {
|
||||
@@ -152,11 +152,11 @@ public struct ContainerManager: Sendable {
|
||||
/// Returns a new interface for use with a container.
|
||||
/// - Parameter id: The container ID.
|
||||
public mutating func create(_ id: String) throws -> Containerization.Interface? {
|
||||
let address = try allocator.allocate(id)
|
||||
let ipv4Address = try allocator.allocate(id)
|
||||
return Self.Interface(
|
||||
reference: self.reference,
|
||||
address: address,
|
||||
gateway: self.gateway.description,
|
||||
ipv4Address: ipv4Address,
|
||||
ipv4Gateway: self.ipv4Gateway,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ public struct ContainerManager: Sendable {
|
||||
try allocator.release(id)
|
||||
}
|
||||
|
||||
private static func getSubnet(_ ref: vmnet_network_ref) throws -> CIDRAddress {
|
||||
private static func getSubnet(_ ref: vmnet_network_ref) throws -> CIDRv4 {
|
||||
var subnet = in_addr()
|
||||
var mask = in_addr()
|
||||
vmnet_network_get_ipv4_subnet(ref, &subnet, &mask)
|
||||
@@ -174,19 +174,19 @@ public struct ContainerManager: Sendable {
|
||||
let sa = UInt32(bigEndian: subnet.s_addr)
|
||||
let mv = UInt32(bigEndian: mask.s_addr)
|
||||
|
||||
let lower = IPv4Address(fromValue: sa & mv)
|
||||
let upper = IPv4Address(fromValue: lower.value + ~mv)
|
||||
let lower = IPv4Address(sa & mv)
|
||||
let upper = IPv4Address(lower.value + ~mv)
|
||||
|
||||
return try CIDRAddress(lower: lower, upper: upper)
|
||||
return try CIDRv4(lower: lower, upper: upper)
|
||||
}
|
||||
|
||||
private static func configureSubnet(_ config: vmnet_network_configuration_ref, subnet: CIDRAddress) throws {
|
||||
private static func configureSubnet(_ config: vmnet_network_configuration_ref, subnet: CIDRv4) throws {
|
||||
let gateway = subnet.gateway
|
||||
|
||||
var ga = in_addr()
|
||||
inet_pton(AF_INET, gateway.description, &ga)
|
||||
|
||||
let mask = IPv4Address(fromValue: subnet.prefixLength.prefixMask32)
|
||||
let mask = IPv4Address(subnet.prefix.prefixMask32)
|
||||
var ma = in_addr()
|
||||
inet_pton(AF_INET, mask.description, &ma)
|
||||
|
||||
@@ -415,7 +415,8 @@ public struct ContainerManager: Sendable {
|
||||
}
|
||||
if let interface = try self.network?.create(id) {
|
||||
config.interfaces = [interface]
|
||||
config.dns = .init(nameservers: [interface.gateway!])
|
||||
// FIXME: throw instead of crash here if we can't unwrap?
|
||||
config.dns = .init(nameservers: [interface.ipv4Gateway!.description])
|
||||
}
|
||||
config.bootLog = BootLog.file(path: self.containerRoot.appendingPathComponent(id).appendingPathComponent("bootlog.log"))
|
||||
try configuration(&config)
|
||||
@@ -454,10 +455,10 @@ public struct ContainerManager: Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
extension CIDRAddress {
|
||||
extension CIDRv4 {
|
||||
/// The gateway address of the network.
|
||||
public var gateway: IPv4Address {
|
||||
IPv4Address(fromValue: self.lower.value + 1)
|
||||
IPv4Address(self.lower.value + 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,14 +14,16 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import ContainerizationExtras
|
||||
|
||||
/// A network interface.
|
||||
public protocol Interface: Sendable {
|
||||
/// The interface IPv4 address and subnet prefix length, as a CIDR address.
|
||||
/// Example: `192.168.64.3/24`
|
||||
var address: String { get }
|
||||
var ipv4Address: CIDRv4 { get }
|
||||
|
||||
/// The IP address for the default route, or nil for no default route.
|
||||
var gateway: String? { get }
|
||||
var ipv4Gateway: IPv4Address? { get }
|
||||
|
||||
/// The interface MAC address, or nil to auto-configure the address.
|
||||
var macAddress: String? { get }
|
||||
|
||||
@@ -434,10 +434,10 @@ extension LinuxContainer {
|
||||
// 3. If a gateway IP address is present, add the default route.
|
||||
for (index, i) in self.interfaces.enumerated() {
|
||||
let name = "eth\(index)"
|
||||
try await agent.addressAdd(name: name, address: i.address)
|
||||
try await agent.addressAdd(name: name, ipv4Address: i.ipv4Address)
|
||||
try await agent.up(name: name, mtu: 1280)
|
||||
if let gateway = i.gateway {
|
||||
try await agent.routeAddDefault(name: name, gateway: gateway)
|
||||
if let ipv4Gateway = i.ipv4Gateway {
|
||||
try await agent.routeAddDefault(name: name, ipv4Gateway: ipv4Gateway)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -393,10 +393,10 @@ extension LinuxPod {
|
||||
// 3. If a gateway IP address is present, add the default route.
|
||||
for (index, i) in self.interfaces.enumerated() {
|
||||
let name = "eth\(index)"
|
||||
try await agent.addressAdd(name: name, address: i.address)
|
||||
try await agent.addressAdd(name: name, ipv4Address: i.ipv4Address)
|
||||
try await agent.up(name: name, mtu: 1280)
|
||||
if let gateway = i.gateway {
|
||||
try await agent.routeAddDefault(name: name, gateway: gateway)
|
||||
if let ipv4Gateway = i.ipv4Gateway {
|
||||
try await agent.routeAddDefault(name: name, ipv4Gateway: ipv4Gateway)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,14 +14,16 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import ContainerizationExtras
|
||||
|
||||
public struct NATInterface: Interface {
|
||||
public var address: String
|
||||
public var gateway: String?
|
||||
public var ipv4Address: CIDRv4
|
||||
public var ipv4Gateway: IPv4Address?
|
||||
public var macAddress: String?
|
||||
|
||||
public init(address: String, gateway: String?, macAddress: String? = nil) {
|
||||
self.address = address
|
||||
self.gateway = gateway
|
||||
public init(ipv4Address: CIDRv4, ipv4Gateway: IPv4Address?, macAddress: String? = nil) {
|
||||
self.ipv4Address = ipv4Address
|
||||
self.ipv4Gateway = ipv4Gateway
|
||||
self.macAddress = macAddress
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
import vmnet
|
||||
import Virtualization
|
||||
import ContainerizationError
|
||||
import ContainerizationExtras
|
||||
import Foundation
|
||||
import Synchronization
|
||||
|
||||
@@ -26,8 +27,8 @@ import Synchronization
|
||||
/// container/virtual machine.
|
||||
@available(macOS 26, *)
|
||||
public final class NATNetworkInterface: Interface, Sendable {
|
||||
public let address: String
|
||||
public let gateway: String?
|
||||
public let ipv4Address: CIDRv4
|
||||
public let ipv4Gateway: IPv4Address?
|
||||
public let macAddress: String?
|
||||
|
||||
@available(macOS 26, *)
|
||||
@@ -36,25 +37,25 @@ public final class NATNetworkInterface: Interface, Sendable {
|
||||
|
||||
@available(macOS 26, *)
|
||||
public init(
|
||||
address: String,
|
||||
gateway: String?,
|
||||
ipv4Address: CIDRv4,
|
||||
ipv4Gateway: IPv4Address?,
|
||||
reference: sending vmnet_network_ref,
|
||||
macAddress: String? = nil
|
||||
) {
|
||||
self.address = address
|
||||
self.gateway = gateway
|
||||
self.ipv4Address = ipv4Address
|
||||
self.ipv4Gateway = ipv4Gateway
|
||||
self.macAddress = macAddress
|
||||
self.reference = reference
|
||||
}
|
||||
|
||||
@available(macOS, obsoleted: 26, message: "Use init(address:gateway:reference:macAddress:) instead")
|
||||
@available(macOS, obsoleted: 26, message: "Use init(ipv4Address:ipv4Gateway:reference:macAddress:) instead")
|
||||
public init(
|
||||
address: String,
|
||||
gateway: String?,
|
||||
ipv4Address: CIDRv4,
|
||||
ipv4Gateway: IPv4Address?,
|
||||
macAddress: String? = nil
|
||||
) {
|
||||
self.address = address
|
||||
self.gateway = gateway
|
||||
self.ipv4Address = ipv4Address
|
||||
self.ipv4Gateway = ipv4Gateway
|
||||
self.macAddress = macAddress
|
||||
self.reference = nil
|
||||
}
|
||||
|
||||
@@ -798,7 +798,7 @@ public struct Com_Apple_Containerization_Sandbox_V3_IpAddrAddRequest: Sendable {
|
||||
|
||||
public var interface: String = String()
|
||||
|
||||
public var address: String = String()
|
||||
public var ipv4Address: String = String()
|
||||
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
@@ -822,9 +822,9 @@ public struct Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkRequest: Senda
|
||||
|
||||
public var interface: String = String()
|
||||
|
||||
public var address: String = String()
|
||||
public var dstIpv4Addr: String = String()
|
||||
|
||||
public var srcAddr: String = String()
|
||||
public var srcIpv4Addr: String = String()
|
||||
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
@@ -848,7 +848,7 @@ public struct Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRequest: Se
|
||||
|
||||
public var interface: String = String()
|
||||
|
||||
public var gateway: String = String()
|
||||
public var ipv4Gateway: String = String()
|
||||
|
||||
public var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
@@ -2636,7 +2636,7 @@ extension Com_Apple_Containerization_Sandbox_V3_IpAddrAddRequest: SwiftProtobuf.
|
||||
public static let protoMessageName: String = _protobuf_package + ".IpAddrAddRequest"
|
||||
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "interface"),
|
||||
2: .same(proto: "address"),
|
||||
2: .same(proto: "ipv4Address"),
|
||||
]
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
@@ -2646,7 +2646,7 @@ extension Com_Apple_Containerization_Sandbox_V3_IpAddrAddRequest: SwiftProtobuf.
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularStringField(value: &self.interface) }()
|
||||
case 2: try { try decoder.decodeSingularStringField(value: &self.address) }()
|
||||
case 2: try { try decoder.decodeSingularStringField(value: &self.ipv4Address) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
@@ -2656,15 +2656,15 @@ extension Com_Apple_Containerization_Sandbox_V3_IpAddrAddRequest: SwiftProtobuf.
|
||||
if !self.interface.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.interface, fieldNumber: 1)
|
||||
}
|
||||
if !self.address.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.address, fieldNumber: 2)
|
||||
if !self.ipv4Address.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.ipv4Address, fieldNumber: 2)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_IpAddrAddRequest, rhs: Com_Apple_Containerization_Sandbox_V3_IpAddrAddRequest) -> Bool {
|
||||
if lhs.interface != rhs.interface {return false}
|
||||
if lhs.address != rhs.address {return false}
|
||||
if lhs.ipv4Address != rhs.ipv4Address {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
@@ -2693,8 +2693,8 @@ extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkRequest: SwiftProt
|
||||
public static let protoMessageName: String = _protobuf_package + ".IpRouteAddLinkRequest"
|
||||
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "interface"),
|
||||
2: .same(proto: "address"),
|
||||
3: .same(proto: "srcAddr"),
|
||||
2: .same(proto: "dstIpv4Addr"),
|
||||
3: .same(proto: "srcIpv4Addr"),
|
||||
]
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
@@ -2704,8 +2704,8 @@ extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkRequest: SwiftProt
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularStringField(value: &self.interface) }()
|
||||
case 2: try { try decoder.decodeSingularStringField(value: &self.address) }()
|
||||
case 3: try { try decoder.decodeSingularStringField(value: &self.srcAddr) }()
|
||||
case 2: try { try decoder.decodeSingularStringField(value: &self.dstIpv4Addr) }()
|
||||
case 3: try { try decoder.decodeSingularStringField(value: &self.srcIpv4Addr) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
@@ -2715,19 +2715,19 @@ extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkRequest: SwiftProt
|
||||
if !self.interface.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.interface, fieldNumber: 1)
|
||||
}
|
||||
if !self.address.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.address, fieldNumber: 2)
|
||||
if !self.dstIpv4Addr.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.dstIpv4Addr, fieldNumber: 2)
|
||||
}
|
||||
if !self.srcAddr.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.srcAddr, fieldNumber: 3)
|
||||
if !self.srcIpv4Addr.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.srcIpv4Addr, fieldNumber: 3)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkRequest, rhs: Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkRequest) -> Bool {
|
||||
if lhs.interface != rhs.interface {return false}
|
||||
if lhs.address != rhs.address {return false}
|
||||
if lhs.srcAddr != rhs.srcAddr {return false}
|
||||
if lhs.dstIpv4Addr != rhs.dstIpv4Addr {return false}
|
||||
if lhs.srcIpv4Addr != rhs.srcIpv4Addr {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
@@ -2756,7 +2756,7 @@ extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRequest: SwiftP
|
||||
public static let protoMessageName: String = _protobuf_package + ".IpRouteAddDefaultRequest"
|
||||
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "interface"),
|
||||
2: .same(proto: "gateway"),
|
||||
2: .same(proto: "ipv4Gateway"),
|
||||
]
|
||||
|
||||
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
@@ -2766,7 +2766,7 @@ extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRequest: SwiftP
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularStringField(value: &self.interface) }()
|
||||
case 2: try { try decoder.decodeSingularStringField(value: &self.gateway) }()
|
||||
case 2: try { try decoder.decodeSingularStringField(value: &self.ipv4Gateway) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
@@ -2776,15 +2776,15 @@ extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRequest: SwiftP
|
||||
if !self.interface.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.interface, fieldNumber: 1)
|
||||
}
|
||||
if !self.gateway.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.gateway, fieldNumber: 2)
|
||||
if !self.ipv4Gateway.isEmpty {
|
||||
try visitor.visitSingularStringField(value: self.ipv4Gateway, fieldNumber: 2)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRequest, rhs: Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRequest) -> Bool {
|
||||
if lhs.interface != rhs.interface {return false}
|
||||
if lhs.gateway != rhs.gateway {return false}
|
||||
if lhs.ipv4Gateway != rhs.ipv4Gateway {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -235,22 +235,22 @@ message IpLinkSetResponse {}
|
||||
|
||||
message IpAddrAddRequest {
|
||||
string interface = 1;
|
||||
string address = 2;
|
||||
string ipv4Address = 2;
|
||||
}
|
||||
|
||||
message IpAddrAddResponse {}
|
||||
|
||||
message IpRouteAddLinkRequest {
|
||||
string interface = 1;
|
||||
string address = 2;
|
||||
string srcAddr = 3;
|
||||
string dstIpv4Addr = 2;
|
||||
string srcIpv4Addr = 3;
|
||||
}
|
||||
|
||||
message IpRouteAddLinkResponse {}
|
||||
|
||||
message IpRouteAddDefaultRequest {
|
||||
string interface = 1;
|
||||
string gateway = 2;
|
||||
string ipv4Gateway = 2;
|
||||
}
|
||||
|
||||
message IpRouteAddDefaultResponse {}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import ContainerizationError
|
||||
import ContainerizationExtras
|
||||
import ContainerizationOCI
|
||||
import Foundation
|
||||
|
||||
@@ -66,8 +67,8 @@ public protocol VirtualMachineAgent: Sendable {
|
||||
// Networking
|
||||
func up(name: String, mtu: UInt32?) async throws
|
||||
func down(name: String) async throws
|
||||
func addressAdd(name: String, address: String) async throws
|
||||
func routeAddDefault(name: String, gateway: String) async throws
|
||||
func addressAdd(name: String, ipv4Address: CIDRv4) async throws
|
||||
func routeAddDefault(name: String, ipv4Gateway: IPv4Address) async throws
|
||||
func configureDNS(config: DNS, location: String) async throws
|
||||
func configureHosts(config: Hosts, location: String) async throws
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import ContainerizationError
|
||||
import ContainerizationExtras
|
||||
import ContainerizationOCI
|
||||
import ContainerizationOS
|
||||
import Foundation
|
||||
@@ -359,20 +360,20 @@ extension Vminitd {
|
||||
}
|
||||
|
||||
/// Add an IP address to the sandbox's network interfaces.
|
||||
public func addressAdd(name: String, address: String) async throws {
|
||||
public func addressAdd(name: String, ipv4Address: CIDRv4) async throws {
|
||||
_ = try await client.ipAddrAdd(
|
||||
.with {
|
||||
$0.interface = name
|
||||
$0.address = address
|
||||
$0.ipv4Address = ipv4Address.description
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the default route in the sandbox's environment.
|
||||
public func routeAddDefault(name: String, gateway: String) async throws {
|
||||
public func routeAddDefault(name: String, ipv4Gateway: IPv4Address) async throws {
|
||||
_ = try await client.ipRouteAddDefault(
|
||||
.with {
|
||||
$0.interface = name
|
||||
$0.gateway = gateway
|
||||
$0.ipv4Gateway = ipv4Gateway.description
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Describes an IPv4 or IPv6 CIDR address block.
|
||||
@frozen
|
||||
public enum CIDR: CustomStringConvertible, Equatable, Sendable, Hashable {
|
||||
|
||||
case v4(IPv4Address, Prefix)
|
||||
case v6(IPv6Address, Prefix)
|
||||
|
||||
/// Create a CIDR address block.
|
||||
public init(_ cidr: String) throws {
|
||||
if let cidrV4 = try? CIDRv4(cidr) {
|
||||
self = .v4(cidrV4.address, cidrV4.prefix)
|
||||
} else if let cidrV6 = try? CIDRv6(cidr) {
|
||||
self = .v6(cidrV6.address, cidrV6.prefix)
|
||||
} else {
|
||||
throw Error.invalidCIDR(cidr: cidr)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a CIDR address from a member IP and a prefix length.
|
||||
public init(_ address: IPAddress, prefix: Prefix) throws {
|
||||
switch address {
|
||||
case .v4(let addr):
|
||||
guard prefix.length <= 32 else {
|
||||
throw Error.invalidCIDR(cidr: "\(address)/\(prefix)")
|
||||
}
|
||||
self = .v4(addr, prefix)
|
||||
case .v6(let addr):
|
||||
guard prefix.length <= 128 else {
|
||||
throw Error.invalidCIDR(cidr: "\(address)/\(prefix)")
|
||||
}
|
||||
self = .v6(addr, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the smallest CIDR block that includes the lower and upper bounds.
|
||||
///
|
||||
/// For type-safe construction, prefer `v4Range(lower:upper:)` or `v6Range(lower:upper:)`.
|
||||
public init(lower: IPAddress, upper: IPAddress) throws {
|
||||
switch (lower, upper) {
|
||||
case (.v4(let lowerAddr), .v4(let upperAddr)):
|
||||
let cidr = try CIDRv4(lower: lowerAddr, upper: upperAddr)
|
||||
self = .v4(cidr.address, cidr.prefix)
|
||||
case (.v6(let lowerAddr), .v6(let upperAddr)):
|
||||
let cidr = try CIDRv6(lower: lowerAddr, upper: upperAddr)
|
||||
self = .v6(cidr.address, cidr.prefix)
|
||||
default:
|
||||
throw Error.invalidAddressRange(lower: lower.description, upper: upper.description)
|
||||
}
|
||||
}
|
||||
|
||||
/// The IP component of this CIDR address.
|
||||
@inlinable
|
||||
public var address: IPAddress {
|
||||
switch self {
|
||||
case .v4(let addr, _):
|
||||
return .v4(addr)
|
||||
case .v6(let addr, _):
|
||||
return .v6(addr)
|
||||
}
|
||||
}
|
||||
|
||||
/// The prefix length of this CIDR address.
|
||||
@inlinable
|
||||
public var prefix: Prefix {
|
||||
switch self {
|
||||
case .v4(_, let prefix), .v6(_, let prefix):
|
||||
return prefix
|
||||
}
|
||||
}
|
||||
|
||||
/// The lowest address in this CIDR block
|
||||
@inlinable
|
||||
public var lower: IPAddress {
|
||||
switch self {
|
||||
case (.v4(let addr, let prefix)):
|
||||
return .v4(IPv4Address(addr.value & prefix.prefixMask32))
|
||||
case (.v6(let addr, let prefix)):
|
||||
return .v6(IPv6Address(addr.value & prefix.prefixMask128))
|
||||
}
|
||||
}
|
||||
|
||||
/// The highest address in this CIDR block (broadcast address).
|
||||
@inlinable
|
||||
public var upper: IPAddress {
|
||||
switch self {
|
||||
case .v4(let addr, let prefix):
|
||||
return .v4(IPv4Address(addr.value | prefix.suffixMask32))
|
||||
case .v6(let addr, let prefix):
|
||||
return .v6(IPv6Address(addr.value | prefix.suffixMask128, zone: addr.zone))
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if the CIDR block contains the specified address.
|
||||
///
|
||||
/// Compares network portion of the given IP address.
|
||||
@inlinable
|
||||
public func contains(_ ip: IPAddress) -> Bool {
|
||||
switch (self, ip) {
|
||||
case (.v4(let network, let prefix), .v4(let ip)):
|
||||
return network.value == (ip.value & prefix.prefixMask32)
|
||||
case (.v6(let network, let prefix), .v6(let ip)):
|
||||
return (network.zone == ip.zone) && (network.value == (ip.value & prefix.prefixMask128))
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve the text representation of the CIDR block.
|
||||
public var description: String {
|
||||
"\(address)/\(prefix)"
|
||||
}
|
||||
}
|
||||
|
||||
extension CIDR {
|
||||
public enum Error: Swift.Error {
|
||||
case invalidCIDR(cidr: String)
|
||||
case invalidAddressRange(lower: String, upper: String)
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Describes an IPv4 CIDR address block.
|
||||
public struct CIDRAddress: CustomStringConvertible, Equatable, Sendable {
|
||||
|
||||
/// The base IPv4 address of the CIDR block.
|
||||
public let lower: IPv4Address
|
||||
|
||||
/// The last IPv4 address of the CIDR block
|
||||
public let upper: IPv4Address
|
||||
|
||||
/// The IPv4 address component of the CIDR block.
|
||||
public let address: IPv4Address
|
||||
|
||||
/// The address prefix length for the CIDR block, which determines its size.
|
||||
public let prefixLength: PrefixLength
|
||||
|
||||
/// Create an CIDR address block from its text representation.
|
||||
public init(_ cidr: String) throws {
|
||||
let split = cidr.components(separatedBy: "/")
|
||||
guard split.count == 2 else {
|
||||
throw NetworkAddressError.invalidCIDR(cidr: cidr)
|
||||
}
|
||||
address = try IPv4Address(split[0])
|
||||
guard let prefixLength = PrefixLength(split[1]) else {
|
||||
throw NetworkAddressError.invalidCIDR(cidr: cidr)
|
||||
}
|
||||
guard prefixLength >= 0 && prefixLength <= 32 else {
|
||||
throw NetworkAddressError.invalidCIDR(cidr: cidr)
|
||||
}
|
||||
|
||||
self.prefixLength = prefixLength
|
||||
lower = address.prefix(prefixLength: prefixLength)
|
||||
upper = IPv4Address(fromValue: lower.value + prefixLength.suffixMask32)
|
||||
}
|
||||
|
||||
/// Create a CIDR address from a member IP and a prefix length.
|
||||
public init(_ address: IPv4Address, prefixLength: PrefixLength) throws {
|
||||
guard prefixLength >= 0 && prefixLength <= 32 else {
|
||||
throw NetworkAddressError.invalidCIDR(cidr: "\(address)/\(prefixLength)")
|
||||
}
|
||||
|
||||
self.prefixLength = prefixLength
|
||||
self.address = address
|
||||
lower = address.prefix(prefixLength: prefixLength)
|
||||
upper = IPv4Address(fromValue: lower.value + prefixLength.suffixMask32)
|
||||
}
|
||||
|
||||
/// Create the smallest CIDR block that includes the lower and upper bounds.
|
||||
public init(lower: IPv4Address, upper: IPv4Address) throws {
|
||||
guard lower.value <= upper.value else {
|
||||
throw NetworkAddressError.invalidAddressRange(lower: lower.description, upper: upper.description)
|
||||
}
|
||||
|
||||
address = lower
|
||||
for prefixLength: PrefixLength in 1...32 {
|
||||
// find the first case where a subnet mask would put lower and upper in different CIDR block
|
||||
let mask = prefixLength.prefixMask32
|
||||
|
||||
if (lower.value & mask) != (upper.value & mask) {
|
||||
self.prefixLength = prefixLength - 1
|
||||
self.lower = address.prefix(prefixLength: self.prefixLength)
|
||||
self.upper = IPv4Address(fromValue: self.lower.value + self.prefixLength.suffixMask32)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// if lower and upper are same, create a /32 block
|
||||
self.prefixLength = 32
|
||||
self.lower = lower
|
||||
self.upper = upper
|
||||
}
|
||||
|
||||
/// Get the offset of the specified address, relative to the
|
||||
/// base address for the CIDR block, returning nil if the block
|
||||
/// does not contain the address.
|
||||
public func getIndex(_ address: IPv4Address) -> UInt32? {
|
||||
guard address.value >= lower.value && address.value <= upper.value else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return address.value - lower.value
|
||||
}
|
||||
|
||||
/// Return true if the CIDR block contains the specified address.
|
||||
public func contains(ipv4: IPv4Address) -> Bool {
|
||||
lower.value <= ipv4.value && ipv4.value <= upper.value
|
||||
}
|
||||
|
||||
/// Return true if the CIDR block contains all addresses of another CIDR block.
|
||||
public func contains(cidr: CIDRAddress) -> Bool {
|
||||
lower.value <= cidr.lower.value && cidr.upper.value <= upper.value
|
||||
}
|
||||
|
||||
/// Return true if the CIDR block shares any addresses with another CIDR block.
|
||||
public func overlaps(cidr: CIDRAddress) -> Bool {
|
||||
(lower.value <= cidr.lower.value && upper.value >= cidr.lower.value)
|
||||
|| (upper.value >= cidr.upper.value && lower.value <= cidr.upper.value)
|
||||
}
|
||||
|
||||
/// Retrieve the text representation of the CIDR block.
|
||||
public var description: String {
|
||||
"\(address)/\(prefixLength)"
|
||||
}
|
||||
}
|
||||
|
||||
extension CIDRAddress: Codable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let text = try container.decode(String.self)
|
||||
try self.init(text)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(self.description)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Describes an IPv4 CIDR address block.
|
||||
@frozen
|
||||
public struct CIDRv4: CustomStringConvertible, Equatable, Sendable, Hashable {
|
||||
/// The IP component of this CIDR address.
|
||||
public let address: IPv4Address
|
||||
|
||||
/// The prefix length of this CIDR address.
|
||||
public let prefix: Prefix
|
||||
|
||||
/// Create a CIDR address block.
|
||||
public init(_ cidr: String) throws {
|
||||
let split = cidr.split(separator: "/")
|
||||
guard split.count == 2 else {
|
||||
throw CIDR.Error.invalidCIDR(cidr: cidr)
|
||||
}
|
||||
guard let prefixLength = UInt8(split[1]), let prefix = Prefix(length: prefixLength) else {
|
||||
throw CIDR.Error.invalidCIDR(cidr: cidr)
|
||||
}
|
||||
|
||||
let address = try IPv4Address(String(split[0]))
|
||||
try self.init(address, prefix: prefix)
|
||||
}
|
||||
|
||||
/// Create a CIDR address from a member IP and a prefix length.
|
||||
public init(_ address: IPv4Address, prefix: Prefix) throws {
|
||||
guard prefix.length <= 32 else {
|
||||
throw CIDR.Error.invalidCIDR(cidr: "\(address)/\(prefix)")
|
||||
}
|
||||
self.address = address
|
||||
self.prefix = prefix
|
||||
}
|
||||
|
||||
/// Create the smallest IPv4 CIDR block that includes the lower and upper bounds.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lower: The lower bound IPv4 address
|
||||
/// - upper: The upper bound IPv4 address
|
||||
/// - Returns: The smallest CIDR block containing both addresses
|
||||
/// - Throws: If lower > upper
|
||||
public init(lower: IPv4Address, upper: IPv4Address) throws {
|
||||
guard lower.value <= upper.value else {
|
||||
throw CIDR.Error.invalidAddressRange(lower: lower.description, upper: upper.description)
|
||||
}
|
||||
|
||||
for length in 1...32 {
|
||||
let prefixLength = Prefix(unchecked: UInt8(length))
|
||||
let mask = prefixLength.prefixMask32
|
||||
if (lower.value & mask) != (upper.value & mask) {
|
||||
let prefix = Prefix(unchecked: UInt8(length - 1))
|
||||
let networkAddr = IPv4Address(lower.value & prefix.prefixMask32)
|
||||
try self.init(networkAddr, prefix: prefix)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Same address - /32 block
|
||||
let prefix = Prefix(unchecked: 32)
|
||||
let networkAddr = IPv4Address(lower.value & prefix.prefixMask32)
|
||||
try self.init(networkAddr, prefix: prefix)
|
||||
}
|
||||
|
||||
/// The lowest address in this CIDR block
|
||||
@inlinable
|
||||
public var lower: IPv4Address {
|
||||
IPv4Address(address.value & prefix.prefixMask32)
|
||||
}
|
||||
|
||||
/// The highest address in this CIDR block (broadcast address).
|
||||
@inlinable
|
||||
public var upper: IPv4Address {
|
||||
IPv4Address(address.value | prefix.suffixMask32)
|
||||
}
|
||||
|
||||
/// Return true if the CIDR block contains the specified address.
|
||||
///
|
||||
/// Compares network portion of the given IP address.
|
||||
@inlinable
|
||||
public func contains(_ ip: IPv4Address) -> Bool {
|
||||
(address.value & prefix.prefixMask32) == (ip.value & prefix.prefixMask32)
|
||||
}
|
||||
|
||||
/// Retrieve the text representation of the CIDR block.
|
||||
public var description: String {
|
||||
"\(address)/\(prefix)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Describes an IPv4 or IPv6 CIDR address block.
|
||||
@frozen
|
||||
public struct CIDRv6: CustomStringConvertible, Equatable, Sendable, Hashable {
|
||||
|
||||
/// The IP component of this CIDR address.
|
||||
public let address: IPv6Address
|
||||
|
||||
/// The prefix length of this CIDR address.
|
||||
public let prefix: Prefix
|
||||
|
||||
/// Create a CIDR address block.
|
||||
public init(_ cidr: String) throws {
|
||||
let split = cidr.split(separator: "/")
|
||||
guard split.count == 2 else {
|
||||
throw CIDR.Error.invalidCIDR(cidr: cidr)
|
||||
}
|
||||
guard let prefixLength = UInt8(split[1]), let prefix = Prefix(length: prefixLength) else {
|
||||
throw CIDR.Error.invalidCIDR(cidr: cidr)
|
||||
}
|
||||
|
||||
let address = try IPv6Address(String(split[0]))
|
||||
try self.init(address, prefix: prefix)
|
||||
}
|
||||
|
||||
/// Create a CIDR address from a member IP and a prefix length.
|
||||
public init(_ address: IPv6Address, prefix: Prefix) throws {
|
||||
guard prefix.length <= 128 else {
|
||||
throw CIDR.Error.invalidCIDR(cidr: "\(address)/\(prefix)")
|
||||
}
|
||||
self.address = address
|
||||
self.prefix = prefix
|
||||
}
|
||||
|
||||
/// Create the smallest IPv6 CIDR block that includes the lower and upper bounds.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lower: The lower bound IPv6 address
|
||||
/// - upper: The upper bound IPv6 address
|
||||
/// - Returns: The smallest CIDR block containing both addresses
|
||||
/// - Throws: If lower > upper or zones don't match
|
||||
public init(lower: IPv6Address, upper: IPv6Address) throws {
|
||||
guard lower.value <= upper.value && lower.zone == upper.zone else {
|
||||
throw CIDR.Error.invalidAddressRange(lower: lower.description, upper: upper.description)
|
||||
}
|
||||
|
||||
for length in 1...128 {
|
||||
let prefixLength = Prefix(unchecked: UInt8(length))
|
||||
let mask = prefixLength.prefixMask128
|
||||
if (lower.value & mask) != (upper.value & mask) {
|
||||
let prefix = Prefix(unchecked: UInt8(length - 1))
|
||||
let networkAddr = IPv6Address(lower.value & prefix.prefixMask128, zone: lower.zone)
|
||||
try self.init(networkAddr, prefix: prefix)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Same address - /128 block
|
||||
let prefix = Prefix(unchecked: 128)
|
||||
let networkAddr = IPv6Address(lower.value & prefix.prefixMask128, zone: lower.zone)
|
||||
try self.init(networkAddr, prefix: prefix)
|
||||
}
|
||||
|
||||
/// The lowest address in this CIDR block
|
||||
@inlinable
|
||||
public var lower: IPv6Address {
|
||||
IPv6Address(address.value & prefix.prefixMask128)
|
||||
}
|
||||
|
||||
/// The highest address in this CIDR block (broadcast address).
|
||||
@inlinable
|
||||
public var upper: IPv6Address {
|
||||
IPv6Address(address.value | prefix.suffixMask128, zone: address.zone)
|
||||
}
|
||||
|
||||
/// Return true if the CIDR block contains the specified address.
|
||||
///
|
||||
/// Compares network portion of the given IP address.
|
||||
@inlinable
|
||||
public func contains(_ ip: IPv6Address) -> Bool {
|
||||
(address.zone == ip.zone) && ((address.value & prefix.prefixMask128) == (ip.value & prefix.prefixMask128))
|
||||
}
|
||||
|
||||
/// Retrieve the text representation of the CIDR block.
|
||||
public var description: String {
|
||||
"\(address)/\(prefix)"
|
||||
}
|
||||
}
|
||||
@@ -14,79 +14,122 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Facilitates conversion between IPv4 address representations.
|
||||
public struct IPv4Address: Codable, CustomStringConvertible, Equatable, Sendable {
|
||||
/// The address as a 32-bit integer.
|
||||
public let value: UInt32
|
||||
/// Represents an IP address that can be either IPv4 or IPv6.
|
||||
@frozen
|
||||
public enum IPAddress: Sendable, Hashable, CustomStringConvertible, Equatable {
|
||||
/// An IPv4 address
|
||||
case v4(IPv4Address)
|
||||
|
||||
/// Create an address from a dotted-decimal string, such as "192.168.64.10".
|
||||
public init(_ fromString: String) throws {
|
||||
let split = fromString.components(separatedBy: ".")
|
||||
if split.count != 4 {
|
||||
throw NetworkAddressError.invalidStringAddress(address: fromString)
|
||||
}
|
||||
/// An IPv6 address
|
||||
case v6(IPv6Address)
|
||||
|
||||
var parsedValue: UInt32 = 0
|
||||
for index in 0..<4 {
|
||||
guard let octet = UInt8(split[index]) else {
|
||||
throw NetworkAddressError.invalidStringAddress(address: fromString)
|
||||
/// Parses an IP address string, automatically detecting IPv4 or IPv6 format.
|
||||
///
|
||||
/// - Parameter string: IP address string to parse
|
||||
/// - Returns: An `IPAddress` containing either an IPv4 or IPv6 address
|
||||
/// - Throws: `IPAddressError.unableToParse` if invalid
|
||||
public init(_ string: String) throws {
|
||||
let utf8 = string.utf8
|
||||
var hasColon = false
|
||||
var hasDot = false
|
||||
|
||||
for byte in utf8 {
|
||||
if byte == 58 { // ASCII ':'
|
||||
hasColon = true
|
||||
break
|
||||
}
|
||||
if byte == 46 { // ASCII '.'
|
||||
hasDot = true
|
||||
}
|
||||
parsedValue |= UInt32(octet) << ((3 - index) * 8)
|
||||
}
|
||||
|
||||
value = parsedValue
|
||||
}
|
||||
|
||||
/// Create an address from an array of four bytes in network order (big-endian),
|
||||
/// such as [192, 168, 64, 10].
|
||||
public init(fromNetworkBytes: [UInt8]) throws {
|
||||
guard fromNetworkBytes.count == 4 else {
|
||||
throw NetworkAddressError.invalidNetworkByteAddress(address: fromNetworkBytes)
|
||||
if hasColon {
|
||||
let ipv6 = try IPv6Address.parse(string)
|
||||
self = .v6(ipv6)
|
||||
} else if hasDot {
|
||||
let ipv4 = try IPv4Address(string)
|
||||
self = .v4(ipv4)
|
||||
} else {
|
||||
throw IPAddressError.unableToParse
|
||||
}
|
||||
|
||||
value =
|
||||
(UInt32(fromNetworkBytes[0]) << 24)
|
||||
| (UInt32(fromNetworkBytes[1]) << 16)
|
||||
| (UInt32(fromNetworkBytes[2]) << 8)
|
||||
| UInt32(fromNetworkBytes[3])
|
||||
}
|
||||
|
||||
/// Create an address from a 32-bit integer, such as 0xc0a8_400a.
|
||||
public init(fromValue: UInt32) {
|
||||
value = fromValue
|
||||
}
|
||||
|
||||
/// Retrieve the address as an array of bytes in network byte order.
|
||||
public var networkBytes: [UInt8] {
|
||||
[
|
||||
UInt8((value >> 24) & 0xff),
|
||||
UInt8((value >> 16) & 0xff),
|
||||
UInt8((value >> 8) & 0xff),
|
||||
UInt8(value & 0xff),
|
||||
]
|
||||
}
|
||||
|
||||
/// Retrieve the address as a dotted decimal string.
|
||||
/// String representation of the IP address.
|
||||
public var description: String {
|
||||
networkBytes.map(String.init).joined(separator: ".")
|
||||
switch self {
|
||||
case .v4(let addr):
|
||||
return addr.description
|
||||
case .v6(let addr):
|
||||
return addr.description
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the base IPv4 address for a network that contains this
|
||||
/// address and uses the specified subnet mask length.
|
||||
public func prefix(prefixLength: PrefixLength) -> IPv4Address {
|
||||
IPv4Address(fromValue: value & prefixLength.prefixMask32)
|
||||
/// Returns `true` if this is an IPv4 address.
|
||||
@inlinable
|
||||
public var isV4: Bool {
|
||||
if case .v4 = self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
extension IPv4Address {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let text = try container.decode(String.self)
|
||||
try self.init(text)
|
||||
/// Returns `true` if this is an IPv6 address.
|
||||
@inlinable
|
||||
public var isV6: Bool {
|
||||
if case .v6 = self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(self.description)
|
||||
/// Returns the underlying IPv4 address if this is an IPv4 address, otherwise `nil`.
|
||||
@inlinable
|
||||
public var ipv4: IPv4Address? {
|
||||
if case .v4(let addr) = self {
|
||||
return addr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Returns the underlying IPv6 address if this is an IPv6 address, otherwise `nil`.
|
||||
@inlinable
|
||||
public var ipv6: IPv6Address? {
|
||||
if case .v6(let addr) = self {
|
||||
return addr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a loopback address (127.0.0.0/8 or ::1).
|
||||
@inlinable
|
||||
public var isLoopback: Bool {
|
||||
switch self {
|
||||
case .v4(let addr):
|
||||
return addr.isLoopback
|
||||
case .v6(let addr):
|
||||
return addr.isLoopback
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a multicast address.
|
||||
@inlinable
|
||||
public var isMulticast: Bool {
|
||||
switch self {
|
||||
case .v4(let addr):
|
||||
return addr.isMulticast
|
||||
case .v6(let addr):
|
||||
return addr.isMulticast
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if this is an unspecified address (0.0.0.0 or ::).
|
||||
@inlinable
|
||||
public var isUnspecified: Bool {
|
||||
switch self {
|
||||
case .v4(let addr):
|
||||
return addr.isUnspecified
|
||||
case .v6(let addr):
|
||||
return addr.isUnspecified
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
public struct IPAddressError: Error, Equatable, Hashable, CustomStringConvertible {
|
||||
public var description: String {
|
||||
String(describing: self.base)
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
enum Base: Equatable, Hashable, Sendable {
|
||||
case unableToParse
|
||||
case invalidZoneIdentifier
|
||||
case invalidIPv4Suffix
|
||||
case multipleEllipsis
|
||||
case invalidHexGroup
|
||||
case malformedAddress
|
||||
case incompleteAddress
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
let base: Base
|
||||
|
||||
@inlinable
|
||||
init(_ base: Base) { self.base = base }
|
||||
|
||||
public static var unableToParse: Self {
|
||||
Self(.unableToParse)
|
||||
}
|
||||
|
||||
public static var invalidZoneIdentifier: Self {
|
||||
Self(.invalidZoneIdentifier)
|
||||
}
|
||||
|
||||
public static var invalidIPv4SuffixInIPv6Address: Self {
|
||||
Self(.invalidIPv4Suffix)
|
||||
}
|
||||
|
||||
public static var multipleEllipsis: Self {
|
||||
Self(.multipleEllipsis)
|
||||
}
|
||||
|
||||
public static var invalidHexGroup: Self {
|
||||
Self(.invalidHexGroup)
|
||||
}
|
||||
|
||||
public static var malformedAddress: Self {
|
||||
Self(.malformedAddress)
|
||||
}
|
||||
|
||||
public static var incompleteAddress: Self {
|
||||
Self(.incompleteAddress)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
@frozen
|
||||
public struct IPv4Address: Sendable, Hashable, CustomStringConvertible, Equatable, Comparable {
|
||||
public let value: UInt32
|
||||
|
||||
@inlinable
|
||||
public init(_ value: UInt32) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
/// Creates an IPv4Address from a string representation.
|
||||
///
|
||||
/// - Parameter string: The IPv4 address string in dotted decimal notation (e.g., "192.168.1.1")
|
||||
/// - Throws: `IPAddressError.unableToParse` if the string is not a valid IPv4 address
|
||||
@inlinable
|
||||
public init(_ string: String) throws {
|
||||
self.value = try Self.parse(string)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public var bytes: [UInt8] {
|
||||
Self.bytes(value)
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
static func bytes(_ value: UInt32) -> [UInt8] {
|
||||
var result = [UInt8](repeating: 0, count: 4)
|
||||
result[0] = UInt8((value >> 24) & 0xff)
|
||||
result[1] = UInt8((value >> 16) & 0xff)
|
||||
result[2] = UInt8((value >> 8) & 0xff)
|
||||
result[3] = UInt8(value & 0xff)
|
||||
return result
|
||||
}
|
||||
|
||||
// TODO: spans?
|
||||
@available(macOS 26.0, *)
|
||||
@usableFromInline
|
||||
static func bytes(_ value: UInt32) -> InlineArray<4, UInt8> {
|
||||
let result: InlineArray<4, UInt8> = [
|
||||
UInt8((value >> 24) & 0xff),
|
||||
UInt8((value >> 16) & 0xff),
|
||||
UInt8((value >> 8) & 0xff),
|
||||
UInt8(value & 0xff),
|
||||
]
|
||||
return result
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public var description: String {
|
||||
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
|
||||
}
|
||||
|
||||
/// Parses an IPv4 address string in dotted decimal notation into a UInt32 representation.
|
||||
///
|
||||
/// ## Validation Rules
|
||||
/// - Exactly 4 octets separated by dots
|
||||
/// - Each octet must be 0-255
|
||||
/// - No leading zeros (except for "0" itself)
|
||||
/// - No whitespace characters
|
||||
/// - Only digits and dots allowed
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```swift
|
||||
/// IPv4Address.parse("192.168.1.1") // Returns: 3232235777
|
||||
/// IPv4Address.parse("127.0.0.1") // Returns: 2130706433
|
||||
/// IPv4Address.parse("0.0.0.0") // Returns: 0
|
||||
/// IPv4Address.parse("255.255.255.255") // Returns: 4294967295
|
||||
///
|
||||
/// // Invalid examples:
|
||||
/// IPv4Address.parse("192.168.1") // Wrong number of octets
|
||||
/// IPv4Address.parse("192.168.1.256") // Octet out of range
|
||||
/// IPv4Address.parse("192.168.001.1") // Leading zeros
|
||||
/// IPv4Address.parse(" 192.168.1.1 ") // Whitespace
|
||||
/// ```
|
||||
///
|
||||
/// - Parameter s: The IPv4 address string to parse
|
||||
/// - Returns: The 32-bit representation of the IP address, or `nil` if parsing fails
|
||||
/// - Note: The returned value is in network byte order (big-endian)
|
||||
@usableFromInline
|
||||
internal static func parse(_ s: String) throws -> UInt32 {
|
||||
guard !s.isEmpty, s.count >= 7, s.count <= 15 else {
|
||||
throw IPAddressError.unableToParse
|
||||
}
|
||||
|
||||
// IP addresses should only contain ASCII digits and dots
|
||||
let utf8 = s.utf8
|
||||
for byte in utf8 {
|
||||
// ASCII whitespace: space(32), tab(9), newline(10), return(13)
|
||||
if byte == 32 || byte == 9 || byte == 10 || byte == 13 {
|
||||
throw IPAddressError.unableToParse
|
||||
}
|
||||
}
|
||||
|
||||
// accumulator for the 32bit representation of the IPv4 address
|
||||
var result: UInt32 = 0
|
||||
|
||||
// tracking octet count, max 4 allowed
|
||||
var octetCount = 0
|
||||
var currentOctet = 0
|
||||
|
||||
// number of digits in the string representation of the octet, max 3
|
||||
var digitCount = 0
|
||||
|
||||
for byte in utf8 {
|
||||
if byte == 46 { // ASCII '.'
|
||||
// Validate octet before processing
|
||||
guard octetCount < 3, digitCount > 0, digitCount <= 3, currentOctet <= 255 else {
|
||||
throw IPAddressError.unableToParse
|
||||
}
|
||||
|
||||
// Shift result and add current octet
|
||||
result = (result << 8) | UInt32(currentOctet)
|
||||
|
||||
// Reset for next octet
|
||||
octetCount += 1
|
||||
currentOctet = 0
|
||||
digitCount = 0
|
||||
|
||||
} else if byte >= 48 && byte <= 57 { // ASCII '0'-'9'
|
||||
let digit = Int(byte - 48)
|
||||
|
||||
digitCount += 1
|
||||
|
||||
// Check for invalid leading zeros: "01", "001", etc.
|
||||
// Allow single "0" but reject multi-digit numbers starting with 0
|
||||
if digitCount == 1 && digit == 0 {
|
||||
// First digit is 0 - this is only valid if it's the only digit
|
||||
currentOctet = 0
|
||||
} else if digitCount > 1 && currentOctet == 0 {
|
||||
// We had a leading zero and now have more digits - invalid
|
||||
throw IPAddressError.unableToParse
|
||||
} else {
|
||||
// Normal case: build the octet value
|
||||
currentOctet = currentOctet * 10 + digit
|
||||
}
|
||||
|
||||
// Early termination if octet becomes too large
|
||||
guard currentOctet <= 255, digitCount <= 3 else {
|
||||
throw IPAddressError.unableToParse
|
||||
}
|
||||
|
||||
} else {
|
||||
throw IPAddressError.unableToParse
|
||||
}
|
||||
}
|
||||
|
||||
// Validate final octet
|
||||
guard octetCount == 3, digitCount > 0, digitCount <= 3, currentOctet <= 255 else {
|
||||
throw IPAddressError.unableToParse
|
||||
}
|
||||
|
||||
return (result << 8) | UInt32(currentOctet)
|
||||
}
|
||||
|
||||
// MARK: - Address Classification Methods
|
||||
|
||||
/// Returns `true` if this is the unspecified address (0.0.0.0).
|
||||
///
|
||||
/// Per RFC 791, 0.0.0.0 is the "this network" address.
|
||||
@inlinable
|
||||
public var isUnspecified: Bool {
|
||||
value == 0
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a loopback address (127.0.0.0/8).
|
||||
///
|
||||
/// Per RFC 1122 Section 3.2.1.3, the entire 127.0.0.0/8 block is reserved for loopback.
|
||||
@inlinable
|
||||
public var isLoopback: Bool {
|
||||
(value & 0xFF00_0000) == 0x7F00_0000
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a multicast address (224.0.0.0/4).
|
||||
///
|
||||
/// Per RFC 1112, addresses in the range 224.0.0.0 to 239.255.255.255 are multicast addresses.
|
||||
@inlinable
|
||||
public var isMulticast: Bool {
|
||||
(value & 0xF000_0000) == 0xE000_0000
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a link-local address (169.254.0.0/16).
|
||||
///
|
||||
/// Per RFC 3927, 169.254.0.0/16 is reserved for link-local addresses (APIPA/Auto-IP).
|
||||
@inlinable
|
||||
public var isLinkLocal: Bool {
|
||||
(value & 0xFFFF_0000) == 0xA9FE_0000
|
||||
}
|
||||
|
||||
/// Returns `true` if this is the limited broadcast address (255.255.255.255).
|
||||
///
|
||||
/// Per RFC 919/922, 255.255.255.255 is the limited broadcast address.
|
||||
@inlinable
|
||||
public var isBroadcast: Bool {
|
||||
value == 0xFFFF_FFFF
|
||||
}
|
||||
|
||||
/// Compares two IPv4 addresses numerically.
|
||||
@inlinable
|
||||
public static func < (lhs: IPv4Address, rhs: IPv4Address) -> Bool {
|
||||
lhs.value < rhs.value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
extension IPv6Address {
|
||||
/// Parses an IPv6 address string into an IPv6Address instance.
|
||||
///
|
||||
/// Follows RFC 4291 and RFC 5952.
|
||||
///
|
||||
/// This function supports standard IPv6 notation including:
|
||||
/// - Full addresses: `2001:0db8:0000:0042:0000:8a2e:0370:7334`
|
||||
/// - Zero compression: `2001:db8::8a2e:370:7334`
|
||||
/// - Leading zero omission: `2001:db8:0:42:0:8a2e:370:7334`
|
||||
/// - Unspecified address: `::`
|
||||
/// - Zone identifiers: `fe80::1%eth0`
|
||||
///
|
||||
/// - Parameter input: IPv6 address string (with optional zone identifier after %)
|
||||
/// - Returns: An `IPv6Address` instance representing the parsed address
|
||||
///
|
||||
/// ## Example Usage
|
||||
/// ```swift
|
||||
/// let addr1 = try IPv6Address.parse("2001:db8::1")
|
||||
/// let addr2 = try IPv6Address.parse("::") // Unspecified address
|
||||
/// let addr3 = try IPv6Address.parse("fe80::1%eth0") // With zone identifier
|
||||
/// ```
|
||||
static func parse(_ input: String) throws -> IPv6Address {
|
||||
var ipBytes = [UInt8](repeating: 0, count: 16)
|
||||
var ellipsisPosition: Int?
|
||||
|
||||
// Extract zone identifier
|
||||
let (address, zone) = try extractZoneIdentifier(from: input)
|
||||
|
||||
// RFC 4291 Section 2.2.3: IPv4 suffix must be at the end (last 32 bits)
|
||||
var remainingAddress = address
|
||||
var ipv6ByteLimit = 16 // Maximum bytes available for IPv6 hex groups
|
||||
var hasIPv4Suffix = false
|
||||
|
||||
// check if the IPv6 address has IPv4 address in it.
|
||||
if let (ipv6Part, ipv4Bytes) = try extractIPv4Suffix(from: address) {
|
||||
// If IPv4 present, save directly to last 4 bytes.
|
||||
ipBytes[12] = ipv4Bytes[0]
|
||||
ipBytes[13] = ipv4Bytes[1]
|
||||
ipBytes[14] = ipv4Bytes[2]
|
||||
ipBytes[15] = ipv4Bytes[3]
|
||||
|
||||
// Update address and limit IPv6 parsing to first 12 bytes (6 groups max)
|
||||
remainingAddress = ipv6Part
|
||||
ipv6ByteLimit = 12
|
||||
hasIPv4Suffix = true
|
||||
}
|
||||
|
||||
// Handle leading ellipsis in the IPv6 part
|
||||
if remainingAddress.utf8.starts(with: [58, 58]) { // "::"
|
||||
ellipsisPosition = 0
|
||||
remainingAddress = String(remainingAddress.dropFirst(2))
|
||||
|
||||
// Special case: "::" represents the unspecified address (all zeros)
|
||||
// But if we have IPv4 suffix, the IPv4 bytes are already set correctly
|
||||
if remainingAddress.isEmpty {
|
||||
// If we have IPv4 suffix, ipBytes already has the IPv4 data, just return
|
||||
if hasIPv4Suffix {
|
||||
return Self(ipBytes, zone: zone)
|
||||
}
|
||||
|
||||
// Pure "::" - Return the unspecified address, handling zone identifiers
|
||||
if let zone = zone, !zone.isEmpty {
|
||||
return Self(ipBytes, zone: zone)
|
||||
}
|
||||
return .unspecified
|
||||
}
|
||||
}
|
||||
|
||||
// Parse IPv6 hex groups up to the byte limit
|
||||
var byteIndex = 0
|
||||
let utf8 = remainingAddress.utf8
|
||||
var currentPosition = utf8.startIndex
|
||||
|
||||
while byteIndex < ipv6ByteLimit && currentPosition < utf8.endIndex {
|
||||
let (hexValue, nextPosition) = try parseHexadecimal(
|
||||
from: utf8,
|
||||
startingAt: currentPosition
|
||||
)
|
||||
|
||||
// Store the UInt16 in network-byte order
|
||||
ipBytes[byteIndex] = UInt8(hexValue >> 8)
|
||||
ipBytes[byteIndex + 1] = UInt8(hexValue & 0xFF)
|
||||
byteIndex += 2
|
||||
currentPosition = nextPosition
|
||||
|
||||
// Terminate early if we have consumed the whole string
|
||||
if currentPosition == utf8.endIndex {
|
||||
break
|
||||
}
|
||||
|
||||
// Parse separator and handle ellipsis detection
|
||||
currentPosition = try skipColonSeparator(
|
||||
from: utf8,
|
||||
at: currentPosition,
|
||||
currentByteIndex: byteIndex,
|
||||
ellipsisPosition: &ellipsisPosition
|
||||
)
|
||||
}
|
||||
|
||||
// Validate complete consumption of input
|
||||
guard currentPosition >= utf8.endIndex else {
|
||||
throw IPAddressError.malformedAddress
|
||||
}
|
||||
|
||||
// Apply ellipsis expansion for the IPv6 portion
|
||||
try expandEllipsis(
|
||||
in: &ipBytes,
|
||||
parsedBytes: byteIndex,
|
||||
ellipsisPosition: ellipsisPosition,
|
||||
byteLimit: ipv6ByteLimit
|
||||
)
|
||||
let value = ipBytes.reduce(UInt128(0)) { ($0 << 8) | UInt128($1) }
|
||||
return Self(value, zone: zone)
|
||||
}
|
||||
|
||||
// MARK: - Helper Functions
|
||||
|
||||
/// Extracts IPv4 suffix if present at the end of the address
|
||||
///
|
||||
/// Follows: RFC 4291 Section 2.2.3: Alternative form x:x:x:x:x:x:d.d.d.d
|
||||
/// IPv4 must be the last 32 bits and preceded by a colon
|
||||
///
|
||||
/// - Parameter input: The IPv6 address string to check
|
||||
/// - Returns: Optional tuple of (IPv6 part without IPv4, IPv4 bytes array) if IPv4 found, nil otherwise
|
||||
/// - Throws: `IPAddressError.invalidIPv4Suffix` for invalid IPv4 addresses
|
||||
internal static func extractIPv4Suffix(from input: String) throws -> (String, [UInt8])? {
|
||||
// must contain a dot to be IPv4
|
||||
guard input.utf8.contains(46) else { // ASCII '.'
|
||||
return nil
|
||||
}
|
||||
|
||||
// IPv4 address must be present after last colon
|
||||
guard let lastColonIndex = input.lastIndex(of: ":") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: maybe refactor for performance
|
||||
let afterColon = input.index(after: lastColonIndex)
|
||||
guard afterColon < input.endIndex else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let possibleIPv4 = String(input[afterColon...])
|
||||
guard let ipv4Value = try? IPv4Address.parse(possibleIPv4) else {
|
||||
throw IPAddressError.invalidIPv4SuffixInIPv6Address
|
||||
}
|
||||
|
||||
// Check if lastColonIndex is the second ':' of '::'. If so, ensure to include it.
|
||||
let isDoubleColon = lastColonIndex > input.startIndex && input[input.index(before: lastColonIndex)] == ":"
|
||||
let ipv6Part = isDoubleColon ? String(input[...lastColonIndex]) : String(input[..<lastColonIndex])
|
||||
return (ipv6Part, IPv4Address.bytes(ipv4Value))
|
||||
}
|
||||
|
||||
/// Extracts zone identifier
|
||||
///
|
||||
/// - Parameter input: The full IPv6 address string with potential zone identifier
|
||||
/// - Returns: Tuple of (address part, optional zone identifier)
|
||||
/// - Throws: `IPAddressError.invalidZoneIdentifier` for malformed zone identifiers
|
||||
private static func extractZoneIdentifier(from input: String) throws -> (String, String?) {
|
||||
guard let percentIndex = input.lastIndex(of: "%") else {
|
||||
return (input, nil)
|
||||
}
|
||||
|
||||
let zoneStartIndex = input.index(after: percentIndex)
|
||||
guard zoneStartIndex < input.endIndex else {
|
||||
throw IPAddressError.invalidZoneIdentifier
|
||||
}
|
||||
|
||||
let addressPart = String(input[..<percentIndex])
|
||||
let zoneIdentifier = String(input[zoneStartIndex...])
|
||||
|
||||
return (addressPart, zoneIdentifier)
|
||||
}
|
||||
|
||||
/// Parses a hexadecimal group from an IPv6 address component.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - utf8: The UTF-8 view to parse from
|
||||
/// - startIndex: Starting position in the UTF-8 view
|
||||
/// - Returns: Tuple of (parsed hex value as UInt16, next position after parsed digits)
|
||||
/// - Throws: `IPAddressError.invalidHexGroup` if no valid hex digits are found
|
||||
///
|
||||
/// ## Example
|
||||
/// ```swift
|
||||
/// let utf8 = "2001:db8::1".utf8
|
||||
/// let (value, nextPos) = try parseHexadecimal(from: utf8, startingAt: utf8.startIndex)
|
||||
/// // value = 0x2001, nextPos points to ':'
|
||||
/// ```
|
||||
@inlinable
|
||||
internal static func parseHexadecimal(
|
||||
from group: String.UTF8View,
|
||||
startingAt startIndex: String.UTF8View.Index
|
||||
) throws -> (UInt16, String.UTF8View.Index) {
|
||||
var accumulator: UInt16 = 0
|
||||
var digitCount = 0
|
||||
var currentIndex = startIndex
|
||||
|
||||
while currentIndex < group.endIndex && digitCount < 4 {
|
||||
let byte = group[currentIndex]
|
||||
|
||||
// Fast hex digit parsing using ASCII values
|
||||
let hexValue: UInt16
|
||||
if byte >= 48 && byte <= 57 { // '0'-'9'
|
||||
hexValue = UInt16(byte - 48)
|
||||
} else if byte >= 65 && byte <= 70 { // 'A'-'F'
|
||||
hexValue = UInt16(byte - 65 + 10)
|
||||
} else if byte >= 97 && byte <= 102 { // 'a'-'f'
|
||||
hexValue = UInt16(byte - 97 + 10)
|
||||
} else {
|
||||
break // Not a hex digit
|
||||
}
|
||||
|
||||
accumulator = (accumulator << 4) + hexValue
|
||||
digitCount += 1
|
||||
currentIndex = group.index(after: currentIndex)
|
||||
}
|
||||
|
||||
guard digitCount > 0 else {
|
||||
// No hex digits found
|
||||
throw IPAddressError.invalidHexGroup
|
||||
}
|
||||
return (accumulator, currentIndex)
|
||||
}
|
||||
|
||||
/// Parses a colon separator between IPv6 groups and detects ellipsis notation (::).
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - utf8: The UTF-8 view being parsed
|
||||
/// - position: Current position in the UTF-8 view (must point to a colon)
|
||||
/// - currentByteIndex: Current byte index in the IP array
|
||||
/// - ellipsisPosition: Inout parameter tracking ellipsis position
|
||||
/// - Returns: Next position in the UTF-8 view after parsing separator
|
||||
///
|
||||
/// ## Example
|
||||
/// ```swift
|
||||
/// let utf8 = "2001:db8::1".utf8
|
||||
/// var ellipsisPos: Int? = nil
|
||||
/// // After parsing "2001", position points to first ':'
|
||||
/// let nextPos = try skipColonSeparator(from: utf8, at: position,
|
||||
/// currentByteIndex: 2,
|
||||
/// ellipsisPosition: &ellipsisPos)
|
||||
/// // For single colon: nextPos points to 'd' in 'db8'
|
||||
/// // For double colon (::): ellipsisPos = 2, nextPos points to '1'
|
||||
/// ```
|
||||
private static func skipColonSeparator(
|
||||
from group: String.UTF8View,
|
||||
at position: String.UTF8View.Index,
|
||||
currentByteIndex: Int,
|
||||
ellipsisPosition: inout Int?
|
||||
) throws -> String.UTF8View.Index {
|
||||
// Expect colon separator
|
||||
guard group[position] == 58 else { // ASCII ':'
|
||||
throw IPAddressError.malformedAddress
|
||||
}
|
||||
|
||||
let afterFirstColon = group.index(after: position)
|
||||
guard afterFirstColon < group.endIndex else {
|
||||
// Trailing colon not allowed
|
||||
throw IPAddressError.malformedAddress
|
||||
}
|
||||
|
||||
// Check for double colon, return position after that
|
||||
if group[afterFirstColon] == 58 { // ASCII ':'
|
||||
guard ellipsisPosition == nil else {
|
||||
// Multiple :: not allowed
|
||||
throw IPAddressError.multipleEllipsis
|
||||
}
|
||||
ellipsisPosition = currentByteIndex
|
||||
let afterSecondColon = group.index(after: afterFirstColon)
|
||||
return afterSecondColon
|
||||
}
|
||||
return afterFirstColon
|
||||
}
|
||||
|
||||
/// Expands ellipsis for IPv6 addresses
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - ipBytes: Inout array of IP bytes to modify
|
||||
/// - parsedBytes: Number of bytes already parsed for IPv6 groups
|
||||
/// - ellipsisPosition: Optional position where ellipsis was found
|
||||
/// - byteLimit: Maximum bytes available for IPv6 (16 for pure IPv6, 12 if IPv4 suffix present)
|
||||
/// - Throws: `IPAddressError.incompleteAddress` for invalid address lengths
|
||||
private static func expandEllipsis(
|
||||
in ipBytes: inout [UInt8],
|
||||
parsedBytes: Int,
|
||||
ellipsisPosition: Int?,
|
||||
byteLimit: Int = 16
|
||||
) throws {
|
||||
guard let ellipsisPosition = ellipsisPosition else {
|
||||
// No ellipsis - validate we have exactly filled the available bytes
|
||||
guard parsedBytes == byteLimit else {
|
||||
throw IPAddressError.incompleteAddress // Incomplete address without ellipsis
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate expansion within the byte limit
|
||||
let bytesToExpand = byteLimit - parsedBytes
|
||||
guard bytesToExpand > 0 else {
|
||||
throw IPAddressError.malformedAddress // No room for ellipsis expansion
|
||||
}
|
||||
|
||||
let suffixBytes = Array(ipBytes[ellipsisPosition..<parsedBytes])
|
||||
let targetStartIndex = byteLimit - suffixBytes.count
|
||||
|
||||
// Clear the expansion area using Swift's range-based assignment
|
||||
for i in ellipsisPosition..<targetStartIndex {
|
||||
ipBytes[i] = 0
|
||||
}
|
||||
|
||||
// Place suffix at the end of the IPv6 section using Swift's collection assignment
|
||||
for (offset, byte) in suffixBytes.enumerated() {
|
||||
ipBytes[targetStartIndex + offset] = byte
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 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 {
|
||||
@usableFromInline
|
||||
internal let value: UInt128
|
||||
|
||||
@usableFromInline
|
||||
internal 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: `IPAddressError` 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 representing the IPv6 address
|
||||
/// - zone: Optional zone identifier (e.g., "eth0")
|
||||
@inlinable
|
||||
public init(_ bytes: [UInt8], zone: String? = nil) {
|
||||
precondition(bytes.count == 16, "IPv6 address must be exactly 16 bytes")
|
||||
|
||||
// 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 ?? "")
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ extension IPv4Address {
|
||||
}
|
||||
return Int(address.value - lower)
|
||||
},
|
||||
indexToAddress: { IPv4Address(fromValue: lower + UInt32($0)) }
|
||||
indexToAddress: { IPv4Address(lower + UInt32($0)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Errors related to IP and CIDR addresses.
|
||||
public enum NetworkAddressError: Swift.Error, Equatable, CustomStringConvertible {
|
||||
case invalidStringAddress(address: String)
|
||||
case invalidNetworkByteAddress(address: [UInt8])
|
||||
case invalidCIDR(cidr: String)
|
||||
case invalidAddressForSubnet(address: String, cidr: String)
|
||||
case invalidAddressRange(lower: String, upper: String)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .invalidStringAddress(let address):
|
||||
return "invalid IP address string \(address)"
|
||||
case .invalidNetworkByteAddress(let address):
|
||||
return "invalid IP address bytes \(address)"
|
||||
case .invalidCIDR(let cidr):
|
||||
return "invalid CIDR block: \(cidr)"
|
||||
case .invalidAddressForSubnet(let address, let cidr):
|
||||
return "invalid address \(address) for subnet \(cidr)"
|
||||
case .invalidAddressRange(let lower, let upper):
|
||||
return "invalid range for addresses \(lower) and \(upper)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public typealias PrefixLength = UInt8
|
||||
|
||||
extension PrefixLength {
|
||||
/// Compute a bit mask that passes the suffix bits, given the network prefix mask length.
|
||||
public var suffixMask32: UInt32 {
|
||||
if self <= 0 {
|
||||
return 0xffff_ffff
|
||||
}
|
||||
return self >= 32 ? 0x0000_0000 : (1 << (32 - self)) - 1
|
||||
}
|
||||
|
||||
/// Compute a bit mask that passes the prefix bits, given the network prefix mask length.
|
||||
public var prefixMask32: UInt32 {
|
||||
~self.suffixMask32
|
||||
}
|
||||
|
||||
/// Compute a bit mask that passes the suffix bits, given the network prefix mask length.
|
||||
public var suffixMask48: UInt64 {
|
||||
if self <= 0 {
|
||||
return 0x0000_ffff_ffff_ffff
|
||||
}
|
||||
return self >= 48 ? 0x0000_0000_0000_0000 : (1 << (48 - self)) - 1
|
||||
}
|
||||
|
||||
/// Compute a bit mask that passes the prefix bits, given the network prefix mask length.
|
||||
public var prefixMask48: UInt64 {
|
||||
~self.suffixMask48 & 0x0000_ffff_ffff_ffff
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// CIDR prefix length (e.g., `/24` for a 24-bit network mask).
|
||||
@frozen
|
||||
public struct Prefix: Sendable, CustomStringConvertible, Hashable {
|
||||
public let length: UInt8
|
||||
|
||||
/// Create a prefix (0-128). Use `ipv4(_:)` or `ipv6(_:)` for version-specific validation.
|
||||
public init?(length: UInt8) {
|
||||
guard length <= 128 else { return nil }
|
||||
self.length = length
|
||||
}
|
||||
|
||||
/// Create an IPv4 prefix (0-32). Returns `nil` if length > 32.
|
||||
public static func ipv4(_ length: UInt8) -> Prefix? {
|
||||
guard length <= 32 else { return nil }
|
||||
return Prefix(unchecked: length)
|
||||
}
|
||||
|
||||
/// Create an IPv6 prefix (0-128). Returns `nil` if length > 128.
|
||||
public static func ipv6(_ length: UInt8) -> Prefix? {
|
||||
guard length <= 128 else { return nil }
|
||||
return Prefix(unchecked: length)
|
||||
}
|
||||
|
||||
/// Internal unchecked initializer for known-valid values.
|
||||
internal init(unchecked length: UInt8) {
|
||||
self.length = length
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
"\(length)"
|
||||
}
|
||||
}
|
||||
|
||||
extension Prefix {
|
||||
/// Computes a 32-bit mask for the suffix (host) portion of an IPv4 address.
|
||||
///
|
||||
/// Example: Prefix `/24` → `0x0000_00FF` (255 host addresses)
|
||||
@inlinable
|
||||
public var suffixMask32: UInt32 {
|
||||
if self.length <= 0 {
|
||||
return 0xffff_ffff
|
||||
}
|
||||
return self.length >= 32 ? 0x0000_0000 : (1 << (32 - self.length)) - 1
|
||||
}
|
||||
|
||||
/// Network portion mask (high-order bits) for IPv4.
|
||||
///
|
||||
/// Example: Prefix `/24` → `0xFFFF_FF00` (255.255.255.0)
|
||||
@inlinable
|
||||
public var prefixMask32: UInt32 {
|
||||
~self.suffixMask32
|
||||
}
|
||||
|
||||
/// Computes a 128-bit mask for the suffix (host) portion of an IPv6 address.
|
||||
///
|
||||
/// Example: Prefix `/64` → `0x0000_0000_0000_0000_FFFF_FFFF_FFFF_FFFF`
|
||||
@inlinable
|
||||
public var suffixMask128: UInt128 {
|
||||
if self.length <= 0 {
|
||||
return UInt128.max
|
||||
}
|
||||
return self.length >= 128 ? 0 : (1 << (128 - self.length)) - 1
|
||||
}
|
||||
|
||||
/// Network portion mask (high-order bits) for IPv6.
|
||||
///
|
||||
/// Example: Prefix `/64` → `0xFFFF_FFFF_FFFF_FFFF_0000_0000_0000_0000`
|
||||
@inlinable
|
||||
public var prefixMask128: UInt128 {
|
||||
~self.suffixMask128
|
||||
}
|
||||
}
|
||||
@@ -193,8 +193,8 @@ public struct NetlinkSession {
|
||||
/// Adds an IPv4 address to an interface.
|
||||
/// - Parameters:
|
||||
/// - interface: The name of the interface.
|
||||
/// - address: The IPv4 address to add.
|
||||
public func addressAdd(interface: String, address: String) throws {
|
||||
/// - ipv4Address: The CIDRv4 address describing the interface IP and subnet prefix length.
|
||||
public func addressAdd(interface: String, ipv4Address: CIDRv4) throws {
|
||||
// ip addr add [addr] dev [interface]
|
||||
// ip address {add|change|replace} IFADDR dev IFNAME [ LIFETIME ] [ CONFFLAG-LIST ]
|
||||
// IFADDR := PREFIX | ADDR peer PREFIX
|
||||
@@ -205,9 +205,9 @@ public struct NetlinkSession {
|
||||
// CONFFLAG := [ home | nodad | mngtmpaddr | noprefixroute | autojoin ]
|
||||
// LIFETIME := [ valid_lft LFT ] [ preferred_lft LFT ]
|
||||
// LFT := forever | SECONDS
|
||||
let parsed = try parseCIDR(cidr: address)
|
||||
let interfaceIndex = try getInterfaceIndex(interface)
|
||||
let ipAddressBytes = try IPv4Address(parsed.address).networkBytes
|
||||
|
||||
let ipAddressBytes = ipv4Address.address.bytes
|
||||
let addressAttrSize = RTAttribute.size + MemoryLayout<UInt8>.size * ipAddressBytes.count
|
||||
let requestSize = NetlinkMessageHeader.size + AddressInfo.size + 2 * addressAttrSize
|
||||
var requestBuffer = [UInt8](repeating: 0, count: requestSize)
|
||||
@@ -224,7 +224,7 @@ public struct NetlinkSession {
|
||||
|
||||
let requestInfo = AddressInfo(
|
||||
family: UInt8(AddressFamily.AF_INET),
|
||||
prefixLength: parsed.prefix,
|
||||
prefixLength: ipv4Address.prefix.length,
|
||||
flags: 0,
|
||||
scope: NetlinkScope.RT_SCOPE_UNIVERSE,
|
||||
index: UInt32(interfaceIndex))
|
||||
@@ -253,37 +253,22 @@ public struct NetlinkSession {
|
||||
}
|
||||
}
|
||||
|
||||
private func parseCIDR(cidr: String) throws -> (address: String, prefix: UInt8) {
|
||||
let split = cidr.components(separatedBy: "/")
|
||||
guard split.count == 2 else {
|
||||
throw NetworkAddressError.invalidCIDR(cidr: cidr)
|
||||
}
|
||||
let address = split[0]
|
||||
guard let prefixLength = PrefixLength(split[1]) else {
|
||||
throw NetworkAddressError.invalidCIDR(cidr: cidr)
|
||||
}
|
||||
guard prefixLength >= 0 && prefixLength <= 32 else {
|
||||
throw NetworkAddressError.invalidCIDR(cidr: cidr)
|
||||
}
|
||||
return (address, prefixLength)
|
||||
}
|
||||
|
||||
/// Adds a route to an interface.
|
||||
/// Adds an IPv4 route to an interface.
|
||||
/// - Parameters:
|
||||
/// - interface: The name of the interface.
|
||||
/// - destinationAddress: The destination address to route to.
|
||||
/// - srcAddr: The source address to route from.
|
||||
/// - dstIpv4Addr: The CIDRv4 address describing the gateway IP and subnet prefix length.
|
||||
/// - srcIpv4Addr: The source IPv4 address to route from.
|
||||
public func routeAdd(
|
||||
interface: String,
|
||||
destinationAddress: String,
|
||||
srcAddr: String
|
||||
dstIpv4Addr: CIDRv4,
|
||||
srcIpv4Addr: IPv4Address
|
||||
) throws {
|
||||
// ip route add [dest-cidr] dev [interface] src [src-addr] proto kernel
|
||||
let parsed = try parseCIDR(cidr: destinationAddress)
|
||||
let interfaceIndex = try getInterfaceIndex(interface)
|
||||
let dstAddrBytes = try IPv4Address(parsed.address).networkBytes
|
||||
|
||||
let dstAddrBytes = dstIpv4Addr.address.bytes
|
||||
let dstAddrAttrSize = RTAttribute.size + dstAddrBytes.count
|
||||
let srcAddrBytes = try IPv4Address(srcAddr).networkBytes
|
||||
let srcAddrBytes = srcIpv4Addr.bytes
|
||||
let srcAddrAttrSize = RTAttribute.size + srcAddrBytes.count
|
||||
let interfaceAttrSize = RTAttribute.size + MemoryLayout<UInt32>.size
|
||||
let requestSize =
|
||||
@@ -301,7 +286,7 @@ public struct NetlinkSession {
|
||||
|
||||
let requestInfo = RouteInfo(
|
||||
family: UInt8(AddressFamily.AF_INET),
|
||||
dstLen: parsed.prefix,
|
||||
dstLen: dstIpv4Addr.prefix.length,
|
||||
srcLen: 0,
|
||||
tos: 0,
|
||||
table: RouteTable.MAIN,
|
||||
@@ -345,16 +330,16 @@ public struct NetlinkSession {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a default route to an interface.
|
||||
/// Adds a default IPv4 route to an interface.
|
||||
/// - Parameters:
|
||||
/// - interface: The name of the interface.
|
||||
/// - gateway: The gateway address.
|
||||
/// - ipv4Gateway: The gateway address.
|
||||
public func routeAddDefault(
|
||||
interface: String,
|
||||
gateway: String
|
||||
ipv4Gateway: IPv4Address
|
||||
) throws {
|
||||
// ip route add default via [dst-address] src [src-address]
|
||||
let dstAddrBytes = try IPv4Address(gateway).networkBytes
|
||||
let dstAddrBytes = ipv4Gateway.bytes
|
||||
let dstAddrAttrSize = RTAttribute.size + dstAddrBytes.count
|
||||
|
||||
let interfaceAttrSize = RTAttribute.size + MemoryLayout<UInt32>.size
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import ArgumentParser
|
||||
import Containerization
|
||||
import ContainerizationError
|
||||
import ContainerizationExtras
|
||||
import ContainerizationOCI
|
||||
import ContainerizationOS
|
||||
import Foundation
|
||||
@@ -124,7 +125,9 @@ extension Application {
|
||||
guard let gateway else {
|
||||
throw ContainerizationError(.invalidArgument, message: "gateway must be specified")
|
||||
}
|
||||
config.interfaces.append(NATInterface(address: ip, gateway: gateway))
|
||||
let ipv4Address = try CIDRv4(ip)
|
||||
let ipv4Gateway = try IPv4Address(gateway)
|
||||
config.interfaces.append(NATInterface(ipv4Address: ipv4Address, ipv4Gateway: ipv4Gateway))
|
||||
config.dns = .init(nameservers: [gateway])
|
||||
if nameservers.count > 0 {
|
||||
config.dns = .init(nameservers: nameservers)
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Testing
|
||||
|
||||
@testable import ContainerizationExtras
|
||||
|
||||
struct TestCIDR {
|
||||
|
||||
// MARK: - Normalization Tests
|
||||
|
||||
struct Normalization {
|
||||
let input: String
|
||||
let expectedNetwork: String
|
||||
let expectedLength: UInt8
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
Normalization(
|
||||
input: "192.168.1.100/24",
|
||||
expectedNetwork: "192.168.1.0",
|
||||
expectedLength: 24
|
||||
),
|
||||
Normalization(
|
||||
input: "10.1.2.3/16",
|
||||
expectedNetwork: "10.1.0.0",
|
||||
expectedLength: 16
|
||||
),
|
||||
Normalization(
|
||||
input: "2001:db8::1234/64",
|
||||
expectedNetwork: "2001:db8::",
|
||||
expectedLength: 64
|
||||
),
|
||||
Normalization(
|
||||
input: "172.16.0.1/12",
|
||||
expectedNetwork: "172.16.0.0",
|
||||
expectedLength: 12
|
||||
),
|
||||
])
|
||||
func testNormalization(testCase: Normalization) throws {
|
||||
let cidr = try CIDR(testCase.input)
|
||||
#expect(cidr.lower.description == testCase.expectedNetwork)
|
||||
#expect(cidr.prefix.length == testCase.expectedLength)
|
||||
}
|
||||
|
||||
struct ParsePreservation {
|
||||
let input: String
|
||||
let expectedIP: String
|
||||
let expectedLength: UInt8
|
||||
}
|
||||
|
||||
// MARK: - Bounds Tests
|
||||
|
||||
struct Bounds {
|
||||
let cidr: String
|
||||
let lower: String
|
||||
let upper: String
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
Bounds(
|
||||
cidr: "192.168.1.0/24",
|
||||
lower: "192.168.1.0",
|
||||
upper: "192.168.1.255"
|
||||
),
|
||||
Bounds(
|
||||
cidr: "10.0.0.0/8",
|
||||
lower: "10.0.0.0",
|
||||
upper: "10.255.255.255"
|
||||
),
|
||||
Bounds(
|
||||
cidr: "2001:db8::/64",
|
||||
lower: "2001:db8::",
|
||||
upper: "2001:db8::ffff:ffff:ffff:ffff"
|
||||
),
|
||||
Bounds(
|
||||
cidr: "192.168.1.0/32",
|
||||
lower: "192.168.1.0",
|
||||
upper: "192.168.1.0"
|
||||
),
|
||||
])
|
||||
func testBounds(testCase: Bounds) throws {
|
||||
let block = try CIDR(testCase.cidr)
|
||||
#expect(block.lower.description == testCase.lower)
|
||||
#expect(block.upper.description == testCase.upper)
|
||||
}
|
||||
|
||||
// MARK: - Containment Tests
|
||||
|
||||
struct IPContainment {
|
||||
let cidr: String
|
||||
let ip: String
|
||||
let shouldContain: Bool
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
IPContainment(
|
||||
cidr: "192.168.1.0/24",
|
||||
ip: "192.168.1.100",
|
||||
shouldContain: true
|
||||
),
|
||||
IPContainment(
|
||||
cidr: "192.168.1.0/24",
|
||||
ip: "192.168.2.1",
|
||||
shouldContain: false
|
||||
),
|
||||
IPContainment(
|
||||
cidr: "10.0.0.0/8",
|
||||
ip: "10.255.255.255",
|
||||
shouldContain: true
|
||||
),
|
||||
IPContainment(
|
||||
cidr: "10.0.0.0/8",
|
||||
ip: "11.0.0.1",
|
||||
shouldContain: false
|
||||
),
|
||||
IPContainment(
|
||||
cidr: "2001:db8::/32",
|
||||
ip: "2001:db8::1",
|
||||
shouldContain: true
|
||||
),
|
||||
])
|
||||
func testContainsIP(testCase: IPContainment) throws {
|
||||
let block = try CIDR(testCase.cidr)
|
||||
let address = try IPAddress(testCase.ip)
|
||||
#expect(block.contains(address) == testCase.shouldContain)
|
||||
}
|
||||
|
||||
@Test func testDoesNotContainDifferentIPv6Zone() throws {
|
||||
let cidr = try CIDR("fe80::1/64")
|
||||
let ip = try IPv6Address("fe80::2%eth1")
|
||||
#expect(!cidr.contains(.v6(ip)))
|
||||
}
|
||||
|
||||
// MARK: - Range Constructor
|
||||
|
||||
@Test func testRangeConstructorFindsSmallestBlock() throws {
|
||||
let lower = try IPAddress("192.168.1.0")
|
||||
let upper = try IPAddress("192.168.1.255")
|
||||
let cidr = try CIDR(lower: lower, upper: upper)
|
||||
#expect(cidr.prefix.length == 24)
|
||||
#expect(cidr.address.description == "192.168.1.0")
|
||||
}
|
||||
|
||||
@Test func testRangeConstructorSingleIPv4() throws {
|
||||
let ip = try IPAddress("192.168.1.100")
|
||||
let cidr = try CIDR(lower: ip, upper: ip)
|
||||
#expect(cidr.prefix.length == 32)
|
||||
#expect(cidr.address.description == "192.168.1.100")
|
||||
}
|
||||
|
||||
@Test func testRangeConstructorIPv6() throws {
|
||||
let lower = try IPAddress("2001:db8::")
|
||||
let upper = try IPAddress("2001:db8::ffff:ffff:ffff:ffff")
|
||||
let cidr = try CIDR(lower: lower, upper: upper)
|
||||
#expect(cidr.prefix.length == 64)
|
||||
#expect(cidr.address.description == "2001:db8::")
|
||||
}
|
||||
|
||||
@Test func testRangeConstructorSingleIPv6() throws {
|
||||
let ip = try IPAddress("2001:db8::1")
|
||||
let cidr = try CIDR(lower: ip, upper: ip)
|
||||
#expect(cidr.prefix.length == 128)
|
||||
#expect(cidr.address.description == "2001:db8::1")
|
||||
}
|
||||
|
||||
@Test func testRangeConstructorRejectsMixedVersions() throws {
|
||||
#expect(throws: CIDR.Error.self) {
|
||||
let v4 = try IPAddress("192.168.1.0")
|
||||
let v6 = try IPAddress("2001:db8::1")
|
||||
_ = try CIDR(lower: v4, upper: v6)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testRangeConstructorRejectsDifferentZones() throws {
|
||||
#expect(throws: CIDR.Error.self) {
|
||||
let lower = IPAddress.v6(try IPv6Address("fe80::1%eth0"))
|
||||
let upper = IPAddress.v6(try IPv6Address("fe80::2%eth1"))
|
||||
_ = try CIDR(lower: lower, upper: upper)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Validation Tests
|
||||
|
||||
struct InvalidInput {
|
||||
let input: String
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
InvalidInput(input: "192.168.1.0/33"), // IPv4 prefix too large
|
||||
InvalidInput(input: "2001:db8::/129"), // IPv6 prefix too large
|
||||
InvalidInput(input: "192.168.1.0"), // Missing prefix
|
||||
InvalidInput(input: "192.168.1.0/"), // Empty prefix
|
||||
InvalidInput(input: "192.168.1.0/abc"), // Invalid prefix
|
||||
])
|
||||
func testRejectsInvalidInput(testCase: InvalidInput) throws {
|
||||
#expect(throws: CIDR.Error.self) {
|
||||
try CIDR(testCase.input)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testRejectsInvalidRangeOrder() throws {
|
||||
#expect(throws: CIDR.Error.self) {
|
||||
let lower = try IPAddress("192.168.1.255")
|
||||
let upper = try IPAddress("192.168.1.0")
|
||||
_ = try CIDR(lower: lower, upper: upper)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Range Constructor Validation Tests
|
||||
|
||||
@Test func testRangeConstructorValidatesContainment() throws {
|
||||
// This should work: 192.168.1.64 to 192.168.1.127 -> /26
|
||||
let lower1 = try IPAddress("192.168.1.64")
|
||||
let upper1 = try IPAddress("192.168.1.127")
|
||||
let cidr1 = try CIDR(lower: lower1, upper: upper1)
|
||||
#expect(cidr1.prefix.length == 26)
|
||||
#expect(cidr1.contains(lower1))
|
||||
#expect(cidr1.contains(upper1))
|
||||
}
|
||||
|
||||
@Test func testRangeConstructorValidatesIPv6Containment() throws {
|
||||
// Test IPv6 range containment validation
|
||||
let lower = try IPAddress("2001:db8::1000")
|
||||
let upper = try IPAddress("2001:db8::1fff")
|
||||
let cidr = try CIDR(lower: lower, upper: upper)
|
||||
#expect(cidr.contains(lower))
|
||||
#expect(cidr.contains(upper))
|
||||
}
|
||||
|
||||
// MARK: - Version-Specific Range Constructors
|
||||
|
||||
@Test func testV4RangeConstructor() throws {
|
||||
let lower = try IPAddress("192.168.1.0")
|
||||
let upper = try IPAddress("192.168.1.255")
|
||||
let cidr = try CIDR(lower: lower, upper: upper)
|
||||
#expect(cidr.prefix.length == 24)
|
||||
#expect(cidr.address.description == "192.168.1.0")
|
||||
}
|
||||
|
||||
@Test func testV4RangeSingleAddress() throws {
|
||||
let addr = try IPAddress("192.168.1.100")
|
||||
let cidr = try CIDR(lower: addr, upper: addr)
|
||||
#expect(cidr.prefix.length == 32)
|
||||
#expect(cidr.address.description == "192.168.1.100")
|
||||
}
|
||||
|
||||
@Test func testV6RangeConstructor() throws {
|
||||
let lower = try IPAddress("2001:db8::")
|
||||
let upper = try IPAddress("2001:db8::ffff:ffff:ffff:ffff")
|
||||
let cidr = try CIDR(lower: lower, upper: upper)
|
||||
#expect(cidr.prefix.length == 64)
|
||||
#expect(cidr.address.description == "2001:db8::")
|
||||
}
|
||||
|
||||
@Test func testV6RangeSingleAddress() throws {
|
||||
let addr = try IPAddress("2001:db8::1")
|
||||
let cidr = try CIDR(lower: addr, upper: addr)
|
||||
#expect(cidr.prefix.length == 128)
|
||||
#expect(cidr.address.description == "2001:db8::1")
|
||||
}
|
||||
|
||||
@Test func testV4RangeRejectsInvalidOrder() throws {
|
||||
let lower = try IPAddress("192.168.1.255")
|
||||
let upper = try IPAddress("192.168.1.0")
|
||||
#expect(throws: CIDR.Error.self) {
|
||||
_ = try CIDR(lower: lower, upper: upper)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testV6RangeRejectsInvalidOrder() throws {
|
||||
let lower = try IPAddress("2001:db8::ffff")
|
||||
let upper = try IPAddress("2001:db8::1")
|
||||
#expect(throws: CIDR.Error.self) {
|
||||
_ = try CIDR(lower: lower, upper: upper)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testV6RangeRejectsDifferentZones() throws {
|
||||
let lower = try IPAddress("fe80::1%eth0")
|
||||
let upper = try IPAddress("fe80::2%eth1")
|
||||
#expect(throws: CIDR.Error.self) {
|
||||
_ = try CIDR(lower: lower, upper: upper)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Description Tests
|
||||
|
||||
@Test func testDescriptionFormat() throws {
|
||||
let cidr = try CIDR("10.0.0.0/8")
|
||||
#expect(cidr.description == "10.0.0.0/8")
|
||||
}
|
||||
|
||||
@Test func testPreservesAddress() throws {
|
||||
let cidr = try CIDR("192.168.1.100/24")
|
||||
#expect(cidr.description == "192.168.1.100/24")
|
||||
}
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
//
|
||||
// swiftlint:disable force_try
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import ContainerizationExtras
|
||||
|
||||
final class TestCIDRAddress {
|
||||
@Test
|
||||
func testMissingSplitError() {
|
||||
let cidr = "192.168.64.0"
|
||||
do {
|
||||
_ = try CIDRAddress(cidr)
|
||||
#expect(Bool(false), "Expected AddressError.invalidCIDR to be thrown")
|
||||
} catch {
|
||||
#expect(error as? NetworkAddressError == .invalidCIDR(cidr: cidr), "Unexpected error thrown: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testInvalidIPError() {
|
||||
let cidr = "192.168.256.1/24"
|
||||
do {
|
||||
_ = try CIDRAddress(cidr)
|
||||
#expect(Bool(false), "Expected AddressError.invalidCIDR to be thrown")
|
||||
} catch {
|
||||
#expect(
|
||||
error as? NetworkAddressError == .invalidStringAddress(address: "192.168.256.1"),
|
||||
"Unexpected error thrown: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testInvalidSubnetTypeError() {
|
||||
let cidr = "192.168.64.0/foo"
|
||||
do {
|
||||
_ = try CIDRAddress(cidr)
|
||||
#expect(Bool(false), "Expected AddressError.invalidCIDR to be thrown")
|
||||
} catch {
|
||||
#expect(error as? NetworkAddressError == .invalidCIDR(cidr: cidr), "Unexpected error thrown: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testInvalidPrefixTooLargeError() {
|
||||
let cidr = "192.168.64.0/33"
|
||||
do {
|
||||
_ = try CIDRAddress(cidr)
|
||||
#expect(Bool(false), "Expected AddressError.invalidCIDR to be thrown")
|
||||
} catch {
|
||||
#expect(error as? NetworkAddressError == .invalidCIDR(cidr: cidr), "Unexpected error thrown: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testInvalidPrefixTooSmallError() {
|
||||
let cidr = "192.168.64.0/-1"
|
||||
do {
|
||||
_ = try CIDRAddress(cidr)
|
||||
#expect(Bool(false), "Expected AddressError.invalidCIDR to be thrown")
|
||||
} catch {
|
||||
#expect(error as? NetworkAddressError == .invalidCIDR(cidr: cidr), "Unexpected error thrown: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testComparison() async throws {
|
||||
let cidr64_1 = try! CIDRAddress("192.168.64.1/24")
|
||||
let cidr64_1a = try! CIDRAddress("192.168.64.1/24")
|
||||
#expect(cidr64_1 == cidr64_1a)
|
||||
#expect(cidr64_1.contains(cidr: cidr64_1a))
|
||||
#expect(cidr64_1a.contains(cidr: cidr64_1))
|
||||
#expect(cidr64_1.overlaps(cidr: cidr64_1a))
|
||||
#expect(cidr64_1a.overlaps(cidr: cidr64_1))
|
||||
|
||||
let cidr64_2 = try! CIDRAddress("192.168.64.2/24")
|
||||
#expect(cidr64_1 != cidr64_2)
|
||||
|
||||
let addr63_255 = try! IPv4Address("192.168.63.255")
|
||||
#expect(!cidr64_1.contains(ipv4: addr63_255))
|
||||
|
||||
let addr64_0 = try! IPv4Address("192.168.64.0")
|
||||
#expect(cidr64_1.contains(ipv4: addr64_0))
|
||||
|
||||
let addr64_255 = try! IPv4Address("192.168.64.255")
|
||||
#expect(cidr64_1.contains(ipv4: addr64_255))
|
||||
|
||||
let addr65_0 = try! IPv4Address("192.168.65.0")
|
||||
#expect(!cidr64_1.contains(ipv4: addr65_0))
|
||||
|
||||
let cidr64_prefix25 = try! CIDRAddress("192.168.64.0/25")
|
||||
#expect(cidr64_1.contains(cidr: cidr64_prefix25))
|
||||
#expect(!cidr64_prefix25.contains(cidr: cidr64_1))
|
||||
#expect(cidr64_1.overlaps(cidr: cidr64_prefix25))
|
||||
#expect(cidr64_prefix25.overlaps(cidr: cidr64_1))
|
||||
|
||||
let cidr64_prefix25a = try! CIDRAddress("192.168.64.128/25")
|
||||
#expect(cidr64_1.contains(cidr: cidr64_prefix25a))
|
||||
#expect(!cidr64_prefix25a.contains(cidr: cidr64_1))
|
||||
#expect(cidr64_1.overlaps(cidr: cidr64_prefix25a))
|
||||
#expect(cidr64_prefix25a.overlaps(cidr: cidr64_1))
|
||||
|
||||
let cidr63_prefix24 = try! CIDRAddress("192.168.63.0/24")
|
||||
#expect(!cidr64_1.contains(cidr: cidr63_prefix24))
|
||||
#expect(!cidr63_prefix24.contains(cidr: cidr64_1))
|
||||
#expect(!cidr64_1.overlaps(cidr: cidr63_prefix24))
|
||||
#expect(!cidr63_prefix24.overlaps(cidr: cidr64_1))
|
||||
|
||||
let cidr65_prefix24 = try! CIDRAddress("192.168.65.0/24")
|
||||
#expect(!cidr64_1.contains(cidr: cidr65_prefix24))
|
||||
#expect(!cidr65_prefix24.contains(cidr: cidr64_1))
|
||||
#expect(!cidr64_1.overlaps(cidr: cidr65_prefix24))
|
||||
#expect(!cidr65_prefix24.overlaps(cidr: cidr64_1))
|
||||
}
|
||||
|
||||
@Test
|
||||
func testBiggestSubnet() throws {
|
||||
let cidr = "1.2.3.4/0"
|
||||
let subnet = try CIDRAddress(cidr)
|
||||
#expect(try! IPv4Address("0.0.0.0") == subnet.lower)
|
||||
#expect(try! IPv4Address("1.2.3.4") == subnet.address)
|
||||
#expect(try! IPv4Address("255.255.255.255") == subnet.upper)
|
||||
#expect(0 == subnet.prefixLength)
|
||||
#expect(cidr == subnet.description)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testSmallestSubnet() throws {
|
||||
let cidr = "255.255.255.255/32"
|
||||
let subnet = try CIDRAddress(cidr)
|
||||
#expect(try! IPv4Address("255.255.255.255") == subnet.lower)
|
||||
#expect(try! IPv4Address("255.255.255.255") == subnet.address)
|
||||
#expect(try! IPv4Address("255.255.255.255") == subnet.upper)
|
||||
#expect(32 == subnet.prefixLength)
|
||||
#expect(cidr == subnet.description)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testJustRightSubnet() throws {
|
||||
let cidr = "192.168.64.10/24"
|
||||
let subnet = try CIDRAddress(cidr)
|
||||
#expect(try! IPv4Address("192.168.64.0") == subnet.lower)
|
||||
#expect(try! IPv4Address("192.168.64.10") == subnet.address)
|
||||
#expect(try! IPv4Address("192.168.64.255") == subnet.upper)
|
||||
#expect(24 == subnet.prefixLength)
|
||||
#expect(cidr == subnet.description)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testBiggestRangedSubnet() throws {
|
||||
let lower = try IPv4Address("127.255.255.255")
|
||||
let upper = try IPv4Address("128.0.0.0")
|
||||
let subnet = try CIDRAddress(lower: lower, upper: upper)
|
||||
#expect(try! IPv4Address("0.0.0.0") == subnet.lower)
|
||||
#expect(try! IPv4Address("127.255.255.255") == subnet.address)
|
||||
#expect(try! IPv4Address("255.255.255.255") == subnet.upper)
|
||||
#expect(0 == subnet.prefixLength)
|
||||
#expect("\(lower)/0" == subnet.description)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testSmallestRangedSubnet() throws {
|
||||
let lower = try IPv4Address("255.255.255.255")
|
||||
let subnet = try CIDRAddress(lower: lower, upper: lower)
|
||||
#expect(lower == subnet.lower)
|
||||
#expect(lower == subnet.address)
|
||||
#expect(lower == subnet.upper)
|
||||
#expect(32 == subnet.prefixLength)
|
||||
#expect("\(lower)/32" == subnet.description)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testJustRightRangedSubnet() throws {
|
||||
let lower = try IPv4Address("192.168.64.10")
|
||||
let upper = try IPv4Address("192.168.64.254")
|
||||
let subnet = try CIDRAddress(lower: lower, upper: upper)
|
||||
#expect(try! IPv4Address("192.168.64.0") == subnet.lower)
|
||||
#expect(try! IPv4Address("192.168.64.10") == subnet.address)
|
||||
#expect(try! IPv4Address("192.168.64.255") == subnet.upper)
|
||||
#expect(24 == subnet.prefixLength)
|
||||
#expect("\(lower)/24" == subnet.description)
|
||||
_ = 16 >> PrefixLength(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testCoding() async throws {
|
||||
let text = "200.100.50.25/12"
|
||||
let expected: CIDRAddress = try CIDRAddress(text)
|
||||
let data = try JSONEncoder().encode(expected)
|
||||
let actual = try JSONDecoder().decode(CIDRAddress.self, from: data)
|
||||
#expect(expected == actual)
|
||||
}
|
||||
}
|
||||
@@ -14,77 +14,162 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import ContainerizationExtras
|
||||
|
||||
final class TestIPv4Address {
|
||||
@Test
|
||||
func testUintMask() {
|
||||
#expect(0xffff_ffff == PrefixLength(0).suffixMask32)
|
||||
#expect(0x0000_7fff == PrefixLength(17).suffixMask32)
|
||||
#expect(0x0000_0000 == PrefixLength(32).suffixMask32)
|
||||
#expect(0x0000_0000 == PrefixLength(33).suffixMask32)
|
||||
#expect(0x0000_0000 == PrefixLength(0).prefixMask32)
|
||||
#expect(0xfffe_0000 == PrefixLength(15).prefixMask32)
|
||||
#expect(0xffff_ffff == PrefixLength(32).prefixMask32)
|
||||
#expect(0xffff_ffff == PrefixLength(33).prefixMask32)
|
||||
@Suite("Unified IPAddress Tests")
|
||||
struct IPAddressTests {
|
||||
|
||||
@Test(
|
||||
"Parse IPv4 addresses",
|
||||
arguments: [
|
||||
"192.168.1.1",
|
||||
"127.0.0.1",
|
||||
"0.0.0.0",
|
||||
"255.255.255.255",
|
||||
]
|
||||
)
|
||||
func testParseIPv4(input: String) throws {
|
||||
let ip = try IPAddress(input)
|
||||
#expect(ip.isV4, "Should be IPv4: \(input)")
|
||||
#expect(!ip.isV6, "Should not be IPv6: \(input)")
|
||||
#expect(ip.ipv4 != nil, "Should have IPv4 value: \(input)")
|
||||
#expect(ip.ipv6 == nil, "Should not have IPv6 value: \(input)")
|
||||
}
|
||||
|
||||
@Test
|
||||
func testOctetCountError() {
|
||||
let ipAddressValue = "192.168.64"
|
||||
do {
|
||||
_ = try IPv4Address(ipAddressValue)
|
||||
#expect(Bool(false), "Expected AddressError.invalidStringAddress to be thrown")
|
||||
} catch {
|
||||
#expect(
|
||||
error as? NetworkAddressError == .invalidStringAddress(address: ipAddressValue),
|
||||
"Unexpected error thrown: \(error)")
|
||||
@Test(
|
||||
"Parse IPv6 addresses",
|
||||
arguments: [
|
||||
"2001:db8::1",
|
||||
"::1",
|
||||
"::",
|
||||
"fe80::1",
|
||||
"2001:db8:0:0:0:0:0:1",
|
||||
]
|
||||
)
|
||||
func testParseIPv6(input: String) throws {
|
||||
let ip = try IPAddress(input)
|
||||
#expect(ip.isV6, "Should be IPv6: \(input)")
|
||||
#expect(!ip.isV4, "Should not be IPv4: \(input)")
|
||||
#expect(ip.ipv6 != nil, "Should have IPv6 value: \(input)")
|
||||
#expect(ip.ipv4 == nil, "Should not have IPv4 value: \(input)")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Loopback detection",
|
||||
arguments: [
|
||||
// Loopback addresses
|
||||
("127.0.0.1", true, "IPv4 loopback"),
|
||||
("127.0.0.255", true, "IPv4 loopback variant"),
|
||||
("127.255.255.255", true, "Any 127.x.x.x"),
|
||||
("::1", true, "IPv6 loopback"),
|
||||
// Non-loopback addresses
|
||||
("192.168.1.1", false, "Private IPv4"),
|
||||
("2001:db8::1", false, "IPv6 documentation"),
|
||||
("0.0.0.0", false, "IPv4 unspecified"),
|
||||
("::", false, "IPv6 unspecified"),
|
||||
]
|
||||
)
|
||||
func testLoopback(input: String, expected: Bool, description: String) throws {
|
||||
let ip = try IPAddress(input)
|
||||
#expect(ip.isLoopback == expected, "\(description): \(input) should\(expected ? "" : " not") be loopback")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Multicast detection",
|
||||
arguments: [
|
||||
// Multicast addresses
|
||||
("224.0.0.1", true, "IPv4 multicast start"),
|
||||
("239.255.255.255", true, "IPv4 multicast end (224.0.0.0/4)"),
|
||||
("ff02::1", true, "IPv6 link-local multicast"),
|
||||
("ff00::1", true, "IPv6 multicast"),
|
||||
// Non-multicast addresses
|
||||
("192.168.1.1", false, "Private IPv4"),
|
||||
("2001:db8::1", false, "IPv6 documentation"),
|
||||
("223.255.255.255", false, "Just before multicast range"),
|
||||
]
|
||||
)
|
||||
func testMulticast(input: String, expected: Bool, description: String) throws {
|
||||
let ip = try IPAddress(input)
|
||||
#expect(ip.isMulticast == expected, "\(description): \(input) should\(expected ? "" : " not") be multicast")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Unspecified detection",
|
||||
arguments: [
|
||||
// Unspecified addresses
|
||||
("0.0.0.0", true, "IPv4 unspecified"),
|
||||
("::", true, "IPv6 unspecified"),
|
||||
// Specified addresses
|
||||
("0.0.0.1", false, "Not unspecified IPv4"),
|
||||
("192.168.1.1", false, "Private IPv4"),
|
||||
("::1", false, "IPv6 loopback"),
|
||||
("2001:db8::1", false, "IPv6 documentation"),
|
||||
]
|
||||
)
|
||||
func testUnspecified(input: String, expected: Bool, description: String) throws {
|
||||
let ip = try IPAddress(input)
|
||||
#expect(ip.isUnspecified == expected, "\(description): \(input) should\(expected ? "" : " not") be unspecified")
|
||||
}
|
||||
|
||||
@Test("Comparable - IPv4 ordering")
|
||||
func testIPv4Ordering() throws {
|
||||
let ip1 = try IPv4Address("192.168.1.1")
|
||||
let ip2 = try IPv4Address("192.168.1.2")
|
||||
let ip3 = try IPv4Address("192.168.2.1")
|
||||
|
||||
#expect(ip1 < ip2)
|
||||
#expect(ip2 < ip3)
|
||||
#expect(ip1 < ip3)
|
||||
#expect(!(ip2 < ip1))
|
||||
}
|
||||
|
||||
@Test("Comparable - IPv6 ordering")
|
||||
func testIPv6Ordering() throws {
|
||||
let ip1 = try IPv6Address("2001:db8::1")
|
||||
let ip2 = try IPv6Address("2001:db8::2")
|
||||
let ip3 = try IPv6Address("2001:db9::1")
|
||||
|
||||
#expect(ip1 < ip2)
|
||||
#expect(ip2 < ip3)
|
||||
#expect(ip1 < ip3)
|
||||
#expect(!(ip2 < ip1))
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Equality",
|
||||
arguments: [
|
||||
("192.168.1.1", "192.168.1.1", true, "Same IPv4"),
|
||||
("192.168.1.1", "192.168.1.2", false, "Different IPv4"),
|
||||
("2001:db8::1", "2001:0db8:0000:0000:0000:0000:0000:0001", true, "Same IPv6, different format"),
|
||||
("2001:db8::1", "2001:db8::2", false, "Different IPv6"),
|
||||
]
|
||||
)
|
||||
func testEquality(addr1: String, addr2: String, shouldBeEqual: Bool, description: String) throws {
|
||||
let ip1 = try IPAddress(addr1)
|
||||
let ip2 = try IPAddress(addr2)
|
||||
|
||||
if shouldBeEqual {
|
||||
#expect(ip1 == ip2, "\(description): \(addr1) should equal \(addr2)")
|
||||
} else {
|
||||
#expect(ip1 != ip2, "\(description): \(addr1) should not equal \(addr2)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testOctetInvalidError() {
|
||||
let ipAddressValue = "192.168.256.255"
|
||||
do {
|
||||
_ = try IPv4Address(ipAddressValue)
|
||||
#expect(Bool(false), "Expected AddressError.invalidStringAddress to be thrown")
|
||||
} catch {
|
||||
#expect(
|
||||
error as? NetworkAddressError == .invalidStringAddress(address: ipAddressValue),
|
||||
"Unexpected error thrown: \(error)")
|
||||
}
|
||||
}
|
||||
@Test("Hashable")
|
||||
func testHashable() throws {
|
||||
var dict: [IPAddress: String] = [:]
|
||||
|
||||
@Test
|
||||
func testAddressFromString() throws {
|
||||
let ipAddressValue = "192.168.64.25"
|
||||
let ipAddress = try IPv4Address(ipAddressValue)
|
||||
#expect(ipAddressValue == ipAddress.description)
|
||||
#expect(UInt32(0xc0a8_4019) == ipAddress.value)
|
||||
#expect([192, 168, 64, 25] == ipAddress.networkBytes)
|
||||
}
|
||||
let ip1 = try IPAddress("192.168.1.1")
|
||||
let ip2 = try IPAddress("2001:db8::1")
|
||||
|
||||
@Test
|
||||
func testAddressPrefix() throws {
|
||||
let ipAddressValue = "172.18.204.85"
|
||||
let ipAddress = try IPv4Address(ipAddressValue)
|
||||
#expect(0x0000_0000 == ipAddress.prefix(prefixLength: 0).value)
|
||||
#expect(0xac12_cc55 == ipAddress.prefix(prefixLength: 32).value)
|
||||
#expect(0xac12_cc55 == ipAddress.prefix(prefixLength: 33).value)
|
||||
#expect(0xac10_0000 == ipAddress.prefix(prefixLength: 14).value)
|
||||
}
|
||||
dict[ip1] = "IPv4"
|
||||
dict[ip2] = "IPv6"
|
||||
|
||||
@Test
|
||||
func testCoding() async throws {
|
||||
let text = "200.100.50.25"
|
||||
let expected = try IPv4Address(text)
|
||||
let data = try JSONEncoder().encode(expected)
|
||||
let actual = try JSONDecoder().decode(IPv4Address.self, from: data)
|
||||
#expect(expected == actual)
|
||||
#expect(dict[ip1] == "IPv4")
|
||||
#expect(dict[ip2] == "IPv6")
|
||||
#expect(dict.count == 2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import ContainerizationExtras
|
||||
|
||||
@Suite("IPv4Address Tests")
|
||||
struct IPv4AddressTests {
|
||||
|
||||
// MARK: - Initializer Tests
|
||||
|
||||
@Suite("Initializers")
|
||||
struct InitializerTests {
|
||||
|
||||
@Test(
|
||||
"UInt32 initializer",
|
||||
arguments: [
|
||||
(0x7F00_0001, "127.0.0.1"), // localhost
|
||||
(0x0000_0000, "0.0.0.0"), // zero address
|
||||
(0xFFFF_FFFF, "255.255.255.255"), // max address
|
||||
(0xC0A8_0101, "192.168.1.1"), // private network
|
||||
(0x0808_0808, "8.8.8.8"), // Google DNS
|
||||
]
|
||||
)
|
||||
func testUInt32Initializer(inputValue: UInt32, description: String) {
|
||||
let address = IPv4Address(inputValue)
|
||||
#expect(address.value == inputValue)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"String initializer - valid addresses",
|
||||
arguments: [
|
||||
("127.0.0.1", 0x7F00_0001), // localhost
|
||||
("0.0.0.0", 0x0000_0000), // zero address
|
||||
("255.255.255.255", 0xFFFF_FFFF), // broadcast
|
||||
("10.0.0.1", 0x0A00_0001), // private network 10.x
|
||||
("192.168.1.1", 0xC0A8_0101), // private network 192.168.x
|
||||
("172.16.0.1", 0xAC10_0001), // private network 172.16.x
|
||||
("1.2.3.4", 0x0102_0304), // single digits
|
||||
("192.168.100.254", 0xC0A8_64FE), // mixed digits
|
||||
]
|
||||
)
|
||||
func testStringInitializerValid(addressString: String, expectedValue: UInt32) throws {
|
||||
let address = try IPv4Address(addressString)
|
||||
#expect(address.value == expectedValue)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"String initializer - invalid addresses",
|
||||
arguments: [
|
||||
"", // empty string
|
||||
"1.2.3", // too short
|
||||
"1.2.3.4.5", // too many octets
|
||||
"192.168.1.256", // octet out of range
|
||||
"192.168.001.1", // leading zeros
|
||||
"01.2.3.4", // leading zero first octet
|
||||
" 192.168.1.1", // leading whitespace
|
||||
"192.168.1.1 ", // trailing whitespace
|
||||
"192. 168.1.1", // internal whitespace
|
||||
"192.168.1.a", // invalid character
|
||||
"192.168.1.-1", // negative number
|
||||
"192..1.1", // missing octet
|
||||
".168.1.1", // missing first octet
|
||||
"192.168.1.", // missing last octet
|
||||
"192.168.1.1.extra", // too long
|
||||
]
|
||||
)
|
||||
func testStringInitializerInvalid(invalidAddress: String) {
|
||||
#expect(throws: IPAddressError.self) {
|
||||
try IPv4Address(invalidAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Property Tests
|
||||
|
||||
@Suite("Properties")
|
||||
struct PropertyTests {
|
||||
|
||||
@Test(
|
||||
"bytes property",
|
||||
arguments: [
|
||||
(UInt32(0x7F00_0001), [UInt8(127), UInt8(0), UInt8(0), UInt8(1)]), // localhost
|
||||
(UInt32(0x0000_0000), [UInt8(0), UInt8(0), UInt8(0), UInt8(0)]), // zero
|
||||
(UInt32(0xFFFF_FFFF), [UInt8(255), UInt8(255), UInt8(255), UInt8(255)]), // broadcast
|
||||
(UInt32(0xC0A8_0101), [UInt8(192), UInt8(168), UInt8(1), UInt8(1)]), // private network
|
||||
(UInt32(0x1234_5678), [UInt8(0x12), UInt8(0x34), UInt8(0x56), UInt8(0x78)]), // byte order test
|
||||
]
|
||||
)
|
||||
func testBytesProperty(inputValue: UInt32, expectedBytes: [UInt8]) {
|
||||
let address = IPv4Address(inputValue)
|
||||
#expect(address.bytes == expectedBytes)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"description property",
|
||||
arguments: [
|
||||
(0x7F00_0001, "127.0.0.1"), // localhost
|
||||
(0x0000_0000, "0.0.0.0"), // zero
|
||||
(0xFFFF_FFFF, "255.255.255.255"), // broadcast
|
||||
(0xC0A8_0101, "192.168.1.1"), // private network
|
||||
(0x0102_0304, "1.2.3.4"), // single digits
|
||||
]
|
||||
)
|
||||
func testDescriptionProperty(inputValue: UInt32, expectedDescription: String) {
|
||||
let address = IPv4Address(inputValue)
|
||||
#expect(address.description == expectedDescription)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"round-trip string conversion",
|
||||
arguments: [
|
||||
"0.0.0.0",
|
||||
"127.0.0.1",
|
||||
"192.168.1.1",
|
||||
"10.0.0.1",
|
||||
"172.16.0.1",
|
||||
"255.255.255.255",
|
||||
"1.2.3.4",
|
||||
"8.8.8.8",
|
||||
"1.1.1.1",
|
||||
]
|
||||
)
|
||||
func testRoundTripStringConversion(addressString: String) throws {
|
||||
let address = try IPv4Address(addressString)
|
||||
#expect(address.description == addressString)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Protocol Conformance Tests
|
||||
|
||||
@Suite("Protocol Conformances")
|
||||
struct ProtocolConformanceTests {
|
||||
|
||||
@Test("Equatable conformance")
|
||||
func testEquatableConformance() {
|
||||
let addr1 = IPv4Address(0x7F00_0001)
|
||||
let addr2 = IPv4Address(0x7F00_0001)
|
||||
let addr3 = IPv4Address(0xC0A8_0101)
|
||||
|
||||
#expect(addr1 == addr2)
|
||||
#expect(addr1 != addr3)
|
||||
#expect(addr2 != addr3)
|
||||
}
|
||||
|
||||
@Test("Hashable conformance")
|
||||
func testHashableConformance() {
|
||||
let addr1 = IPv4Address(0x7F00_0001)
|
||||
let addr2 = IPv4Address(0x7F00_0001)
|
||||
let addr3 = IPv4Address(0xC0A8_0101)
|
||||
|
||||
// Equal objects should have equal hash values
|
||||
#expect(addr1.hashValue == addr2.hashValue)
|
||||
|
||||
// Different objects should ideally have different hash values
|
||||
// (though this is not guaranteed, it's very likely for these values)
|
||||
#expect(addr1.hashValue != addr3.hashValue)
|
||||
|
||||
// Test that addresses can be used in Sets and Dictionaries
|
||||
let addressSet: Set<IPv4Address> = [addr1, addr2, addr3]
|
||||
#expect(addressSet.count == 2) // addr1 and addr2 are equal
|
||||
|
||||
let addressDict = [addr1: "localhost", addr3: "private"]
|
||||
#expect(addressDict[addr2] == "localhost") // addr2 equals addr1
|
||||
}
|
||||
|
||||
@Test("CustomStringConvertible conformance")
|
||||
func testCustomStringConvertibleConformance() {
|
||||
let address = IPv4Address(0x7F00_0001)
|
||||
let stringRepresentation = String(describing: address)
|
||||
#expect(stringRepresentation == "127.0.0.1")
|
||||
}
|
||||
|
||||
@Test("Sendable conformance")
|
||||
func testSendableConformance() {
|
||||
// This test verifies that IPv4Address can be safely passed across concurrency boundaries
|
||||
let address = IPv4Address(0x7F00_0001)
|
||||
|
||||
Task {
|
||||
let taskAddress = address
|
||||
#expect(taskAddress.value == 0x7F00_0001)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Edge Cases and Error Conditions
|
||||
|
||||
@Suite("Edge Cases")
|
||||
struct EdgeCaseTests {
|
||||
|
||||
@Test(
|
||||
"boundary values",
|
||||
arguments: [
|
||||
("0.0.0.0", 0x0000_0000), // minimum
|
||||
("255.255.255.255", 0xFFFF_FFFF), // maximum
|
||||
("255.0.0.0", 0xFF00_0000), // max first octet
|
||||
("0.255.0.0", 0x00FF_0000), // max second octet
|
||||
("0.0.255.0", 0x0000_FF00), // max third octet
|
||||
("0.0.0.255", 0x0000_00FF), // max fourth octet
|
||||
]
|
||||
)
|
||||
func testBoundaryValues(addressString: String, expectedValue: UInt32) throws {
|
||||
let address = try IPv4Address(addressString)
|
||||
#expect(address.value == expectedValue)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"special addresses",
|
||||
arguments: [
|
||||
"127.0.0.1", // loopback
|
||||
"255.255.255.255", // broadcast
|
||||
"0.0.0.0", // network address
|
||||
"8.8.8.8", // Google DNS
|
||||
"1.1.1.1", // Cloudflare DNS
|
||||
]
|
||||
)
|
||||
func testSpecialAddresses(addressString: String) throws {
|
||||
let address = try IPv4Address(addressString)
|
||||
#expect(address.description == addressString)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"leading zero validation - invalid",
|
||||
arguments: [
|
||||
"01.0.0.0",
|
||||
"0.01.0.0",
|
||||
"0.0.01.0",
|
||||
"0.0.0.01",
|
||||
"192.168.001.1",
|
||||
"010.0.0.1",
|
||||
"00.0.0.1",
|
||||
]
|
||||
)
|
||||
func testLeadingZeroValidationInvalid(invalidAddress: String) {
|
||||
#expect(throws: IPAddressError.self) {
|
||||
try IPv4Address(invalidAddress)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("leading zero validation - valid single zeros")
|
||||
func testLeadingZeroValidationValid() {
|
||||
// Single "0" should be valid
|
||||
#expect(throws: Never.self) {
|
||||
try IPv4Address("0.0.0.0")
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"string length validation - too short",
|
||||
arguments: [
|
||||
"", "1", "1.2", "1.2.3", "1.2.3.",
|
||||
]
|
||||
)
|
||||
func testStringLengthValidationTooShort(shortString: String) {
|
||||
#expect(throws: IPAddressError.self) {
|
||||
try IPv4Address(shortString)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"string length validation - too long",
|
||||
arguments: [
|
||||
"255.255.255.255.1",
|
||||
"1234.168.1.1",
|
||||
"192.1234.1.1",
|
||||
"192.168.1234.1",
|
||||
"192.168.1.1234",
|
||||
]
|
||||
)
|
||||
func testStringLengthValidationTooLong(longString: String) {
|
||||
#expect(throws: IPAddressError.self) {
|
||||
try IPv4Address(longString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Performance Tests
|
||||
|
||||
@Suite("Performance")
|
||||
struct PerformanceTests {
|
||||
|
||||
@Test("parsing performance")
|
||||
func testParsingPerformance() throws {
|
||||
let testAddresses = [
|
||||
"192.168.1.1",
|
||||
"10.0.0.1",
|
||||
"172.16.0.1",
|
||||
"127.0.0.1",
|
||||
"8.8.8.8",
|
||||
"1.1.1.1",
|
||||
"255.255.255.255",
|
||||
"0.0.0.0",
|
||||
]
|
||||
|
||||
// Warm up
|
||||
for _ in 0..<100 {
|
||||
for address in testAddresses {
|
||||
_ = try IPv4Address(address)
|
||||
}
|
||||
}
|
||||
|
||||
// Measure performance
|
||||
let iterations = 10000
|
||||
let startTime = Date()
|
||||
|
||||
for _ in 0..<iterations {
|
||||
for address in testAddresses {
|
||||
_ = try IPv4Address(address)
|
||||
}
|
||||
}
|
||||
|
||||
let endTime = Date()
|
||||
let totalTime = endTime.timeIntervalSince(startTime)
|
||||
let averageTime = totalTime / Double(iterations * testAddresses.count)
|
||||
|
||||
// Should be very fast - less than 1ms per parse on average
|
||||
#expect(averageTime < 0.001, "Parsing should be fast: \(averageTime)s per address")
|
||||
}
|
||||
|
||||
@Test("bytes property performance")
|
||||
func testBytesPropertyPerformance() {
|
||||
let address = IPv4Address(0xC0A8_0101)
|
||||
|
||||
let iterations = 100000
|
||||
let startTime = Date()
|
||||
|
||||
for _ in 0..<iterations {
|
||||
_ = address.bytes
|
||||
}
|
||||
|
||||
let endTime = Date()
|
||||
let totalTime = endTime.timeIntervalSince(startTime)
|
||||
let averageTime = totalTime / Double(iterations)
|
||||
|
||||
// Should be very fast - less than 0.1ms per call on average
|
||||
#expect(averageTime < 0.0001, "Bytes property should be fast: \(averageTime)s per call")
|
||||
}
|
||||
|
||||
@Test("description property performance")
|
||||
func testDescriptionPropertyPerformance() {
|
||||
let address = IPv4Address(0xC0A8_0101)
|
||||
|
||||
let iterations = 10000
|
||||
let startTime = Date()
|
||||
|
||||
for _ in 0..<iterations {
|
||||
_ = address.description
|
||||
}
|
||||
|
||||
let endTime = Date()
|
||||
let totalTime = endTime.timeIntervalSince(startTime)
|
||||
let averageTime = totalTime / Double(iterations)
|
||||
|
||||
// Should be reasonably fast - less than 1ms per call on average
|
||||
#expect(averageTime < 0.001, "Description property should be fast: \(averageTime)s per call")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Integration Tests
|
||||
|
||||
@Suite("Integration")
|
||||
struct IntegrationTests {
|
||||
|
||||
@Test(
|
||||
"comprehensive round-trip test",
|
||||
arguments: [
|
||||
(0x0000_0000, "0.0.0.0"),
|
||||
(0x7F00_0001, "127.0.0.1"),
|
||||
(0xC0A8_0101, "192.168.1.1"),
|
||||
(0x0A00_0001, "10.0.0.1"),
|
||||
(0xAC10_0001, "172.16.0.1"),
|
||||
(0xFFFF_FFFF, "255.255.255.255"),
|
||||
(0x0808_0808, "8.8.8.8"),
|
||||
(0x0101_0101, "1.1.1.1"),
|
||||
(0x1234_5678, "18.52.86.120"),
|
||||
(0xDEAD_BEEF, "222.173.190.239"),
|
||||
]
|
||||
)
|
||||
func testComprehensiveRoundTrip(expectedValue: UInt32, expectedString: String) throws {
|
||||
// Test UInt32 -> String
|
||||
let addressFromUInt32 = IPv4Address(expectedValue)
|
||||
#expect(addressFromUInt32.description == expectedString)
|
||||
|
||||
// Test String -> UInt32
|
||||
let addressFromString = try IPv4Address(expectedString)
|
||||
#expect(addressFromString.value == expectedValue)
|
||||
|
||||
// Test equality
|
||||
#expect(addressFromUInt32 == addressFromString)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"error message consistency",
|
||||
arguments: [
|
||||
"",
|
||||
"256.1.1.1",
|
||||
"1.2.3",
|
||||
"1.2.3.4.5",
|
||||
"192.168.001.1",
|
||||
" 192.168.1.1",
|
||||
"192.168.1.1 ",
|
||||
"192.168.1.a",
|
||||
]
|
||||
)
|
||||
func testErrorMessageConsistency(invalidInput: String) {
|
||||
do {
|
||||
_ = try IPv4Address(invalidInput)
|
||||
#expect(Bool(false), "Should have thrown for input: \(invalidInput)")
|
||||
} catch let error as IPAddressError {
|
||||
#expect(error == IPAddressError.unableToParse)
|
||||
} catch {
|
||||
#expect(Bool(false), "Should have thrown IPAddressError, got: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,734 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import ContainerizationExtras
|
||||
|
||||
@Suite("IPv6Address Parsing Tests")
|
||||
struct IPv6AddressParseTests {
|
||||
|
||||
// MARK: - Valid Hexadecimal Group Tests
|
||||
|
||||
@Test(
|
||||
"Parsing valid hexadecimal groups",
|
||||
arguments: [
|
||||
("0", 0x0),
|
||||
("1", 0x1),
|
||||
("F", 0xF),
|
||||
("FF", 0xFF),
|
||||
("FFF", 0xFFF),
|
||||
("FFFF", 0xFFFF),
|
||||
("1234", 0x1234),
|
||||
("abcd", 0xABCD),
|
||||
("ABCD", 0xABCD),
|
||||
("0000", 0x0000),
|
||||
]
|
||||
)
|
||||
func testParseValidHexadecimalGroups(input: String, expectedValue: UInt16) throws {
|
||||
let utf8 = input.utf8
|
||||
let (parsedValue, nextIndex) = try IPv6Address.parseHexadecimal(
|
||||
from: utf8,
|
||||
startingAt: utf8.startIndex
|
||||
)
|
||||
|
||||
#expect(
|
||||
parsedValue == expectedValue,
|
||||
"For input '\(input)': expected \(expectedValue) but got \(parsedValue)"
|
||||
)
|
||||
#expect(nextIndex == input.endIndex, "Parser should consume entire input")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Parsing hexadecimal groups with trailing characters",
|
||||
arguments: [
|
||||
("FF:1234", 0xFF, ":1234"),
|
||||
("1234G", 0x1234, "G"),
|
||||
("AB::CD", 0xAB, "::CD"),
|
||||
("0Z", 0x0, "Z"),
|
||||
]
|
||||
)
|
||||
func testParseHexadecimalGroupWithTrailingCharacters(
|
||||
input: String,
|
||||
expectedValue: UInt16,
|
||||
expectedRemainder: String
|
||||
) throws {
|
||||
let utf8 = input.utf8
|
||||
let (parsedValue, nextIndex) = try IPv6Address.parseHexadecimal(
|
||||
from: utf8,
|
||||
startingAt: utf8.startIndex
|
||||
)
|
||||
|
||||
let remainder = String(input[String.Index(nextIndex, within: input)!...])
|
||||
|
||||
#expect(
|
||||
parsedValue == expectedValue,
|
||||
"For input '\(input)': expected \(expectedValue) but got \(parsedValue)"
|
||||
)
|
||||
#expect(
|
||||
remainder == expectedRemainder,
|
||||
"For input '\(input)': expected remainder '\(expectedRemainder)' but got '\(remainder)'"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Error Handling Tests
|
||||
|
||||
@Test(
|
||||
"Parsing invalid hexadecimal groups should throw",
|
||||
arguments: [
|
||||
"", // Empty string - no hex digits found
|
||||
"G", // Invalid hex character - no hex digits found
|
||||
"GGGG", // All invalid hex characters - no hex digits found
|
||||
]
|
||||
)
|
||||
func testParseInvalidHexadecimalGroup(invalidInput: String) {
|
||||
#expect(throws: IPAddressError.self) {
|
||||
let utf8 = invalidInput.utf8
|
||||
_ = try IPv6Address.parseHexadecimal(
|
||||
from: utf8,
|
||||
startingAt: utf8.startIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Parsing hexadecimal groups with overflow behavior",
|
||||
arguments: [
|
||||
("12345", 0x1234, "5"), // 5 digits -> takes first 4
|
||||
("10000", 0x1000, "0"), // 5 digits -> takes first 4
|
||||
("FFFFF", 0xFFFF, "F"), // 5 digits -> takes first 4
|
||||
("123456789", 0x1234, "56789"), // Many digits -> takes first 4
|
||||
]
|
||||
)
|
||||
func testParseHexadecimalGroupOverflow(
|
||||
input: String,
|
||||
expectedValue: UInt16,
|
||||
expectedRemainder: String
|
||||
) throws {
|
||||
let utf8 = input.utf8
|
||||
let (parsedValue, nextIndex) = try IPv6Address.parseHexadecimal(
|
||||
from: utf8,
|
||||
startingAt: utf8.startIndex
|
||||
)
|
||||
|
||||
let remainder = String(input[String.Index(nextIndex, within: input)!...])
|
||||
|
||||
#expect(
|
||||
parsedValue == expectedValue,
|
||||
"For input '\(input)': expected \(expectedValue) but got \(parsedValue)"
|
||||
)
|
||||
#expect(
|
||||
remainder == expectedRemainder,
|
||||
"For input '\(input)': expected remainder '\(expectedRemainder)' but got '\(remainder)'"
|
||||
)
|
||||
}
|
||||
|
||||
@Test("Parsing from middle of string")
|
||||
func testParseHexadecimalGroupFromMiddle() throws {
|
||||
let input = "prefix1234suffix"
|
||||
let utf8 = input.utf8
|
||||
let startIndex = utf8.index(utf8.startIndex, offsetBy: 6) // Start at "1234"
|
||||
|
||||
let (parsedValue, nextIndex) = try IPv6Address.parseHexadecimal(
|
||||
from: utf8,
|
||||
startingAt: startIndex
|
||||
)
|
||||
|
||||
#expect(parsedValue == 0x1234)
|
||||
|
||||
let remainder = String(input[String.Index(nextIndex, within: input)!...])
|
||||
#expect(remainder == "suffix")
|
||||
}
|
||||
|
||||
// MARK: - Performance Tests
|
||||
|
||||
@Test("Performance with maximum length hex groups")
|
||||
func testParsePerformance() throws {
|
||||
let testInput = "FFFF"
|
||||
|
||||
// Measure performance of parsing operation
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
let count = 10000
|
||||
for _ in 0..<count {
|
||||
let utf8 = testInput.utf8
|
||||
_ = try IPv6Address.parseHexadecimal(
|
||||
from: utf8,
|
||||
startingAt: utf8.startIndex
|
||||
)
|
||||
}
|
||||
|
||||
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
|
||||
|
||||
// Expect parsing to be reasonably fast (less than 1ms per operation on average)
|
||||
print("Parsed \(count) IPv6 addresses in \(timeElapsed)s")
|
||||
#expect(timeElapsed < 0.1, "Parsing should be performant: \(timeElapsed)s for 10000 operations")
|
||||
}
|
||||
|
||||
// MARK: - RFC 4291 Section 2.3 Text Representation of Address Prefixes Tests
|
||||
|
||||
@Test("RFC 4291 Section 2.3 - Valid address prefix representations")
|
||||
func testRFC4291Section23ValidPrefixRepresentations() throws {
|
||||
// Examples of valid 60-bit prefix 20010DB80000CD3 (hexadecimal)
|
||||
let validPrefixCases = [
|
||||
"2001:0DB8:0000:CD30:0000:0000:0000:0000/60",
|
||||
"2001:0DB8::CD30:0:0:0:0/60",
|
||||
"2001:0DB8:0:CD30::/60",
|
||||
]
|
||||
|
||||
let parsed = try validPrefixCases.map { testCase in
|
||||
let addressPart = String(testCase.prefix(while: { $0 != "/" }))
|
||||
return try IPv6Address.parse(addressPart).bytes
|
||||
}
|
||||
|
||||
#expect(
|
||||
parsed.allSatisfy { $0 == parsed.first },
|
||||
"All valid prefix representations should parse to identical byte arrays"
|
||||
)
|
||||
}
|
||||
|
||||
@Test("RFC 4291 Section 2.3 - Invalid address prefix representations")
|
||||
func testRFC4291Section23InvalidPrefixRepresentations() throws {
|
||||
// RFC 4291 Section 2.3 examples of invalid representations
|
||||
let invalidPrefixCases = [
|
||||
"2001:0DB8:0:CD3/60", // ex 1: may drop leading zeros, but not trailing zeros"
|
||||
"2001:0DB8::CD30/60", // ex 2: expands to wrong address
|
||||
"2001:0DB8::CD3/60", // ex 3: expands to wrong address
|
||||
]
|
||||
|
||||
let validAddress = "2001:0DB8:0000:CD30:0000:0000:0000:0000"
|
||||
let parsedValidAddress: [UInt8] = [32, 1, 13, 184, 0, 0, 205, 48, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
let parsedLibValidAddress = try IPv6Address.parse(validAddress).bytes
|
||||
#expect(parsedLibValidAddress == parsedValidAddress)
|
||||
|
||||
for testCase in invalidPrefixCases {
|
||||
let addressPart = String(testCase.prefix(while: { $0 != "/" }))
|
||||
|
||||
do {
|
||||
let actualBytes = try IPv6Address.parse(addressPart).bytes
|
||||
#expect(actualBytes != parsedValidAddress, "\(testCase) should not match valid prefix")
|
||||
} catch {
|
||||
// If parsing fails, that's also acceptable for invalid representations
|
||||
#expect(error is IPAddressError, "\(testCase) should throw IPAddressError if it fails to parse")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.3 - Leading zero rules in prefixes",
|
||||
arguments: [
|
||||
("2001:DB8:0:0CD3::", "2001:DB8:0:CD3::", "Can drop leading zeros 0CD3 -> CD3"),
|
||||
("2001:0DB8::", "2001:DB8::", "Can drop leading zeros 0DB8 -> DB8"),
|
||||
("0001:0002:0003::", "1:2:3::", "Can drop leading zeros in multiple groups"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section23LeadingZeroRules(form1: String, form2: String, description: String) throws {
|
||||
let bytes1 = try IPv6Address.parse(form1).bytes
|
||||
let bytes2 = try IPv6Address.parse(form2).bytes
|
||||
#expect(bytes1 == bytes2, "\(description)")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.3 - cannot drop trailing zeros",
|
||||
arguments: [
|
||||
("2001:DB8:0:CD30::", "2001:DB8:0:CD3::", "Cannot drop trailing zeros in CD30 -> CD3"),
|
||||
("2001:DB80::", "2001:DB8::", "Cannot drop trailing zero DB80 -> DB8"),
|
||||
("ABCD:EF00::", "ABCD:EF::", "Cannot drop trailing zeros EF00 -> EF"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section23CannotDropTrailingZeros(full: String, truncated: String, description: String) throws {
|
||||
let fullBytes = try IPv6Address.parse(full).bytes
|
||||
let truncatedBytes = try IPv6Address.parse(truncated).bytes
|
||||
#expect(fullBytes != truncatedBytes, "\(description)")
|
||||
}
|
||||
|
||||
@Test("RFC 4291 Section 2.3 - Node address and subnet prefix combination")
|
||||
func testRFC4291Section23NodeAddressSubnetCombination() throws {
|
||||
// RFC example: node address 2001:0DB8:0:CD30:123:4567:89AB:CDEF
|
||||
// and its subnet number 2001:0DB8:0:CD30::/60
|
||||
// can be abbreviated as 2001:0DB8:0:CD30:123:4567:89AB:CDEF/60
|
||||
|
||||
let nodeAddress = "2001:0DB8:0:CD30:123:4567:89AB:CDEF"
|
||||
let subnetPrefix = "2001:0DB8:0:CD30::"
|
||||
|
||||
// Both should parse successfully
|
||||
#expect(throws: Never.self, "Node address should parse successfully") {
|
||||
_ = try IPv6Address.parse(nodeAddress)
|
||||
}
|
||||
|
||||
#expect(throws: Never.self, "Subnet prefix should parse successfully") {
|
||||
_ = try IPv6Address.parse(subnetPrefix)
|
||||
}
|
||||
|
||||
// Verify that the subnet prefix is indeed a prefix of the node address
|
||||
let nodeBytes = try IPv6Address.parse(nodeAddress).bytes
|
||||
let subnetBytes = try IPv6Address.parse(subnetPrefix).bytes
|
||||
|
||||
// Validate that node address has the subnet as a prefix (60-bit prefix = 7.5 bytes)
|
||||
let prefixLength = 60
|
||||
let hasValidPrefix = nodeBytes.hasPrefix(subnetBytes, upToBits: prefixLength)
|
||||
|
||||
#expect(
|
||||
hasValidPrefix,
|
||||
"Node address should have subnet as a \(prefixLength)-bit prefix"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - RFC 4291 Section 2.2 Comprehensive Text Representation Tests
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Preferred form with all groups",
|
||||
arguments: [
|
||||
"2001:0db8:0000:0042:0000:8a2e:0370:7334",
|
||||
"ABCD:EF01:2345:6789:ABCD:EF01:2345:6789",
|
||||
"0000:0000:0000:0000:0000:0000:0000:0000",
|
||||
"FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF",
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22PreferredForm(testCase: String) throws {
|
||||
#expect(throws: Never.self, "Should parse preferred form: \(testCase)") {
|
||||
_ = try IPv6Address.parse(testCase)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Leading zero omission in all positions",
|
||||
arguments: [
|
||||
("2001:0db8:0000:0042:0000:8a2e:0370:7334", "2001:db8:0:42:0:8a2e:370:7334"),
|
||||
("0001:0002:0003:0004:0005:0006:0007:0008", "1:2:3:4:5:6:7:8"),
|
||||
("0000:0001:0002:0003:0004:0005:0006:0007", "0:1:2:3:4:5:6:7"),
|
||||
("1000:0100:0010:0001:1000:0100:0010:0001", "1000:100:10:1:1000:100:10:1"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22LeadingZeroOmission(full: String, compressed: String) throws {
|
||||
let fullBytes = try IPv6Address.parse(full).bytes
|
||||
let compressedBytes = try IPv6Address.parse(compressed).bytes
|
||||
#expect(fullBytes == compressedBytes, "'\(full)' should equal '\(compressed)'")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Zero compression at beginning",
|
||||
arguments: [
|
||||
("::", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
|
||||
("::1", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]),
|
||||
("::8a2e:370:7334", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34]),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22ZeroCompressionAtBeginning(compressed: String, expected: [UInt8]) throws {
|
||||
let parsed = try IPv6Address.parse(compressed)
|
||||
#expect(parsed.bytes == expected, "'\(compressed)' should parse correctly")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Zero compression in middle",
|
||||
arguments: [
|
||||
("2001:db8::8a2e:370:7334", "2001:db8:0:0:0:8a2e:370:7334"),
|
||||
("2001:db8::1", "2001:db8:0:0:0:0:0:1"),
|
||||
("fe80::1", "fe80:0:0:0:0:0:0:1"),
|
||||
("2001:0db8:0:0::1", "2001:db8:0:0:0:0:0:1"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22ZeroCompressionInMiddle(compressed: String, full: String) throws {
|
||||
let compressedBytes = try IPv6Address.parse(compressed).bytes
|
||||
let fullBytes = try IPv6Address.parse(full).bytes
|
||||
#expect(compressedBytes == fullBytes, "'\(compressed)' should equal '\(full)'")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Zero compression at end",
|
||||
arguments: [
|
||||
("2001:db8::", "2001:db8:0:0:0:0:0:0"),
|
||||
("2001:db8:0:0:1::", "2001:db8:0:0:1:0:0:0"),
|
||||
("fe80::", "fe80:0:0:0:0:0:0:0"),
|
||||
("1::", "1:0:0:0:0:0:0:0"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22ZeroCompressionAtEnd(compressed: String, full: String) throws {
|
||||
let compressedBytes = try IPv6Address.parse(compressed).bytes
|
||||
let fullBytes = try IPv6Address.parse(full).bytes
|
||||
#expect(compressedBytes == fullBytes, "'\(compressed)' should equal '\(full)'")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Multiple :: should fail",
|
||||
arguments: [
|
||||
"2001::db8::1",
|
||||
"::1::2",
|
||||
"fe80::1::2::3",
|
||||
"::1::",
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22MultipleDoubleColonsShouldFail(invalid: String) {
|
||||
#expect(throws: IPAddressError.self, "Multiple '::' should fail: \(invalid)") {
|
||||
_ = try IPv6Address.parse(invalid)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Case insensitivity",
|
||||
arguments: [
|
||||
("2001:db8::1", "2001:DB8::1", "2001:Db8::1"),
|
||||
("dead:beef::cafe", "DEAD:BEEF::CAFE", "DeAd:BeEf::CaFe"),
|
||||
("fe80::1", "FE80::1", "Fe80::1"),
|
||||
("abcd:ef01:2345:6789::1", "ABCD:EF01:2345:6789::1", "AbCd:Ef01:2345:6789::1"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22CaseInsensitivity(lower: String, upper: String, mixed: String) throws {
|
||||
let lowerBytes = try IPv6Address.parse(lower).bytes
|
||||
let upperBytes = try IPv6Address.parse(upper).bytes
|
||||
let mixedBytes = try IPv6Address.parse(mixed).bytes
|
||||
|
||||
#expect(lowerBytes == upperBytes, "Case should not matter: '\(lower)' vs '\(upper)'")
|
||||
#expect(lowerBytes == mixedBytes, "Case should not matter: '\(lower)' vs '\(mixed)'")
|
||||
}
|
||||
|
||||
@Test("RFC 4291 Section 2.2 - Special addresses")
|
||||
func testRFC4291Section22SpecialAddresses() throws {
|
||||
// Unspecified address
|
||||
let unspecified = try IPv6Address.parse("::")
|
||||
#expect(unspecified.bytes.allSatisfy { $0 == 0 }, ":: should be all zeros")
|
||||
|
||||
// Loopback address
|
||||
let loopback = try IPv6Address.parse("::1")
|
||||
let expectedLoopback: [UInt8] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
|
||||
#expect(loopback.bytes == expectedLoopback, "::1 should be loopback address")
|
||||
|
||||
// IPv4-compatible (deprecated but valid syntax)
|
||||
let ipv4Compat = try IPv6Address.parse("::c000:0201")
|
||||
let expectedCompat: [UInt8] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xc0, 0x00, 0x02, 0x01]
|
||||
#expect(ipv4Compat.bytes == expectedCompat, "::c000:0201 should parse correctly")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Invalid formats should fail",
|
||||
arguments: [
|
||||
"2001:db8", // Too few groups without ::
|
||||
"2001:db8:1:2:3:4:5:6:7", // Too many groups
|
||||
"2001:db8:1:2:3:4:5:6:7:8:9", // Too many groups
|
||||
"2001:db8:::1", // Triple colon
|
||||
"2001:db8::1::2", // Multiple ::
|
||||
"gggg::1", // Invalid hex character
|
||||
"2001:db8:xyz::1", // Invalid hex character
|
||||
"::ffff:", // Trailing colon
|
||||
":2001:db8::1", // Leading single colon
|
||||
"2001:db8::1:", // Trailing single colon
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22InvalidFormatsShouldFail(invalid: String) {
|
||||
#expect(throws: IPAddressError.self, "Invalid format should fail: \(invalid)") {
|
||||
_ = try IPv6Address.parse(invalid)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("RFC 4291 Section 2.2 - Maximum values")
|
||||
func testRFC4291Section22MaximumValues() throws {
|
||||
// All FFs - maximum value
|
||||
let maxAddress = try IPv6Address.parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")
|
||||
#expect(maxAddress.bytes.allSatisfy { $0 == 0xFF }, "All groups should be 0xFFFF")
|
||||
|
||||
// Mix of max and min values
|
||||
let mixedMax = try IPv6Address.parse("ffff:0:ffff:0:ffff:0:ffff:0")
|
||||
let expectedMixed: [UInt8] = [0xff, 0xff, 0, 0, 0xff, 0xff, 0, 0, 0xff, 0xff, 0, 0, 0xff, 0xff, 0, 0]
|
||||
#expect(mixedMax.bytes == expectedMixed, "Should alternate between max and zero")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Single zero groups",
|
||||
arguments: [
|
||||
("2001:db8:0:1:2:3:4:5", "2001:db8:0:1:2:3:4:5"), // Single 0, no compression needed
|
||||
("2001:db8:0:0:1:2:3:4", "2001:db8::1:2:3:4"), // Two zeros can be compressed
|
||||
("0:0:0:0:0:0:0:1", "::1"), // All zeros except last
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22SingleZeroGroups(withZero: String, withCompression: String) throws {
|
||||
let zeroBytes = try IPv6Address.parse(withZero).bytes
|
||||
let compressedBytes = try IPv6Address.parse(withCompression).bytes
|
||||
#expect(zeroBytes == compressedBytes, "'\(withZero)' should equal '\(withCompression)'")
|
||||
}
|
||||
|
||||
@Test("RFC 4291 Section 2.2 - Boundary conditions")
|
||||
func testRFC4291Section22BoundaryConditions() throws {
|
||||
// Single non-zero value in each position
|
||||
for position in 0..<8 {
|
||||
var groups = [String](repeating: "0", count: 8)
|
||||
groups[position] = "1"
|
||||
let address = groups.joined(separator: ":")
|
||||
|
||||
#expect(throws: Never.self, "Single non-zero at position \(position) should parse") {
|
||||
_ = try IPv6Address.parse(address)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the bytes are correct
|
||||
let firstPosition = try IPv6Address.parse("1:0:0:0:0:0:0:0")
|
||||
#expect(firstPosition.bytes[0] == 0 && firstPosition.bytes[1] == 1, "First group should be 0x0001")
|
||||
|
||||
let lastPosition = try IPv6Address.parse("0:0:0:0:0:0:0:1")
|
||||
#expect(lastPosition.bytes[14] == 0 && lastPosition.bytes[15] == 1, "Last group should be 0x0001")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Hex digit limits",
|
||||
arguments: [
|
||||
("1:2:3:4:5:6:7:8", "1-digit groups"),
|
||||
("12:34:56:78:9a:bc:de:f0", "2-digit groups"),
|
||||
("123:456:789:abc:def:123:456:789", "3-digit groups"),
|
||||
("1234:5678:9abc:def0:1234:5678:9abc:def0", "4-digit groups"),
|
||||
("1:12:123:1234:1:12:123:1234", "Mixed digit counts"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22HexDigitLimits(address: String, description: String) throws {
|
||||
#expect(throws: Never.self, "Should parse \(description): \(address)") {
|
||||
_ = try IPv6Address.parse(address)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("RFC 4291 Section 2.2 - Equivalence of different representations")
|
||||
func testRFC4291Section22EquivalenceOfRepresentations() throws {
|
||||
let equivalentGroups: [[String]] = [
|
||||
// Same address, different representations
|
||||
[
|
||||
"2001:0db8:0000:0000:0000:0000:0000:0001",
|
||||
"2001:db8:0:0:0:0:0:1",
|
||||
"2001:db8::1",
|
||||
"2001:0DB8::1",
|
||||
"2001:0DB8:0000:0000:0000:0000:0000:0001",
|
||||
],
|
||||
[
|
||||
"fe80:0000:0000:0000:0000:0000:0000:0001",
|
||||
"fe80::1",
|
||||
"FE80::1",
|
||||
"fe80:0:0:0:0:0:0:1",
|
||||
],
|
||||
[
|
||||
"0000:0000:0000:0000:0000:0000:0000:0000",
|
||||
"::",
|
||||
"0:0:0:0:0:0:0:0",
|
||||
],
|
||||
]
|
||||
|
||||
for group in equivalentGroups {
|
||||
let bytesArray = try group.map { try IPv6Address.parse($0).bytes }
|
||||
let firstBytes = bytesArray[0]
|
||||
|
||||
for (index, bytes) in bytesArray.enumerated() {
|
||||
#expect(bytes == firstBytes, "All forms should be equivalent: \(group[index])")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Zero compression selection (longest run)",
|
||||
arguments: [
|
||||
("2001:db8:0:0:1:0:0:1", "Two runs of 2 zeros each"),
|
||||
("2001:0:0:0:db8:0:0:1", "Run of 3 and run of 2"),
|
||||
("2001:db8:0:0:0:0:1:1", "Run of 4 zeros in middle"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22ZeroCompressionLongestRun(address: String, description: String) throws {
|
||||
#expect(throws: Never.self, "Should parse \(description): \(address)") {
|
||||
_ = try IPv6Address.parse(address)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Edge case with :: at different positions",
|
||||
arguments: [
|
||||
("::1", "0:0:0:0:0:0:0:1"),
|
||||
("1::", "1:0:0:0:0:0:0:0"),
|
||||
("1::1", "1:0:0:0:0:0:0:1"),
|
||||
("1:2::1", "1:2:0:0:0:0:0:1"),
|
||||
("1::2:3", "1:0:0:0:0:0:2:3"),
|
||||
("1:2:3::4:5:6", "1:2:3:0:0:4:5:6"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22DoubleColonAtDifferentPositions(compressed: String, expanded: String) throws {
|
||||
let compressedBytes = try IPv6Address.parse(compressed).bytes
|
||||
let expandedBytes = try IPv6Address.parse(expanded).bytes
|
||||
#expect(compressedBytes == expandedBytes, "'\(compressed)' should equal '\(expanded)'")
|
||||
}
|
||||
|
||||
// MARK: - RFC 4291 Section 2.2 IPv4 Mixed Notation Tests
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - IPv4 mixed notation basic formats",
|
||||
arguments: [
|
||||
// RFC 4291 examples
|
||||
("0:0:0:0:0:0:13.1.68.3", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 1, 68, 3]),
|
||||
("::13.1.68.3", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 1, 68, 3]),
|
||||
|
||||
// IPv4-mapped IPv6 address (::ffff:x.x.x.x)
|
||||
("::ffff:129.144.52.38", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 129, 144, 52, 38]),
|
||||
("0:0:0:0:0:ffff:129.144.52.38", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 129, 144, 52, 38]),
|
||||
|
||||
// IPv4-compatible IPv6 address (deprecated but valid syntax)
|
||||
("::192.168.1.1", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 1]),
|
||||
("::0.0.0.1", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]),
|
||||
|
||||
// Various valid positions
|
||||
("2001:db8::192.0.2.1", [0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 192, 0, 2, 1]),
|
||||
("fe80::192.168.1.1", [0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 1]),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22IPv4MixedNotation(address: String, expected: [UInt8]) throws {
|
||||
let parsed = try IPv6Address.parse(address)
|
||||
#expect(parsed.bytes == expected, "IPv4 mixed notation '\(address)' should parse correctly")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - IPv4 mixed notation with full IPv6 prefix",
|
||||
arguments: [
|
||||
("0:0:0:0:0:0:192.168.1.1", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 1]),
|
||||
("2001:db8:0:0:0:0:192.0.2.1", [0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 192, 0, 2, 1]),
|
||||
("64:ff9b::192.0.2.33", [0, 0x64, 0xff, 0x9b, 0, 0, 0, 0, 0, 0, 0, 0, 192, 0, 2, 33]),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22IPv4MixedNotationFullPrefix(address: String, expected: [UInt8]) throws {
|
||||
let parsed = try IPv6Address.parse(address)
|
||||
#expect(parsed.bytes == expected, "Full prefix with IPv4 '\(address)' should parse correctly")
|
||||
}
|
||||
|
||||
@Test("RFC 4291 Section 2.2 - IPv4 mixed notation edge cases")
|
||||
func testRFC4291Section22IPv4MixedNotationEdgeCases() throws {
|
||||
// Maximum values
|
||||
let maxIPv4 = try IPv6Address.parse("::255.255.255.255")
|
||||
let expectedMax: [UInt8] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255]
|
||||
#expect(maxIPv4.bytes == expectedMax, "Maximum IPv4 values should work")
|
||||
|
||||
// Minimum values
|
||||
let minIPv4 = try IPv6Address.parse("::0.0.0.0")
|
||||
let expectedMin: [UInt8] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
#expect(minIPv4.bytes == expectedMin, "Minimum IPv4 values should work")
|
||||
|
||||
// With non-zero prefix
|
||||
let withPrefix = try IPv6Address.parse("2001:db8:85a3::8a2e:255.255.255.255")
|
||||
let expectedPrefix: [UInt8] = [0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0, 0, 0, 0, 0x8a, 0x2e, 255, 255, 255, 255]
|
||||
#expect(withPrefix.bytes == expectedPrefix, "IPv4 with hex prefix should work")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - IPv4 mixed notation invalid formats",
|
||||
arguments: [
|
||||
"::192.168.1", // Incomplete IPv4
|
||||
"::192.168.1.1.1", // Too many IPv4 octets
|
||||
"::256.1.1.1", // IPv4 octet out of range
|
||||
"::192.168.001.1", // Leading zeros in IPv4
|
||||
"::192.168.-1.1", // Negative in IPv4
|
||||
"::192.168.1.a", // Non-numeric in IPv4
|
||||
"2001:db8:1:2:3:4:5:192.168.1.1", // Too many hex groups before IPv4
|
||||
"::192.168.1.1:1234", // Extra hex after IPv4
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22IPv4MixedNotationInvalid(invalid: String) {
|
||||
#expect(throws: IPAddressError.self, "Invalid IPv4 mixed notation should fail: \(invalid)") {
|
||||
_ = try IPv6Address.parse(invalid)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("RFC 4291 Section 2.2 - IPv4 mixed notation equivalence")
|
||||
func testRFC4291Section22IPv4MixedNotationEquivalence() throws {
|
||||
// These should all represent the same address
|
||||
let equivalentForms = [
|
||||
"0:0:0:0:0:0:192.0.2.1",
|
||||
"::192.0.2.1",
|
||||
"::c000:201", // Same as 192.0.2.1 in hex
|
||||
]
|
||||
|
||||
let bytesArray = try equivalentForms.map { try IPv6Address.parse($0).bytes }
|
||||
let firstBytes = bytesArray[0]
|
||||
|
||||
for (index, bytes) in bytesArray.enumerated() {
|
||||
#expect(bytes == firstBytes, "All forms should be equivalent: \(equivalentForms[index])")
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - IPv4-mapped IPv6 addresses",
|
||||
arguments: [
|
||||
("127.0.0.1", "::ffff:127.0.0.1"),
|
||||
("192.168.1.1", "::ffff:192.168.1.1"),
|
||||
("8.8.8.8", "::ffff:8.8.8.8"),
|
||||
("0.0.0.0", "::ffff:0.0.0.0"),
|
||||
("255.255.255.255", "::ffff:255.255.255.255"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22IPv4MappedAddresses(ipv4: String, ipv6: String) throws {
|
||||
let parsed = try IPv6Address.parse(ipv6)
|
||||
|
||||
// First 10 bytes should be 0
|
||||
#expect(parsed.bytes[0..<10].allSatisfy { $0 == 0 }, "First 10 bytes should be zero")
|
||||
|
||||
// Next 2 bytes should be 0xff
|
||||
#expect(parsed.bytes[10] == 0xff && parsed.bytes[11] == 0xff, "Bytes 10-11 should be 0xffff")
|
||||
|
||||
// Last 4 bytes should match IPv4 address
|
||||
let ipv4Parsed = try IPv4Address.parse(ipv4)
|
||||
let ipv4Bytes = [
|
||||
UInt8((ipv4Parsed >> 24) & 0xFF),
|
||||
UInt8((ipv4Parsed >> 16) & 0xFF),
|
||||
UInt8((ipv4Parsed >> 8) & 0xFF),
|
||||
UInt8(ipv4Parsed & 0xFF),
|
||||
]
|
||||
#expect(Array(parsed.bytes[12..<16]) == ipv4Bytes, "Last 4 bytes should match IPv4")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - IPv4 mixed notation with zone identifier",
|
||||
arguments: [
|
||||
"::ffff:192.168.1.1%eth0",
|
||||
"fe80::192.168.1.1%lo0",
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22IPv4MixedNotationWithZone(testCase: String) throws {
|
||||
#expect(throws: Never.self, "IPv4 mixed notation with zone should parse: \(testCase)") {
|
||||
let parsed = try IPv6Address.parse(testCase)
|
||||
#expect(parsed.zone != nil, "Zone should be preserved")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Array Extension for Prefix Validation
|
||||
|
||||
extension Array where Element == UInt8 {
|
||||
/// Checks if this byte array has another array as a prefix up to the specified number of bits
|
||||
/// - Parameters:
|
||||
/// - prefix: The potential prefix array
|
||||
/// - bits: Number of bits to compare (0-128 for IPv6)
|
||||
/// - Returns: true if the prefix matches for the specified number of bits
|
||||
func hasPrefix(_ prefix: [UInt8], upToBits bits: Int) -> Bool {
|
||||
guard self.count >= 16 && prefix.count >= 16 else { return false }
|
||||
guard bits >= 0 && bits <= 128 else { return false }
|
||||
|
||||
let fullBytes = bits / 8
|
||||
let remainingBits = bits % 8
|
||||
|
||||
// Compare full bytes
|
||||
for i in 0..<fullBytes {
|
||||
if self[i] != prefix[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Compare partial byte if needed
|
||||
if remainingBits > 0 && fullBytes < 16 {
|
||||
let shiftAmount = 8 - remainingBits
|
||||
let mask: UInt8 = shiftAmount >= 8 ? 0 : (0xFF << shiftAmount)
|
||||
return (self[fullBytes] & mask) == (prefix[fullBytes] & mask)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import ContainerizationExtras
|
||||
|
||||
@Suite("IPv6 Address Tests")
|
||||
struct IPv6AddressTests {
|
||||
|
||||
// MARK: - String Representation Tests (RFC 5952)
|
||||
|
||||
@Test(
|
||||
"IPv6 address string representation - RFC 5952",
|
||||
arguments: [
|
||||
// Zero compression algorithm tests
|
||||
("0:0:0:0:0:0:0:0", "::", "all zeros - unspecified address"),
|
||||
("0:0:0:0:0:0:0:1", "::1", "leading zeros - loopback"),
|
||||
("2001:0db8:0:0:0:0:0:0", "2001:db8::", "trailing zeros"),
|
||||
("2001:0:0:0:0:0:0db8:1", "2001::db8:1", "middle zeros - longest run"),
|
||||
("2001:0:0:0:0:0db8:0:1", "2001::db8:0:1", "multiple runs - prefer longest"),
|
||||
("2001:0:0db8:0:1:0:0:2", "2001:0:db8:0:1::2", "tie-breaking - first occurrence wins"),
|
||||
("2001:0:0db8:1:2:3:4:5", "2001:0:db8:1:2:3:4:5", "single zero - no compression (min 2 required)"),
|
||||
("2001:0db8:0:0:1:2:3:4", "2001:db8::1:2:3:4", "exactly 2 zeros - minimum for compression"),
|
||||
("0:0:0:0:1234:0:0:0", "::1234:0:0:0", "tie-breaking - first run wins (4 vs 3 zeros)"),
|
||||
|
||||
// RFC 5952 formatting rules
|
||||
(
|
||||
"ABCD:EF01:2345:6789:9ABC:DEF0:1122:3344", "abcd:ef01:2345:6789:9abc:def0:1122:3344",
|
||||
"lowercase hex (Section 4.3)"
|
||||
),
|
||||
("0001:0002:0003:0004:0005:0006:0007:0008", "1:2:3:4:5:6:7:8", "no leading zeros (Section 4.1)"),
|
||||
|
||||
// Edge cases
|
||||
("2001:a:a:a:0:0db8:0:1", "2001:a:a:a:0:db8:0:1", "only single zeros scattered - no compression"),
|
||||
]
|
||||
)
|
||||
func testIPv6StringRepresentation(input: String, expected: String, description: String) throws {
|
||||
let addr = try IPv6Address.parse(input)
|
||||
#expect(
|
||||
addr.description == expected,
|
||||
"Expected '\(expected)' but got '\(addr.description)' for input: '\(input)' (\(description))"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - isUnspecified Tests
|
||||
|
||||
@Test(
|
||||
"isUnspecified - RFC 4291 Section 2.5.2",
|
||||
arguments: [
|
||||
("::", true, "unspecified address (short form)"),
|
||||
("0:0:0:0:0:0:0:0", true, "unspecified address (full form)"),
|
||||
(IPv6Address.unspecified.description, true, "unspecified"),
|
||||
("::1", false, "loopback"),
|
||||
("0:0:0:0:0:0:0:1", false, "loopback (full form)"),
|
||||
("fe80::1", false, "link-local"),
|
||||
("2001:db8::1", false, "global unicast"),
|
||||
]
|
||||
)
|
||||
func testIsUnspecified(address: String, expected: Bool, description: String) throws {
|
||||
let addr = try IPv6Address.parse(address)
|
||||
#expect(
|
||||
addr.isUnspecified == expected,
|
||||
"Address \(address) (\(description)) should\(expected ? "" : " not") be unspecified"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - isLoopback Tests
|
||||
|
||||
@Test(
|
||||
"isLoopback - RFC 4291 Section 2.5.3",
|
||||
arguments: [
|
||||
("::1", true, "loopback (short form)"),
|
||||
("0:0:0:0:0:0:0:1", true, "loopback (full form)"),
|
||||
(IPv6Address.loopback.description, true, "loopback var"),
|
||||
("::", false, "unspecified"),
|
||||
("::2", false, "not loopback"),
|
||||
("0:0:0:0:0:0:0:2", false, "not loopback (full form)"),
|
||||
("fe80::1", false, "link-local"),
|
||||
("2001:db8::1", false, "global unicast"),
|
||||
]
|
||||
)
|
||||
func testIsLoopback(addressString: String, expected: Bool, description: String) throws {
|
||||
let addr = try IPv6Address.parse(addressString)
|
||||
#expect(
|
||||
addr.isLoopback == expected,
|
||||
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be loopback"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - isMulticast Tests
|
||||
|
||||
@Test(
|
||||
"isMulticast - RFC 4291 Section 2.7",
|
||||
arguments: [
|
||||
// Positive cases - all multicast addresses start with ff
|
||||
("ff00::1", true, "Reserved multicast"),
|
||||
("ff01::1", true, "Interface-local multicast"),
|
||||
("ff02::1", true, "Link-local multicast (all nodes)"),
|
||||
("ff02::2", true, "Link-local multicast (all routers)"),
|
||||
("ff05::1", true, "Site-local multicast"),
|
||||
("ff0e::1", true, "Global multicast"),
|
||||
("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true, "Max multicast"),
|
||||
// Negative cases
|
||||
("::", false, "unspecified"),
|
||||
("::1", false, "loopback"),
|
||||
("fe80::1", false, "link-local unicast"),
|
||||
("2001:db8::1", false, "global unicast"),
|
||||
("fd00::1", false, "unique local"),
|
||||
]
|
||||
)
|
||||
func testIsMulticast(addressString: String, expected: Bool, description: String) throws {
|
||||
let addr = try IPv6Address.parse(addressString)
|
||||
#expect(
|
||||
addr.isMulticast == expected,
|
||||
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be multicast"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - isLinkLocal Tests
|
||||
|
||||
@Test(
|
||||
"isLinkLocal - RFC 4291 Section 2.5.6",
|
||||
arguments: [
|
||||
// Positive cases - fe80::/10
|
||||
("fe80::1", true, "basic link-local"),
|
||||
("fe80::dead:beef", true, "link-local with hex"),
|
||||
("fe80:0:0:0:0:0:0:1", true, "link-local (full form)"),
|
||||
("fe80::1234:5678:90ab:cdef", true, "link-local with interface ID"),
|
||||
("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true, "Last address in fe80::/10"),
|
||||
// Negative cases
|
||||
("::", false, "unspecified"),
|
||||
("::1", false, "loopback"),
|
||||
("fec0::1", false, "site-local (deprecated)"),
|
||||
("ff02::1", false, "multicast"),
|
||||
("2001:db8::1", false, "global unicast"),
|
||||
("fd00::1", false, "unique local"),
|
||||
]
|
||||
)
|
||||
func testIsLinkLocal(addressString: String, expected: Bool, description: String) throws {
|
||||
let addr = try IPv6Address.parse(addressString)
|
||||
#expect(
|
||||
addr.isLinkLocal == expected,
|
||||
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be link-local"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - isUniqueLocal Tests
|
||||
|
||||
@Test(
|
||||
"isUniqueLocal - RFC 4193",
|
||||
arguments: [
|
||||
// Positive cases - fc00::/7 (fc00::/8 and fd00::/8)
|
||||
("fc00::1", true, "fc00 unique local"),
|
||||
("fc00:dead:beef::1", true, "fc00 with hex"),
|
||||
("fd00::1", true, "fd00 unique local"),
|
||||
("fd12:3456:789a::1", true, "fd00 with prefix"),
|
||||
("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true, "max unique local"),
|
||||
// Negative cases
|
||||
("::", false, "unspecified"),
|
||||
("::1", false, "loopback"),
|
||||
("fe80::1", false, "link-local"),
|
||||
("ff02::1", false, "multicast"),
|
||||
("2001:db8::1", false, "global unicast"),
|
||||
]
|
||||
)
|
||||
func testIsUniqueLocal(addressString: String, expected: Bool, description: String) throws {
|
||||
let addr = try IPv6Address.parse(addressString)
|
||||
#expect(
|
||||
addr.isUniqueLocal == expected,
|
||||
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be unique local"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - isGlobalUnicast Tests
|
||||
|
||||
@Test(
|
||||
"isGlobalUnicast - RFC 4291 Section 2.5.4",
|
||||
arguments: [
|
||||
// Positive cases - routable on the global internet
|
||||
("2001:db8::1", true, "Documentation (but still global unicast format)"),
|
||||
("2001:4860:4860::8888", true, "Google DNS"),
|
||||
("2606:4700:4700::1111", true, "Cloudflare DNS"),
|
||||
("2001:500::1", true, "Root DNS server"),
|
||||
("2a00:1450:4001::1", true, "Google"),
|
||||
// Negative cases - special addresses
|
||||
("::", false, "unspecified"),
|
||||
("::1", false, "loopback"),
|
||||
("fe80::1", false, "link-local"),
|
||||
("ff02::1", false, "multicast"),
|
||||
("fc00::1", false, "unique local"),
|
||||
("fd00::1", false, "unique local"),
|
||||
]
|
||||
)
|
||||
func testIsGlobalUnicast(addressString: String, expected: Bool, description: String) throws {
|
||||
let addr = try IPv6Address.parse(addressString)
|
||||
#expect(
|
||||
addr.isGlobalUnicast == expected,
|
||||
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be global unicast"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - isDocumentation Tests
|
||||
|
||||
@Test(
|
||||
"isDocumentation - RFC 3849",
|
||||
arguments: [
|
||||
// Positive cases - 2001:db8::/32
|
||||
("2001:db8::1", true, "basic documentation address"),
|
||||
("2001:db8::", true, "documentation prefix"),
|
||||
("2001:db8:0:0:0:0:0:1", true, "documentation (full form)"),
|
||||
("2001:db8:1234:5678:90ab:cdef:1234:5678", true, "documentation with all fields"),
|
||||
("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff", true, "max documentation address"),
|
||||
// Negative cases
|
||||
("2001:db7::1", false, "Just before documentation range"),
|
||||
("2001:db9::1", false, "Just after documentation range"),
|
||||
("2001:4860:4860::8888", false, "Google DNS"),
|
||||
("::", false, "unspecified"),
|
||||
("::1", false, "loopback"),
|
||||
]
|
||||
)
|
||||
func testIsDocumentation(addressString: String, expected: Bool, description: String) throws {
|
||||
let addr = try IPv6Address.parse(addressString)
|
||||
#expect(
|
||||
addr.isDocumentation == expected,
|
||||
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be documentation"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import ContainerizationExtras
|
||||
|
||||
@Suite("IPv6 Mixed IPv4 Notation Tests")
|
||||
struct IPv6IPv4ParsingTests {
|
||||
|
||||
@Test(
|
||||
"Extract IPv4 suffix from various IPv6 formats",
|
||||
arguments: [
|
||||
("::192.168.1.1", "::", [UInt8(192), UInt8(168), UInt8(1), UInt8(1)]),
|
||||
("::ffff:192.0.2.1", "::ffff", [UInt8(192), UInt8(0), UInt8(2), UInt8(1)]),
|
||||
("fe80::192.168.1.1", "fe80::", [UInt8(192), UInt8(168), UInt8(1), UInt8(1)]),
|
||||
("2001:db8::192.0.2.1", "2001:db8::", [UInt8(192), UInt8(0), UInt8(2), UInt8(1)]),
|
||||
("0:0:0:0:0:0:192.168.1.1", "0:0:0:0:0:0", [UInt8(192), UInt8(168), UInt8(1), UInt8(1)]),
|
||||
]
|
||||
)
|
||||
func testIPv4SuffixExtraction(input: String, expectedIPv6: String, expectedIPv4: [UInt8]) throws {
|
||||
let result = try #require(try IPv6Address.extractIPv4Suffix(from: input))
|
||||
#expect(result.0 == expectedIPv6)
|
||||
#expect(result.1 == expectedIPv4)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"No IPv4 suffix for pure IPv6 addresses",
|
||||
arguments: [
|
||||
"2001:db8::1",
|
||||
"fe80::1",
|
||||
"::",
|
||||
"::1",
|
||||
]
|
||||
)
|
||||
func testPureIPv6ReturnsNil(address: String) throws {
|
||||
#expect(try IPv6Address.extractIPv4Suffix(from: address) == nil)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Invalid IPv4 suffix throws error",
|
||||
arguments: [
|
||||
"::256.1.1.1",
|
||||
"::192.168.1",
|
||||
"::192.168.001.1",
|
||||
]
|
||||
)
|
||||
func testInvalidIPv4Throws(invalid: String) {
|
||||
#expect(throws: IPAddressError.self) {
|
||||
_ = try IPv6Address.extractIPv4Suffix(from: invalid)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"IPv4 bytes always at positions 12-15",
|
||||
arguments: [
|
||||
"::192.168.1.1",
|
||||
"::ffff:127.0.0.1",
|
||||
"fe80::10.0.0.1",
|
||||
]
|
||||
)
|
||||
func testIPv4BytePlacement(address: String) throws {
|
||||
let parsed = try IPv6Address.parse(address)
|
||||
let ipv4String = String(address.split(separator: ":").last!)
|
||||
let ipv4 = try IPv4Address.parse(ipv4String)
|
||||
|
||||
#expect(parsed.bytes[12] == UInt8((ipv4 >> 24) & 0xFF))
|
||||
#expect(parsed.bytes[13] == UInt8((ipv4 >> 16) & 0xFF))
|
||||
#expect(parsed.bytes[14] == UInt8((ipv4 >> 8) & 0xFF))
|
||||
#expect(parsed.bytes[15] == UInt8(ipv4 & 0xFF))
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Unspecified address with IPv4 suffix",
|
||||
arguments: [
|
||||
("::192.168.1.1", [UInt8(192), UInt8(168), UInt8(1), UInt8(1)]),
|
||||
("::0.0.0.1", [UInt8(0), UInt8(0), UInt8(0), UInt8(1)]),
|
||||
("::255.255.255.255", [UInt8(255), UInt8(255), UInt8(255), UInt8(255)]),
|
||||
]
|
||||
)
|
||||
func testUnspecifiedWithIPv4(address: String, ipv4: [UInt8]) throws {
|
||||
let parsed = try IPv6Address.parse(address)
|
||||
#expect(parsed.bytes[0..<12].allSatisfy { $0 == 0 })
|
||||
#expect(Array(parsed.bytes[12..<16]) == ipv4)
|
||||
}
|
||||
|
||||
@Test("IPv4 with zone identifier")
|
||||
func testIPv4WithZone() throws {
|
||||
let parsed = try IPv6Address.parse("::192.168.1.1%lo0")
|
||||
|
||||
#expect(parsed.zone == "lo0")
|
||||
#expect(Array(parsed.bytes[12..<16]) == [192, 168, 1, 1])
|
||||
}
|
||||
|
||||
@Test(
|
||||
"IPv4-mapped addresses (::ffff:x.x.x.x)",
|
||||
arguments: [
|
||||
"::ffff:127.0.0.1",
|
||||
"::ffff:192.168.1.1",
|
||||
]
|
||||
)
|
||||
func testIPv4MappedAddresses(address: String) throws {
|
||||
let parsed = try IPv6Address.parse(address)
|
||||
|
||||
#expect(parsed.bytes[0..<10].allSatisfy { $0 == 0 })
|
||||
#expect(parsed.bytes[10] == 0xff && parsed.bytes[11] == 0xff)
|
||||
}
|
||||
|
||||
@Test("Complex ellipsis with IPv4")
|
||||
func testComplexEllipsisWithIPv4() throws {
|
||||
let address = "2001:db8:85a3::8a2e:192.168.1.1"
|
||||
let parsed = try IPv6Address.parse(address)
|
||||
|
||||
#expect(parsed.bytes[10] == 0x8a && parsed.bytes[11] == 0x2e)
|
||||
#expect(Array(parsed.bytes[12..<16]) == [192, 168, 1, 1])
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ final class TestAddressAllocators {
|
||||
@Test
|
||||
func testipv4AddressAllocator() throws {
|
||||
var allocations = Set<UInt32>()
|
||||
let lower = try IPv4Address("192.168.64.1").prefix(prefixLength: 24).value
|
||||
let lower = try IPv4Address("192.168.64.1").value & Prefix(length: 24)!.prefixMask32
|
||||
let allocator = try IPv4Address.allocator(
|
||||
lower: lower, size: 3)
|
||||
allocations.insert(try allocator.allocate().value)
|
||||
@@ -117,7 +117,7 @@ final class TestAddressAllocators {
|
||||
@Test
|
||||
func testHighestIPv4AddressAllocator() throws {
|
||||
var allocations = Set<UInt32>()
|
||||
let lower = try IPv4Address("255.255.255.255").prefix(prefixLength: 32).value
|
||||
let lower = try IPv4Address("255.255.255.255").value & Prefix(length: 32)!.prefixMask32
|
||||
let allocator = try IPv4Address.allocator(
|
||||
lower: lower, size: 1)
|
||||
allocations.insert(try allocator.allocate().value)
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Testing
|
||||
|
||||
@testable import ContainerizationExtras
|
||||
|
||||
struct TestPrefix {
|
||||
|
||||
// MARK: - IPv4 Mask Tests
|
||||
|
||||
struct IPv4Mask {
|
||||
let length: UInt8
|
||||
let expectedPrefix: UInt32
|
||||
let expectedSuffix: UInt32
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
IPv4Mask(
|
||||
length: 0,
|
||||
expectedPrefix: 0x0000_0000,
|
||||
expectedSuffix: 0xFFFF_FFFF
|
||||
),
|
||||
IPv4Mask(
|
||||
length: 8,
|
||||
expectedPrefix: 0xFF00_0000,
|
||||
expectedSuffix: 0x00FF_FFFF
|
||||
),
|
||||
IPv4Mask(
|
||||
length: 16,
|
||||
expectedPrefix: 0xFFFF_0000,
|
||||
expectedSuffix: 0x0000_FFFF
|
||||
),
|
||||
IPv4Mask(
|
||||
length: 24,
|
||||
expectedPrefix: 0xFFFF_FF00,
|
||||
expectedSuffix: 0x0000_00FF
|
||||
),
|
||||
IPv4Mask(
|
||||
length: 32,
|
||||
expectedPrefix: 0xFFFF_FFFF,
|
||||
expectedSuffix: 0x0000_0000
|
||||
),
|
||||
])
|
||||
func testIPv4Masks(testCase: IPv4Mask) {
|
||||
let prefix = Prefix(length: testCase.length)!
|
||||
#expect(prefix.prefixMask32 == testCase.expectedPrefix)
|
||||
#expect(prefix.suffixMask32 == testCase.expectedSuffix)
|
||||
}
|
||||
|
||||
@Test func testMasksAreInverses32() {
|
||||
for length in 0...32 {
|
||||
let prefix = Prefix(length: UInt8(length))!
|
||||
#expect(~prefix.prefixMask32 == prefix.suffixMask32)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - IPv6 Mask Tests
|
||||
|
||||
struct IPv6Mask {
|
||||
let length: UInt8
|
||||
let expectedPrefix: UInt128
|
||||
let expectedSuffix: UInt128
|
||||
}
|
||||
|
||||
@Test func testIPv6Masks() {
|
||||
let cases = [
|
||||
IPv6Mask(
|
||||
length: 0,
|
||||
expectedPrefix: UInt128(0),
|
||||
expectedSuffix: UInt128.max
|
||||
),
|
||||
IPv6Mask(
|
||||
length: 64,
|
||||
expectedPrefix: 0xFFFF_FFFF_FFFF_FFFF_0000_0000_0000_0000,
|
||||
expectedSuffix: 0x0000_0000_0000_0000_FFFF_FFFF_FFFF_FFFF
|
||||
),
|
||||
IPv6Mask(
|
||||
length: 128,
|
||||
expectedPrefix: UInt128.max,
|
||||
expectedSuffix: UInt128(0)
|
||||
),
|
||||
]
|
||||
for testCase in cases {
|
||||
let prefix = Prefix(length: testCase.length)!
|
||||
#expect(prefix.prefixMask128 == testCase.expectedPrefix)
|
||||
#expect(prefix.suffixMask128 == testCase.expectedSuffix)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testMasksAreInverses128() {
|
||||
for length in stride(from: 0, through: 128, by: 8) {
|
||||
let prefix = Prefix(length: UInt8(length))!
|
||||
#expect(~prefix.prefixMask128 == prefix.suffixMask128)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Description Tests
|
||||
|
||||
@Test func testDescription() {
|
||||
#expect(Prefix(length: 24)!.description == "24")
|
||||
#expect(Prefix(length: 0)!.description == "0")
|
||||
#expect(Prefix(length: 128)!.description == "128")
|
||||
}
|
||||
|
||||
// MARK: - Validation Tests
|
||||
|
||||
@Test func testValidationRejectsInvalidLengths() {
|
||||
// Valid ranges
|
||||
#expect(Prefix(length: 0) != nil)
|
||||
#expect(Prefix(length: 32) != nil)
|
||||
#expect(Prefix(length: 128) != nil)
|
||||
|
||||
// Invalid ranges
|
||||
#expect(Prefix(length: 129) == nil)
|
||||
#expect(Prefix(length: 200) == nil)
|
||||
#expect(Prefix(length: 255) == nil)
|
||||
}
|
||||
|
||||
@Test func testIPv4SpecificValidation() {
|
||||
// Valid IPv4 prefixes
|
||||
#expect(Prefix.ipv4(0) != nil)
|
||||
#expect(Prefix.ipv4(16) != nil)
|
||||
#expect(Prefix.ipv4(32) != nil)
|
||||
|
||||
// Invalid IPv4 prefixes
|
||||
#expect(Prefix.ipv4(33) == nil)
|
||||
#expect(Prefix.ipv4(64) == nil)
|
||||
#expect(Prefix.ipv4(128) == nil)
|
||||
}
|
||||
|
||||
@Test func testIPv6SpecificValidation() {
|
||||
// Valid IPv6 prefixes
|
||||
#expect(Prefix.ipv6(0) != nil)
|
||||
#expect(Prefix.ipv6(64) != nil)
|
||||
#expect(Prefix.ipv6(128) != nil)
|
||||
|
||||
// Invalid IPv6 prefixes
|
||||
#expect(Prefix.ipv6(129) == nil)
|
||||
#expect(Prefix.ipv6(255) == nil)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import ContainerizationExtras
|
||||
import ContainerizationOS
|
||||
import Testing
|
||||
|
||||
@@ -274,7 +275,7 @@ struct NetlinkSessionTest {
|
||||
)
|
||||
|
||||
let session = NetlinkSession(socket: mockSocket)
|
||||
try session.addressAdd(interface: "eth0", address: "192.168.64.250/24")
|
||||
try session.addressAdd(interface: "eth0", ipv4Address: try CIDRv4("192.168.64.250/24"))
|
||||
|
||||
#expect(mockSocket.requests.count == 2)
|
||||
#expect(mockSocket.responseIndex == 2)
|
||||
@@ -321,8 +322,8 @@ struct NetlinkSessionTest {
|
||||
let session = NetlinkSession(socket: mockSocket)
|
||||
try session.routeAdd(
|
||||
interface: "eth0",
|
||||
destinationAddress: "192.168.64.0/24",
|
||||
srcAddr: "192.168.64.3"
|
||||
dstIpv4Addr: try CIDRv4("192.168.64.0/24"),
|
||||
srcIpv4Addr: try IPv4Address("192.168.64.3")
|
||||
)
|
||||
|
||||
#expect(mockSocket.requests.count == 2)
|
||||
|
||||
+2
-2
@@ -17,8 +17,8 @@ WARNINGS_AS_ERRORS ?= true
|
||||
SWIFT_WARNING_CONFIG := $(if $(filter-out false,$(WARNINGS_AS_ERRORS)),-Xswiftc -warnings-as-errors)
|
||||
SWIFT_CONFIGURATION := --swift-sdk aarch64-swift-linux-musl $(SWIFT_WARNING_CONFIG)
|
||||
|
||||
SWIFT_VERSION := 6.2
|
||||
SWIFT_SDK_URL := https://download.swift.org/swift-6.2-release/static-sdk/swift-6.2-RELEASE/swift-6.2-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz
|
||||
SWIFT_VERSION := 6.2.3
|
||||
SWIFT_SDK_URL := https://download.swift.org/swift-6.2.3-release/static-sdk/swift-6.2.3-RELEASE/swift-6.2.3-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz
|
||||
SWIFT_SDK_PATH := /tmp/$(notdir $(SWIFT_SDK_URL))
|
||||
|
||||
SWIFTLY_URL := https://download.swift.org/swiftly/darwin/swiftly.pkg
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import Cgroup
|
||||
import Containerization
|
||||
import ContainerizationError
|
||||
import ContainerizationExtras
|
||||
import ContainerizationNetlink
|
||||
import ContainerizationOCI
|
||||
import ContainerizationOS
|
||||
@@ -774,13 +775,14 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvid
|
||||
"ipAddrAdd",
|
||||
metadata: [
|
||||
"interface": "\(request.interface)",
|
||||
"address": "\(request.address)",
|
||||
"ipv4Address": "\(request.ipv4Address)",
|
||||
])
|
||||
|
||||
do {
|
||||
let socket = try DefaultNetlinkSocket()
|
||||
let session = NetlinkSession(socket: socket, log: log)
|
||||
try session.addressAdd(interface: request.interface, address: request.address)
|
||||
let ipv4Address = try CIDRv4(request.ipv4Address)
|
||||
try session.addressAdd(interface: request.interface, ipv4Address: ipv4Address)
|
||||
} catch {
|
||||
log.error(
|
||||
"ipAddrAdd",
|
||||
@@ -800,17 +802,19 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvid
|
||||
"ipRouteAddLink",
|
||||
metadata: [
|
||||
"interface": "\(request.interface)",
|
||||
"address": "\(request.address)",
|
||||
"srcAddr": "\(request.srcAddr)",
|
||||
"dstIpv4Addr": "\(request.dstIpv4Addr)",
|
||||
"srcIpv4Addr": "\(request.srcIpv4Addr)",
|
||||
])
|
||||
|
||||
do {
|
||||
let socket = try DefaultNetlinkSocket()
|
||||
let session = NetlinkSession(socket: socket, log: log)
|
||||
let dstIpv4Addr = try CIDRv4(request.dstIpv4Addr)
|
||||
let srcIpv4Addr = try IPv4Address(request.srcIpv4Addr)
|
||||
try session.routeAdd(
|
||||
interface: request.interface,
|
||||
destinationAddress: request.address,
|
||||
srcAddr: request.srcAddr
|
||||
dstIpv4Addr: dstIpv4Addr,
|
||||
srcIpv4Addr: srcIpv4Addr
|
||||
)
|
||||
} catch {
|
||||
log.error(
|
||||
@@ -832,13 +836,14 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvid
|
||||
"ipRouteAddDefault",
|
||||
metadata: [
|
||||
"interface": "\(request.interface)",
|
||||
"gateway": "\(request.gateway)",
|
||||
"ipv4Gateway": "\(request.ipv4Gateway)",
|
||||
])
|
||||
|
||||
do {
|
||||
let socket = try DefaultNetlinkSocket()
|
||||
let session = NetlinkSession(socket: socket, log: log)
|
||||
try session.routeAddDefault(interface: request.interface, gateway: request.gateway)
|
||||
let ipv4Gateway = try IPv4Address(request.ipv4Gateway)
|
||||
try session.routeAddDefault(interface: request.interface, ipv4Gateway: ipv4Gateway)
|
||||
} catch {
|
||||
log.error(
|
||||
"ipRouteAddDefault",
|
||||
|
||||
Reference in new issue
Block a user