Files
J Logan 8da5f2306d Move Bindable to ContainerizationExtras, fix binding bugs. (#481)
- Bindable is generally useful, so it makes sense to move it out of the
netlink library. Removed Equatable conformance from Bindable as this
isn't generally necessary (it's only used for unit tests in netlink).
- Fix some offset checks in a couple data binding functions and improve
the unit tests.
2026-01-21 15:34:10 -08:00

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)
}
}