mirror of
https://github.com/apple/container.git
synced 2026-09-12 10:45:42 +00:00
Adds MACAddress type. (#468)
- Simplifies processing and validation of MAC addresses for apple/container#1005, where we need to create MAC addresses and corresponding link local IPv6 addresses. - Adds Codable for IPAddress and CIDR for apple/container#1006, so we can parse and relay IP addresses regardless of address family for port forwards.
This commit is contained in:
@@ -93,7 +93,7 @@ public struct ContainerManager: Sendable {
|
||||
public struct Interface: Containerization.Interface, VZInterface, Sendable {
|
||||
public let ipv4Address: CIDRv4
|
||||
public let ipv4Gateway: IPv4Address?
|
||||
public let macAddress: String?
|
||||
public let macAddress: MACAddress?
|
||||
|
||||
// `reference` isn't used concurrently.
|
||||
nonisolated(unsafe) private let reference: vmnet_network_ref
|
||||
@@ -102,7 +102,7 @@ public struct ContainerManager: Sendable {
|
||||
reference: vmnet_network_ref,
|
||||
ipv4Address: CIDRv4,
|
||||
ipv4Gateway: IPv4Address,
|
||||
macAddress: String? = nil
|
||||
macAddress: MACAddress? = nil
|
||||
) {
|
||||
self.ipv4Address = ipv4Address
|
||||
self.ipv4Gateway = ipv4Gateway
|
||||
@@ -114,7 +114,7 @@ public struct ContainerManager: Sendable {
|
||||
public func device() throws -> VZVirtioNetworkDeviceConfiguration {
|
||||
let config = VZVirtioNetworkDeviceConfiguration()
|
||||
if let macAddress = self.macAddress {
|
||||
guard let mac = VZMACAddress(string: macAddress) else {
|
||||
guard let mac = VZMACAddress(string: macAddress.description) else {
|
||||
throw ContainerizationError(.invalidArgument, message: "invalid mac address \(macAddress)")
|
||||
}
|
||||
config.macAddress = mac
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -26,5 +26,5 @@ public protocol Interface: Sendable {
|
||||
var ipv4Gateway: IPv4Address? { get }
|
||||
|
||||
/// The interface MAC address, or nil to auto-configure the address.
|
||||
var macAddress: String? { get }
|
||||
var macAddress: MACAddress? { get }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -19,9 +19,9 @@ import ContainerizationExtras
|
||||
public struct NATInterface: Interface {
|
||||
public var ipv4Address: CIDRv4
|
||||
public var ipv4Gateway: IPv4Address?
|
||||
public var macAddress: String?
|
||||
public var macAddress: MACAddress?
|
||||
|
||||
public init(ipv4Address: CIDRv4, ipv4Gateway: IPv4Address?, macAddress: String? = nil) {
|
||||
public init(ipv4Address: CIDRv4, ipv4Gateway: IPv4Address?, macAddress: MACAddress? = nil) {
|
||||
self.ipv4Address = ipv4Address
|
||||
self.ipv4Gateway = ipv4Gateway
|
||||
self.macAddress = macAddress
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -29,7 +29,7 @@ import Synchronization
|
||||
public final class NATNetworkInterface: Interface, Sendable {
|
||||
public let ipv4Address: CIDRv4
|
||||
public let ipv4Gateway: IPv4Address?
|
||||
public let macAddress: String?
|
||||
public let macAddress: MACAddress?
|
||||
|
||||
@available(macOS 26, *)
|
||||
// `reference` isn't used concurrently.
|
||||
@@ -40,7 +40,7 @@ public final class NATNetworkInterface: Interface, Sendable {
|
||||
ipv4Address: CIDRv4,
|
||||
ipv4Gateway: IPv4Address?,
|
||||
reference: sending vmnet_network_ref,
|
||||
macAddress: String? = nil
|
||||
macAddress: MACAddress? = nil
|
||||
) {
|
||||
self.ipv4Address = ipv4Address
|
||||
self.ipv4Gateway = ipv4Gateway
|
||||
@@ -52,7 +52,7 @@ public final class NATNetworkInterface: Interface, Sendable {
|
||||
public init(
|
||||
ipv4Address: CIDRv4,
|
||||
ipv4Gateway: IPv4Address?,
|
||||
macAddress: String? = nil
|
||||
macAddress: MACAddress? = nil
|
||||
) {
|
||||
self.ipv4Address = ipv4Address
|
||||
self.ipv4Gateway = ipv4Gateway
|
||||
@@ -66,7 +66,7 @@ extension NATNetworkInterface: VZInterface {
|
||||
public func device() throws -> VZVirtioNetworkDeviceConfiguration {
|
||||
let config = VZVirtioNetworkDeviceConfiguration()
|
||||
if let macAddress = self.macAddress {
|
||||
guard let mac = VZMACAddress(string: macAddress) else {
|
||||
guard let mac = VZMACAddress(string: macAddress.description) else {
|
||||
throw ContainerizationError(.invalidArgument, message: "invalid mac address \(macAddress)")
|
||||
}
|
||||
config.macAddress = mac
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -465,7 +465,7 @@ extension NATInterface: VZInterface {
|
||||
public func device() throws -> VZVirtioNetworkDeviceConfiguration {
|
||||
let config = VZVirtioNetworkDeviceConfiguration()
|
||||
if let macAddress = self.macAddress {
|
||||
guard let mac = VZMACAddress(string: macAddress) else {
|
||||
guard let mac = VZMACAddress(string: macAddress.description) else {
|
||||
throw ContainerizationError(.invalidArgument, message: "invalid mac address \(macAddress)")
|
||||
}
|
||||
config.macAddress = mac
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// Copyright © 2026 Apple Inc. and the Containerization project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
public struct IPAddressError: Error, Equatable, Hashable, CustomStringConvertible {
|
||||
public struct AddressError: Error, Equatable, Hashable, CustomStringConvertible {
|
||||
public var description: String {
|
||||
String(describing: self.base)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -133,3 +133,16 @@ extension CIDR {
|
||||
case invalidAddressRange(lower: String, upper: String)
|
||||
}
|
||||
}
|
||||
|
||||
extension CIDR: Codable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let string = try container.decode(String.self)
|
||||
try self.init(string)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(description)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -27,7 +27,7 @@ public enum IPAddress: Sendable, Hashable, CustomStringConvertible, Equatable {
|
||||
///
|
||||
/// - Parameter string: IP address string to parse
|
||||
/// - Returns: An `IPAddress` containing either an IPv4 or IPv6 address
|
||||
/// - Throws: `IPAddressError.unableToParse` if invalid
|
||||
/// - Throws: `AddressError.unableToParse` if invalid
|
||||
public init(_ string: String) throws {
|
||||
let utf8 = string.utf8
|
||||
var hasColon = false
|
||||
@@ -50,7 +50,7 @@ public enum IPAddress: Sendable, Hashable, CustomStringConvertible, Equatable {
|
||||
let ipv4 = try IPv4Address(string)
|
||||
self = .v4(ipv4)
|
||||
} else {
|
||||
throw IPAddressError.unableToParse
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,3 +133,16 @@ public enum IPAddress: Sendable, Hashable, CustomStringConvertible, Equatable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension IPAddress: Codable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let string = try container.decode(String.self)
|
||||
try self.init(string)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(description)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -26,7 +26,7 @@ public struct IPv4Address: Sendable, Hashable, CustomStringConvertible, Equatabl
|
||||
/// 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
|
||||
/// - Throws: `AddressError.unableToParse` if the string is not a valid IPv4 address
|
||||
@inlinable
|
||||
public init(_ string: String) throws {
|
||||
self.value = try Self.parse(string)
|
||||
@@ -94,7 +94,7 @@ public struct IPv4Address: Sendable, Hashable, CustomStringConvertible, Equatabl
|
||||
@usableFromInline
|
||||
internal static func parse(_ s: String) throws -> UInt32 {
|
||||
guard !s.isEmpty, s.count >= 7, s.count <= 15 else {
|
||||
throw IPAddressError.unableToParse
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
|
||||
// IP addresses should only contain ASCII digits and dots
|
||||
@@ -102,7 +102,7 @@ public struct IPv4Address: Sendable, Hashable, CustomStringConvertible, Equatabl
|
||||
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
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ public struct IPv4Address: Sendable, Hashable, CustomStringConvertible, Equatabl
|
||||
if byte == 46 { // ASCII '.'
|
||||
// Validate octet before processing
|
||||
guard octetCount < 3, digitCount > 0, digitCount <= 3, currentOctet <= 255 else {
|
||||
throw IPAddressError.unableToParse
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
|
||||
// Shift result and add current octet
|
||||
@@ -143,7 +143,7 @@ public struct IPv4Address: Sendable, Hashable, CustomStringConvertible, Equatabl
|
||||
currentOctet = 0
|
||||
} else if digitCount > 1 && currentOctet == 0 {
|
||||
// We had a leading zero and now have more digits - invalid
|
||||
throw IPAddressError.unableToParse
|
||||
throw AddressError.unableToParse
|
||||
} else {
|
||||
// Normal case: build the octet value
|
||||
currentOctet = currentOctet * 10 + digit
|
||||
@@ -151,17 +151,17 @@ public struct IPv4Address: Sendable, Hashable, CustomStringConvertible, Equatabl
|
||||
|
||||
// Early termination if octet becomes too large
|
||||
guard currentOctet <= 255, digitCount <= 3 else {
|
||||
throw IPAddressError.unableToParse
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
|
||||
} else {
|
||||
throw IPAddressError.unableToParse
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
}
|
||||
|
||||
// Validate final octet
|
||||
guard octetCount == 3, digitCount > 0, digitCount <= 3, currentOctet <= 255 else {
|
||||
throw IPAddressError.unableToParse
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
|
||||
return (result << 8) | UInt32(currentOctet)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -115,7 +115,7 @@ extension IPv6Address {
|
||||
|
||||
// Validate complete consumption of input
|
||||
guard currentPosition >= utf8.endIndex else {
|
||||
throw IPAddressError.malformedAddress
|
||||
throw AddressError.malformedAddress
|
||||
}
|
||||
|
||||
// Apply ellipsis expansion for the IPv6 portion
|
||||
@@ -138,7 +138,7 @@ extension IPv6Address {
|
||||
///
|
||||
/// - 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
|
||||
/// - Throws: `AddressError.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 '.'
|
||||
@@ -158,7 +158,7 @@ extension IPv6Address {
|
||||
|
||||
let possibleIPv4 = String(input[afterColon...])
|
||||
guard let ipv4Value = try? IPv4Address.parse(possibleIPv4) else {
|
||||
throw IPAddressError.invalidIPv4SuffixInIPv6Address
|
||||
throw AddressError.invalidIPv4SuffixInIPv6Address
|
||||
}
|
||||
|
||||
// Check if lastColonIndex is the second ':' of '::'. If so, ensure to include it.
|
||||
@@ -171,7 +171,7 @@ extension IPv6Address {
|
||||
///
|
||||
/// - 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
|
||||
/// - Throws: `AddressError.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)
|
||||
@@ -179,7 +179,7 @@ extension IPv6Address {
|
||||
|
||||
let zoneStartIndex = input.index(after: percentIndex)
|
||||
guard zoneStartIndex < input.endIndex else {
|
||||
throw IPAddressError.invalidZoneIdentifier
|
||||
throw AddressError.invalidZoneIdentifier
|
||||
}
|
||||
|
||||
let addressPart = String(input[..<percentIndex])
|
||||
@@ -194,7 +194,7 @@ extension IPv6Address {
|
||||
/// - 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
|
||||
/// - Throws: `AddressError.invalidHexGroup` if no valid hex digits are found
|
||||
///
|
||||
/// ## Example
|
||||
/// ```swift
|
||||
@@ -233,7 +233,7 @@ extension IPv6Address {
|
||||
|
||||
guard digitCount > 0 else {
|
||||
// No hex digits found
|
||||
throw IPAddressError.invalidHexGroup
|
||||
throw AddressError.invalidHexGroup
|
||||
}
|
||||
return (accumulator, currentIndex)
|
||||
}
|
||||
@@ -266,20 +266,20 @@ extension IPv6Address {
|
||||
) throws -> String.UTF8View.Index {
|
||||
// Expect colon separator
|
||||
guard group[position] == 58 else { // ASCII ':'
|
||||
throw IPAddressError.malformedAddress
|
||||
throw AddressError.malformedAddress
|
||||
}
|
||||
|
||||
let afterFirstColon = group.index(after: position)
|
||||
guard afterFirstColon < group.endIndex else {
|
||||
// Trailing colon not allowed
|
||||
throw IPAddressError.malformedAddress
|
||||
throw AddressError.malformedAddress
|
||||
}
|
||||
|
||||
// Check for double colon, return position after that
|
||||
if group[afterFirstColon] == 58 { // ASCII ':'
|
||||
guard ellipsisPosition == nil else {
|
||||
// Multiple :: not allowed
|
||||
throw IPAddressError.multipleEllipsis
|
||||
throw AddressError.multipleEllipsis
|
||||
}
|
||||
ellipsisPosition = currentByteIndex
|
||||
let afterSecondColon = group.index(after: afterFirstColon)
|
||||
@@ -295,7 +295,7 @@ extension IPv6Address {
|
||||
/// - 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
|
||||
/// - Throws: `AddressError.incompleteAddress` for invalid address lengths
|
||||
private static func expandEllipsis(
|
||||
in ipBytes: inout [UInt8],
|
||||
parsedBytes: Int,
|
||||
@@ -305,7 +305,7 @@ extension IPv6Address {
|
||||
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
|
||||
throw AddressError.incompleteAddress // Incomplete address without ellipsis
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -313,7 +313,7 @@ extension IPv6Address {
|
||||
// Calculate expansion within the byte limit
|
||||
let bytesToExpand = byteLimit - parsedBytes
|
||||
guard bytesToExpand > 0 else {
|
||||
throw IPAddressError.malformedAddress // No room for ellipsis expansion
|
||||
throw AddressError.malformedAddress // No room for ellipsis expansion
|
||||
}
|
||||
|
||||
let suffixBytes = Array(ipBytes[ellipsisPosition..<parsedBytes])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -25,7 +25,7 @@ public struct IPv6Address: Sendable, Hashable, CustomStringConvertible, Equatabl
|
||||
/// 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
|
||||
/// - Throws: `AddressError` if the string is not a valid IPv6 address
|
||||
public init(_ address: String) throws {
|
||||
self = try Self.parse(address)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2026 Apple Inc. and the Containerization project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// An EUI-48 MAC address as specified by IEEE 802.
|
||||
@frozen
|
||||
public struct MACAddress: Sendable, Hashable, CustomStringConvertible, Equatable, Comparable {
|
||||
public let value: UInt64
|
||||
|
||||
/// Creates an MACAddress from an integer.
|
||||
///
|
||||
/// - Parameter value: The big-endian value of the MAC address.
|
||||
/// The most significant 16 bits of the value are ignored.
|
||||
@inlinable
|
||||
public init(_ value: UInt64) {
|
||||
self.value = value & 0x0000_ffff_ffff_ffff
|
||||
}
|
||||
|
||||
/// Creates an MACAddress from a string representation.
|
||||
///
|
||||
/// - Parameter string: The MAC address string with colon or dash delimiters.
|
||||
/// - Throws: `AddressError.unableToParse` if the string is not a valid MAC 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: UInt64) -> [UInt8] {
|
||||
var result = [UInt8](repeating: 0, count: 6)
|
||||
result[0] = UInt8((value >> 40) & 0xff)
|
||||
result[1] = UInt8((value >> 32) & 0xff)
|
||||
result[2] = UInt8((value >> 24) & 0xff)
|
||||
result[3] = UInt8((value >> 16) & 0xff)
|
||||
result[4] = UInt8((value >> 8) & 0xff)
|
||||
result[5] = UInt8(value & 0xff)
|
||||
return result
|
||||
}
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
@usableFromInline
|
||||
static func bytes(_ value: UInt64) -> InlineArray<6, UInt8> {
|
||||
let result: InlineArray<6, UInt8> = [
|
||||
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
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public var description: String {
|
||||
bytes.map { String(format: "%02x", $0) }.joined(separator: ":")
|
||||
}
|
||||
|
||||
/// Parses an MAC address string into a UInt64 representation.
|
||||
///
|
||||
/// ## Validation Rules
|
||||
/// - Exactly six groups of two hexadecimal digits, separated by colons
|
||||
/// or dashes
|
||||
/// - No whitespace characters
|
||||
/// - Only hexadecimal digits and colons allowed
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```swift
|
||||
/// MACAddress.parse("01:23:45:67:89:ab") // Returns: 0x0000_0123_4567_89ab
|
||||
/// MACAddress.parse("01-23-45-67-89-AB") // Returns: 0x0000_0123_4567_89ab
|
||||
/// MACAddress.parse("00:00:00:00:00:00") // Returns: 0x0000_0000_0000_0000
|
||||
/// MACAddress.parse("ff:ff:ff:ff:ff:ff") // Returns: 0x0000_ffff_ffff_ffff
|
||||
///
|
||||
/// // Invalid examples:
|
||||
/// MACAddress.parse("01:23:45:67:89") // Wrong number of octets
|
||||
/// MACAddress.parse("01:23:45:67:89:a") // Invalid octet length
|
||||
/// MACAddress.parse("01:23:45:67:89:hi") // Invalid octet content
|
||||
/// MACAddress.parse("01:23-45:67-89:ab") // Inconsistent separators
|
||||
/// MACAddress.parse(" 01:23:45:67:89:ab ") // Whitespace
|
||||
/// ```
|
||||
///
|
||||
/// - Parameter s: The MAC address string to parse
|
||||
/// - Returns: The 64-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 -> UInt64 {
|
||||
guard !s.isEmpty, s.count == 17 else {
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
|
||||
// MAC addresses should only contain ASCII hex 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 AddressError.unableToParse
|
||||
}
|
||||
}
|
||||
|
||||
// accumulator for the 64 bit representation of the MAC address
|
||||
var result: UInt64 = 0
|
||||
|
||||
// tracking octet count, max 6 allowed
|
||||
var octetCount = 0
|
||||
var currentOctet = 0
|
||||
|
||||
// number of digits in the string representation of the octet
|
||||
var digitCount = 0
|
||||
|
||||
// separator character to use
|
||||
var separator: String.UTF8View.Element?
|
||||
|
||||
for byte in utf8 {
|
||||
if byte == 0x3a || byte == 0x2d { // ASCII ':'
|
||||
// Ensure separator is consistent
|
||||
guard separator == nil || byte == separator else {
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
separator = byte
|
||||
|
||||
// Validate octet before processing
|
||||
guard octetCount < 5, digitCount == 2 else {
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
|
||||
// Shift result and add current octet
|
||||
result = (result << 8) | UInt64(currentOctet)
|
||||
|
||||
// Reset for next octet
|
||||
octetCount += 1
|
||||
currentOctet = 0
|
||||
digitCount = 0
|
||||
|
||||
} else if byte >= 0x30 && byte <= 0x39 { // ASCII '0'-'9'
|
||||
let digit = Int(byte - 0x30)
|
||||
|
||||
digitCount += 1
|
||||
currentOctet = (currentOctet << 4) + digit
|
||||
|
||||
// Early termination if octet becomes too large
|
||||
guard digitCount <= 2 else {
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
|
||||
} else if byte >= 0x41 && byte <= 0x46 { // ASCII 'A'-'F'
|
||||
let digit = Int(byte - 0x41 + 10)
|
||||
|
||||
digitCount += 1
|
||||
currentOctet = (currentOctet << 4) + digit
|
||||
|
||||
// Early termination if octet becomes too large
|
||||
guard digitCount <= 2 else {
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
|
||||
} else if byte >= 0x61 && byte <= 0x66 { // ASCII 'A'-'F'
|
||||
let digit = Int(byte - 0x61 + 10)
|
||||
|
||||
digitCount += 1
|
||||
currentOctet = (currentOctet << 4) + digit
|
||||
|
||||
// Early termination if octet becomes too large
|
||||
guard digitCount <= 2 else {
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
|
||||
} else {
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
}
|
||||
|
||||
// Validate final octet
|
||||
guard octetCount == 5, digitCount == 2 else {
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
|
||||
return (result << 8) | UInt64(currentOctet)
|
||||
}
|
||||
|
||||
// MARK: - Address Classification Methods
|
||||
|
||||
/// Returns `true` if the MAC address is locally administered.
|
||||
///
|
||||
/// IEEE 802 specifies that the second-least-significant bit of
|
||||
/// the first octet of the MAC address determines whether the
|
||||
/// address is globally unique (bit cleared) or locally
|
||||
/// administered (bit set).
|
||||
@inlinable
|
||||
public var isLocallyAdministered: Bool {
|
||||
(value & 0x0000_0200_0000_0000) != 0
|
||||
}
|
||||
|
||||
/// Returns `true` if the MAC address is multicast.
|
||||
///
|
||||
/// IEEE 802 specifies that the least-significant bit of
|
||||
/// the first octet of the MAC address determines whether the
|
||||
/// address is unicast (bit cleared) or multicast (bit set).
|
||||
@inlinable
|
||||
public var isMulticast: Bool {
|
||||
(value & 0x0000_0100_0000_0000) != 0
|
||||
}
|
||||
|
||||
/// Returns the link local IP address based on the EUI-64 version
|
||||
/// of the MAC address.
|
||||
///
|
||||
/// - Parameter network: The IPv6 address to use for the network prefix
|
||||
/// - Returns: The link local IP address for the MAC address
|
||||
@inlinable
|
||||
public func ipv6Address(network: IPv6Address) -> IPv6Address {
|
||||
let prefixBytes = network.bytes
|
||||
return IPv6Address([
|
||||
prefixBytes[0], prefixBytes[1], prefixBytes[2], prefixBytes[3],
|
||||
prefixBytes[4], prefixBytes[5], prefixBytes[6], prefixBytes[7],
|
||||
bytes[0] ^ 0x02, bytes[1], bytes[2], 0xff,
|
||||
0xfe, bytes[3], bytes[4], bytes[5],
|
||||
])
|
||||
}
|
||||
|
||||
/// Compares two IPv4 addresses numerically.
|
||||
@inlinable
|
||||
public static func < (lhs: MACAddress, rhs: MACAddress) -> Bool {
|
||||
lhs.value < rhs.value
|
||||
}
|
||||
}
|
||||
|
||||
extension MACAddress: Codable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let string = try container.decode(String.self)
|
||||
try self.init(string)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(description)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -172,4 +172,66 @@ struct IPAddressTests {
|
||||
#expect(dict[ip2] == "IPv6")
|
||||
#expect(dict.count == 2)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable encodes to string representation",
|
||||
arguments: [
|
||||
"127.0.0.1",
|
||||
"192.168.1.1",
|
||||
"0.0.0.0",
|
||||
"255.255.255.255",
|
||||
]
|
||||
)
|
||||
func testCodableEncodeIPv4(address: String) throws {
|
||||
let original = try IPAddress(address)
|
||||
let encoded = try JSONEncoder().encode(original)
|
||||
#expect(String(data: encoded, encoding: .utf8) == "\"\(address)\"")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable decodes from string representation",
|
||||
arguments: [
|
||||
"127.0.0.1",
|
||||
"192.168.1.1",
|
||||
"0.0.0.0",
|
||||
"255.255.255.255",
|
||||
]
|
||||
)
|
||||
func testCodableDecodeIPv4(address: String) throws {
|
||||
let json = Data("\"\(address)\"".utf8)
|
||||
let decoded = try JSONDecoder().decode(IPAddress.self, from: json)
|
||||
let expected = try IPAddress(address)
|
||||
#expect(decoded == expected)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable encodes to string representation",
|
||||
arguments: [
|
||||
("::1", "::1"),
|
||||
("2001:db8::1", "2001:db8::1"),
|
||||
("::", "::"),
|
||||
("fe80::1", "fe80::1"),
|
||||
]
|
||||
)
|
||||
func testCodableEncodeIPv6(input: String, expected: String) throws {
|
||||
let original = try IPAddress(input)
|
||||
let encoded = try JSONEncoder().encode(original)
|
||||
#expect(String(data: encoded, encoding: .utf8) == "\"\(expected)\"")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable decodes from string representation",
|
||||
arguments: [
|
||||
"::1",
|
||||
"2001:db8::1",
|
||||
"::",
|
||||
"fe80::1",
|
||||
]
|
||||
)
|
||||
func testCodableDecodeIPv6(address: String) throws {
|
||||
let json = Data("\"\(address)\"".utf8)
|
||||
let decoded = try JSONDecoder().decode(IPAddress.self, from: json)
|
||||
let expected = try IPAddress(address)
|
||||
#expect(decoded == expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -81,7 +81,7 @@ struct IPv4AddressTests {
|
||||
]
|
||||
)
|
||||
func testStringInitializerInvalid(invalidAddress: String) {
|
||||
#expect(throws: IPAddressError.self) {
|
||||
#expect(throws: AddressError.self) {
|
||||
try IPv4Address(invalidAddress)
|
||||
}
|
||||
}
|
||||
@@ -196,6 +196,37 @@ struct IPv4AddressTests {
|
||||
#expect(taskAddress.value == 0x7F00_0001)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable encodes to string representation",
|
||||
arguments: [
|
||||
"127.0.0.1",
|
||||
"192.168.1.1",
|
||||
"0.0.0.0",
|
||||
"255.255.255.255",
|
||||
]
|
||||
)
|
||||
func testCodableEncode(address: String) throws {
|
||||
let original = try IPv4Address(address)
|
||||
let encoded = try JSONEncoder().encode(original)
|
||||
#expect(String(data: encoded, encoding: .utf8) == "\"\(address)\"")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable decodes from string representation",
|
||||
arguments: [
|
||||
"127.0.0.1",
|
||||
"192.168.1.1",
|
||||
"0.0.0.0",
|
||||
"255.255.255.255",
|
||||
]
|
||||
)
|
||||
func testCodableDecode(address: String) throws {
|
||||
let json = Data("\"\(address)\"".utf8)
|
||||
let decoded = try JSONDecoder().decode(IPv4Address.self, from: json)
|
||||
let expected = try IPv4Address(address)
|
||||
#expect(decoded == expected)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Edge Cases and Error Conditions
|
||||
@@ -247,7 +278,7 @@ struct IPv4AddressTests {
|
||||
]
|
||||
)
|
||||
func testLeadingZeroValidationInvalid(invalidAddress: String) {
|
||||
#expect(throws: IPAddressError.self) {
|
||||
#expect(throws: AddressError.self) {
|
||||
try IPv4Address(invalidAddress)
|
||||
}
|
||||
}
|
||||
@@ -267,7 +298,7 @@ struct IPv4AddressTests {
|
||||
]
|
||||
)
|
||||
func testStringLengthValidationTooShort(shortString: String) {
|
||||
#expect(throws: IPAddressError.self) {
|
||||
#expect(throws: AddressError.self) {
|
||||
try IPv4Address(shortString)
|
||||
}
|
||||
}
|
||||
@@ -283,7 +314,7 @@ struct IPv4AddressTests {
|
||||
]
|
||||
)
|
||||
func testStringLengthValidationTooLong(longString: String) {
|
||||
#expect(throws: IPAddressError.self) {
|
||||
#expect(throws: AddressError.self) {
|
||||
try IPv4Address(longString)
|
||||
}
|
||||
}
|
||||
@@ -421,42 +452,11 @@ struct IPv4AddressTests {
|
||||
do {
|
||||
_ = try IPv4Address(invalidInput)
|
||||
#expect(Bool(false), "Should have thrown for input: \(invalidInput)")
|
||||
} catch let error as IPAddressError {
|
||||
#expect(error == IPAddressError.unableToParse)
|
||||
} catch let error as AddressError {
|
||||
#expect(error == AddressError.unableToParse)
|
||||
} catch {
|
||||
#expect(Bool(false), "Should have thrown IPAddressError, got: \(error)")
|
||||
#expect(Bool(false), "Should have thrown AddressError, got: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable encodes to string representation",
|
||||
arguments: [
|
||||
"127.0.0.1",
|
||||
"192.168.1.1",
|
||||
"0.0.0.0",
|
||||
"255.255.255.255",
|
||||
]
|
||||
)
|
||||
func testCodableEncode(address: String) throws {
|
||||
let original = try IPv4Address(address)
|
||||
let encoded = try JSONEncoder().encode(original)
|
||||
#expect(String(data: encoded, encoding: .utf8) == "\"\(address)\"")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable decodes from string representation",
|
||||
arguments: [
|
||||
"127.0.0.1",
|
||||
"192.168.1.1",
|
||||
"0.0.0.0",
|
||||
"255.255.255.255",
|
||||
]
|
||||
)
|
||||
func testCodableDecode(address: String) throws {
|
||||
let json = Data("\"\(address)\"".utf8)
|
||||
let decoded = try JSONDecoder().decode(IPv4Address.self, from: json)
|
||||
let expected = try IPv4Address(address)
|
||||
#expect(decoded == expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -96,7 +96,7 @@ struct IPv6AddressParseTests {
|
||||
]
|
||||
)
|
||||
func testParseInvalidHexadecimalGroup(invalidInput: String) {
|
||||
#expect(throws: IPAddressError.self) {
|
||||
#expect(throws: AddressError.self) {
|
||||
let utf8 = invalidInput.utf8
|
||||
_ = try IPv6Address.parseHexadecimal(
|
||||
from: utf8,
|
||||
@@ -222,7 +222,7 @@ struct IPv6AddressParseTests {
|
||||
#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")
|
||||
#expect(error is AddressError, "\(testCase) should throw IPAddressError if it fails to parse")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -372,7 +372,7 @@ struct IPv6AddressParseTests {
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22MultipleDoubleColonsShouldFail(invalid: String) {
|
||||
#expect(throws: IPAddressError.self, "Multiple '::' should fail: \(invalid)") {
|
||||
#expect(throws: AddressError.self, "Multiple '::' should fail: \(invalid)") {
|
||||
_ = try IPv6Address.parse(invalid)
|
||||
}
|
||||
}
|
||||
@@ -428,7 +428,7 @@ struct IPv6AddressParseTests {
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22InvalidFormatsShouldFail(invalid: String) {
|
||||
#expect(throws: IPAddressError.self, "Invalid format should fail: \(invalid)") {
|
||||
#expect(throws: AddressError.self, "Invalid format should fail: \(invalid)") {
|
||||
_ = try IPv6Address.parse(invalid)
|
||||
}
|
||||
}
|
||||
@@ -633,7 +633,7 @@ struct IPv6AddressParseTests {
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22IPv4MixedNotationInvalid(invalid: String) {
|
||||
#expect(throws: IPAddressError.self, "Invalid IPv4 mixed notation should fail: \(invalid)") {
|
||||
#expect(throws: AddressError.self, "Invalid IPv4 mixed notation should fail: \(invalid)") {
|
||||
_ = try IPv6Address.parse(invalid)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -60,7 +60,7 @@ struct IPv6IPv4ParsingTests {
|
||||
]
|
||||
)
|
||||
func testInvalidIPv4Throws(invalid: String) {
|
||||
#expect(throws: IPAddressError.self) {
|
||||
#expect(throws: AddressError.self) {
|
||||
_ = try IPv6Address.extractIPv4Suffix(from: invalid)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2026 Apple Inc. and the Containerization project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import ContainerizationExtras
|
||||
|
||||
@Suite("MACAddress Tests")
|
||||
struct MACAddressTests {
|
||||
|
||||
// MARK: - Initializer Tests
|
||||
|
||||
@Suite("Initializers")
|
||||
struct InitializerTests {
|
||||
|
||||
@Test(
|
||||
"UInt64 initializer - valid addresses",
|
||||
arguments: [
|
||||
//(0x0123_4567_89ab, "01:23:45:67:89:ab"), // a valid address
|
||||
//(0x0000_0000_0000, "00:00:00:00:00:00"), // zero address
|
||||
//(0xFFFF_FFFF_FFFF, "ff:ff:ff:ff:ff:ff"), // max address
|
||||
(0xffff_0123_4567_89ab, "01:23:45:67:89:ab") // drops the most significant 16 bits
|
||||
]
|
||||
)
|
||||
func testUInt64InitializerValid(inputValue: UInt64, description: String) {
|
||||
let address = MACAddress(inputValue)
|
||||
#expect(address.value == inputValue & 0x0000_ffff_ffff_ffff)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"String initializer - valid addresses",
|
||||
arguments: [
|
||||
("01:23:45:67:89:ab", 0x0123_4567_89ab), // colon separators
|
||||
("01-23-45-67-89-ab", 0x0123_4567_89ab), // dash separators
|
||||
("ab:cd:ef:AB:CD:EF", 0xabcd_efab_cdef), // mixed case
|
||||
("00:00:00:00:00:00", 0x0000_0000_0000), // zero address
|
||||
("ff:ff:ff:ff:ff:ff", 0xffff_ffff_ffff), // max address
|
||||
]
|
||||
)
|
||||
func testStringInitializerValid(addressString: String, expectedValue: UInt64) throws {
|
||||
let address = try MACAddress(addressString)
|
||||
#expect(address.value == expectedValue)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"String initializer - invalid addresses",
|
||||
arguments: [
|
||||
"", // empty string
|
||||
"01:23:45:67:89", // too few octets
|
||||
"01:23:45:67:89:ab:cd", // too many octets
|
||||
"01:23:45:67:89:", // empty octet
|
||||
":23:45:67:89:ab", // empty octet
|
||||
"01::45:67:89:ab", // empty octet
|
||||
"01:23:45:67:89:a", // short octet
|
||||
"1:23:45:67:89:ab", // short octet
|
||||
"01:2:45:67:89:ab", // short octet
|
||||
"01:23:45:67:89:abc", // long octet
|
||||
"012:23:45:67:89:ab", // long octet
|
||||
"01:234:45:67:89:ab", // long octet
|
||||
"01:23:45:67:89:@G", // invalid content 0x40, 0x47
|
||||
"`g:23:45:67:89:ab", // invalid content 0x60, 0x67
|
||||
"01:hi:45:67:89:ab", // invalid content
|
||||
" 01:23:45:67:89:ab", // leading whitespace
|
||||
"01:23:45:67:89:ab ", // trailing whitespace
|
||||
"01: 23:45:67:89:ab", // internal whitespace
|
||||
]
|
||||
)
|
||||
func testStringInitializerInvalid(invalidAddress: String) {
|
||||
#expect(throws: AddressError.self) {
|
||||
try MACAddress(invalidAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Property Tests
|
||||
|
||||
@Suite("Properties")
|
||||
struct PropertyTests {
|
||||
|
||||
@Test(
|
||||
"bytes property",
|
||||
arguments: [
|
||||
(
|
||||
UInt64(0x0123_4567_89ab),
|
||||
[UInt8(0x01), UInt8(0x23), UInt8(0x45), UInt8(0x67), UInt8(0x89), UInt8(0xab)]
|
||||
),
|
||||
(
|
||||
UInt64(0x0000_0000_0000),
|
||||
[UInt8(0x00), UInt8(0x00), UInt8(0x00), UInt8(0x00), UInt8(0x00), UInt8(0x00)]
|
||||
),
|
||||
(
|
||||
UInt64(0xffff_ffff_ffff),
|
||||
[UInt8(0xff), UInt8(0xff), UInt8(0xff), UInt8(0xff), UInt8(0xff), UInt8(0xff)]
|
||||
),
|
||||
(
|
||||
UInt64(0xffff_0123_4567_89ab),
|
||||
[UInt8(0x01), UInt8(0x23), UInt8(0x45), UInt8(0x67), UInt8(0x89), UInt8(0xab)]
|
||||
),
|
||||
]
|
||||
)
|
||||
func testBytesProperty(inputValue: UInt64, expectedBytes: [UInt8]) {
|
||||
let address = MACAddress(inputValue)
|
||||
#expect(address.bytes == expectedBytes)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"description property",
|
||||
arguments: [
|
||||
(0x0123_4567_89ab, "01:23:45:67:89:ab"),
|
||||
(0x0000_0000_0000, "00:00:00:00:00:00"),
|
||||
(0xffff_ffff_ffff, "ff:ff:ff:ff:ff:ff"),
|
||||
(0xffff_0123_4567_89ab, "01:23:45:67:89:ab"),
|
||||
]
|
||||
)
|
||||
func testDescriptionProperty(inputValue: UInt64, expectedDescription: String) {
|
||||
let address = MACAddress(inputValue)
|
||||
#expect(address.description == expectedDescription)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"isLocallyAdministered property",
|
||||
arguments: [
|
||||
(0x0000_1234_5678, false),
|
||||
(0x0200_1234_5678, true),
|
||||
]
|
||||
)
|
||||
func testIsLocallyAdministeredProperty(inputValue: UInt64, expectedValue: Bool) {
|
||||
let address = MACAddress(inputValue)
|
||||
#expect(address.isLocallyAdministered == expectedValue)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"isMulticast property",
|
||||
arguments: [
|
||||
(0x0000_1234_5678, false),
|
||||
(0x0100_1234_5678, true),
|
||||
]
|
||||
)
|
||||
func testIsMulticastProperty(inputValue: UInt64, expectedValue: Bool) {
|
||||
let address = MACAddress(inputValue)
|
||||
#expect(address.isMulticast == expectedValue)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"round-trip string conversion",
|
||||
arguments: [
|
||||
"01:23:45:67:89:ab",
|
||||
"00:00:00:00:00:00",
|
||||
"ff:ff:ff:ff:ff:ff",
|
||||
"01-23-45-67-89-AB",
|
||||
]
|
||||
)
|
||||
func testRoundTripStringConversion(addressString: String) throws {
|
||||
let address = try MACAddress(addressString)
|
||||
#expect(address.description == addressString.lowercased().replacingOccurrences(of: "-", with: ":"))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Link Local Address Tests
|
||||
|
||||
@Suite("Link Local Addresses")
|
||||
struct LinkLocalAddressTests {
|
||||
|
||||
@Test(
|
||||
"Link local address",
|
||||
arguments: [
|
||||
(0x39a7_9407_cbd0, 0xfd97_7b15_d62e_75ac_3ba7_94ff_fe07_cbd0),
|
||||
(0x5e3b_68d7_e510, 0xfd97_7b15_d62e_75ac_5c3b_68ff_fed7_e510),
|
||||
]
|
||||
)
|
||||
func testLinkLocalAddress(mac: UInt64, ipv6: UInt128) {
|
||||
let mac = MACAddress(mac)
|
||||
let ipv6Prefix = IPv6Address(ipv6 & 0xffff_ffff_ffff_ffff_0000_0000_0000_0000)
|
||||
#expect(mac.ipv6Address(network: ipv6Prefix) == IPv6Address(ipv6))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Protocol Conformance Tests
|
||||
|
||||
@Suite("Protocol Conformances")
|
||||
struct ProtocolConformanceTests {
|
||||
|
||||
@Test("Equatable conformance")
|
||||
func testEquatableConformance() {
|
||||
let addr1 = MACAddress(0x0123_4567_89ab)
|
||||
let addr2 = MACAddress(0x0123_4567_89ab)
|
||||
let addr3 = MACAddress(0x0123_4567_89ac)
|
||||
|
||||
#expect(addr1 == addr2)
|
||||
#expect(addr1 != addr3)
|
||||
#expect(addr2 != addr3)
|
||||
}
|
||||
|
||||
@Test("Hashable conformance")
|
||||
func testHashableConformance() {
|
||||
let addr1 = MACAddress(0x0123_4567_89ab)
|
||||
let addr2 = MACAddress(0x0123_4567_89ab)
|
||||
let addr3 = MACAddress(0x0123_4567_89ac)
|
||||
|
||||
// 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<MACAddress> = [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 = MACAddress(0x0123_4567_89ab)
|
||||
let stringRepresentation = String(describing: address)
|
||||
#expect(stringRepresentation == "01:23:45:67:89:ab")
|
||||
}
|
||||
|
||||
@Test("Sendable conformance")
|
||||
func testSendableConformance() {
|
||||
// This test verifies that MACAddress can be safely passed across concurrency boundaries
|
||||
let address = MACAddress(0x0123_4567_89ab)
|
||||
|
||||
Task {
|
||||
let taskAddress = address
|
||||
#expect(taskAddress.value == 0x0123_4567_89ab)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Performance Tests
|
||||
|
||||
@Suite("Performance")
|
||||
struct PerformanceTests {
|
||||
|
||||
@Test("parsing performance")
|
||||
func testParsingPerformance() throws {
|
||||
let testAddresses = [
|
||||
"01:23:45:67:89:ab",
|
||||
"01-23-45-67-89-ab",
|
||||
"01-23-45-67-89-a",
|
||||
"01-23-45-67-89-abc",
|
||||
]
|
||||
|
||||
// Warm up
|
||||
for _ in 0..<100 {
|
||||
for address in testAddresses {
|
||||
_ = try? MACAddress(address)
|
||||
}
|
||||
}
|
||||
|
||||
// Measure performance
|
||||
let iterations = 10000
|
||||
let startTime = Date()
|
||||
|
||||
for _ in 0..<iterations {
|
||||
for address in testAddresses {
|
||||
_ = try? MACAddress(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 = MACAddress(0x0123_4567_89ab)
|
||||
|
||||
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 = MACAddress(0x0123_4567_89ab)
|
||||
|
||||
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: [
|
||||
(0x0123_4567_89ab, "01:23:45:67:89:ab"),
|
||||
(0x0000_0000_0000, "00:00:00:00:00:00"),
|
||||
(0xffff_ffff_ffff, "ff:ff:ff:ff:ff:ff"),
|
||||
]
|
||||
)
|
||||
func testComprehensiveRoundTrip(expectedValue: UInt64, expectedString: String) throws {
|
||||
// Test UInt32 -> String
|
||||
let addressFromUInt32 = MACAddress(expectedValue)
|
||||
#expect(addressFromUInt32.description == expectedString)
|
||||
|
||||
// Test String -> UInt32
|
||||
let addressFromString = try MACAddress(expectedString)
|
||||
#expect(addressFromString.value == expectedValue)
|
||||
|
||||
// Test equality
|
||||
#expect(addressFromUInt32 == addressFromString)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"error message consistency",
|
||||
arguments: [
|
||||
"",
|
||||
"hi:00:00:00:00:00",
|
||||
"01:23:45:67:89",
|
||||
"01:23:45:67:89:ab:cd",
|
||||
"001:23:45:67:89:ab:cd",
|
||||
" 01:23:45:67:89:ab:cd",
|
||||
"01:23:45:67:89:ab:cd ",
|
||||
]
|
||||
)
|
||||
func testErrorMessageConsistency(invalidInput: String) {
|
||||
do {
|
||||
_ = try MACAddress(invalidInput)
|
||||
#expect(Bool(false), "Should have thrown for input: \(invalidInput)")
|
||||
} catch let error as AddressError {
|
||||
#expect(error == AddressError.unableToParse)
|
||||
} catch {
|
||||
#expect(Bool(false), "Should have thrown AddressError, got: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable encodes to string representation",
|
||||
arguments: [
|
||||
"01:23:45:67:89:ab",
|
||||
"00:00:00:00:00:00",
|
||||
"ff:ff:ff:ff:ff:ff",
|
||||
]
|
||||
)
|
||||
func testCodableEncode(address: String) throws {
|
||||
let original = try MACAddress(address)
|
||||
let encoded = try JSONEncoder().encode(original)
|
||||
#expect(String(data: encoded, encoding: .utf8) == "\"\(address)\"")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable decodes from string representation",
|
||||
arguments: [
|
||||
"01:23:45:67:89:ab",
|
||||
"00:00:00:00:00:00",
|
||||
"ff:ff:ff:ff:ff:ff",
|
||||
]
|
||||
)
|
||||
func testCodableDecode(address: String) throws {
|
||||
let json = Data("\"\(address)\"".utf8)
|
||||
let decoded = try JSONDecoder().decode(MACAddress.self, from: json)
|
||||
let expected = try MACAddress(address)
|
||||
#expect(decoded == expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user