//===----------------------------------------------------------------------===// // 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 IPv4Address from 6 bytes. /// /// - Parameters: /// - bytes: 6-byte array in network byte order representing the IPv4 address /// - Throws: `AddressError.unableToParse` if the byte array length is not 6 @inlinable public init(_ bytes: [UInt8]) throws { guard bytes.count == 6 else { throw AddressError.unableToParse } self.value = (UInt64(bytes[0]) << 40) | (UInt64(bytes[1]) << 32) | (UInt64(bytes[2]) << 24) | (UInt64(bytes[3]) << 16) | (UInt64(bytes[4]) << 8) | UInt64(bytes[5]) } /// 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) throws -> IPv6Address { let prefixBytes = network.bytes return try 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) } }