Files
wehub-resource-sync 680845cb1c
Linux build / Determine Swift version (push) Waiting to run
Linux build / Linux compile check (push) Blocked by required conditions
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
chore: import upstream snapshot with attribution
2026-07-13 12:25:30 +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)
}
}