276 lines
9.6 KiB
Swift
276 lines
9.6 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// 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)
|
|
}
|
|
}
|