chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Conforming objects can allocate and free various address types.
|
||||
public protocol AddressAllocator<AddressType>: Sendable {
|
||||
associatedtype AddressType: Sendable
|
||||
|
||||
/// Allocate a new address.
|
||||
func allocate() throws -> AddressType
|
||||
|
||||
/// Attempt to reserve a specific address.
|
||||
func reserve(_ address: AddressType) throws
|
||||
|
||||
/// Free an allocated address.
|
||||
func release(_ address: AddressType) throws
|
||||
|
||||
/// If no addresses are allocated, prevent future allocations and return true.
|
||||
func disableAllocator() -> Bool
|
||||
}
|
||||
|
||||
/// Errors that a type implementing AddressAllocator should throw.
|
||||
public enum AllocatorError: Swift.Error, CustomStringConvertible, Equatable {
|
||||
case allocatorDisabled
|
||||
case allocatorFull
|
||||
case alreadyAllocated(_ address: String)
|
||||
case invalidAddress(_ index: String)
|
||||
case invalidArgument(_ msg: String)
|
||||
case invalidIndex(_ index: Int)
|
||||
case notAllocated(_ address: String)
|
||||
case rangeExceeded
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .allocatorDisabled:
|
||||
return "the allocator is shutting down"
|
||||
case .allocatorFull:
|
||||
return "no free indices are available for allocation"
|
||||
case .alreadyAllocated(let address):
|
||||
return "cannot choose already-allocated address \(address)"
|
||||
case .invalidAddress(let address):
|
||||
return "cannot create index using address \(address)"
|
||||
case .invalidArgument(let msg):
|
||||
return "invalid argument: \(msg)"
|
||||
case .invalidIndex(let index):
|
||||
return "cannot create address using index \(index)"
|
||||
case .notAllocated(let address):
|
||||
return "cannot free unallocated address \(address)"
|
||||
case .rangeExceeded:
|
||||
return "cannot create allocator that overflows maximum address value"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
public struct AddressError: Error, Equatable, Hashable, CustomStringConvertible {
|
||||
public var description: String {
|
||||
String(describing: self.base)
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
enum Base: Equatable, Hashable, Sendable {
|
||||
case unableToParse
|
||||
case invalidZoneIdentifier
|
||||
case invalidIPv4Suffix
|
||||
case multipleEllipsis
|
||||
case invalidHexGroup
|
||||
case malformedAddress
|
||||
case incompleteAddress
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
let base: Base
|
||||
|
||||
@inlinable
|
||||
init(_ base: Base) { self.base = base }
|
||||
|
||||
public static var unableToParse: Self {
|
||||
Self(.unableToParse)
|
||||
}
|
||||
|
||||
public static var invalidZoneIdentifier: Self {
|
||||
Self(.invalidZoneIdentifier)
|
||||
}
|
||||
|
||||
public static var invalidIPv4SuffixInIPv6Address: Self {
|
||||
Self(.invalidIPv4Suffix)
|
||||
}
|
||||
|
||||
public static var multipleEllipsis: Self {
|
||||
Self(.multipleEllipsis)
|
||||
}
|
||||
|
||||
public static var invalidHexGroup: Self {
|
||||
Self(.invalidHexGroup)
|
||||
}
|
||||
|
||||
public static var malformedAddress: Self {
|
||||
Self(.malformedAddress)
|
||||
}
|
||||
|
||||
public static var incompleteAddress: Self {
|
||||
Self(.incompleteAddress)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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 Logging
|
||||
|
||||
/// `AsyncLock` provides a familiar locking API, with the main benefit being that it
|
||||
/// is safe to call async methods while holding the lock. This is primarily used in spots
|
||||
/// where an actor makes sense, but we may need to ensure we don't fall victim to actor
|
||||
/// reentrancy issues.
|
||||
public actor AsyncLock {
|
||||
private var busy = false
|
||||
private var queue: ArraySlice<CheckedContinuation<(), Never>> = []
|
||||
private var log: Logger?
|
||||
|
||||
public struct Context: Sendable {
|
||||
fileprivate init() {}
|
||||
}
|
||||
|
||||
public init(log: Logger? = nil) {
|
||||
self.log = log
|
||||
}
|
||||
|
||||
/// withLock provides a scoped locking API to run a function while holding the lock.
|
||||
public func withLock<T: Sendable>(logMetadata: Logger.Metadata? = nil, _ body: @Sendable @escaping (Context) async throws -> T) async rethrows -> T {
|
||||
log?.debug("acquiring lock", metadata: logMetadata)
|
||||
while self.busy {
|
||||
await withCheckedContinuation { cc in
|
||||
self.queue.append(cc)
|
||||
}
|
||||
}
|
||||
|
||||
self.busy = true
|
||||
|
||||
defer {
|
||||
self.busy = false
|
||||
if let next = self.queue.popFirst() {
|
||||
next.resume(returning: ())
|
||||
} else {
|
||||
self.queue = []
|
||||
}
|
||||
}
|
||||
|
||||
log?.debug("holding lock", metadata: logMetadata)
|
||||
defer { log?.debug("releasing lock", metadata: logMetadata) }
|
||||
let context = Context()
|
||||
return try await body(context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// `AsyncMutex` provides a mutex that protects a piece of data, with the main benefit being that it
|
||||
/// is safe to call async methods while holding the lock. This is primarily used in spots
|
||||
/// where an actor makes sense, but we may need to ensure we don't fall victim to actor
|
||||
/// reentrancy issues.
|
||||
public actor AsyncMutex<T: Sendable> {
|
||||
private final class Box: @unchecked Sendable {
|
||||
var value: T
|
||||
init(_ value: T) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
private var busy = false
|
||||
private var queue: ArraySlice<CheckedContinuation<(), Never>> = []
|
||||
private let box: Box
|
||||
|
||||
public init(_ initialValue: T) {
|
||||
self.box = Box(initialValue)
|
||||
}
|
||||
|
||||
/// withLock provides a scoped locking API to run a function while holding the lock.
|
||||
/// The protected value is passed to the closure for safe access.
|
||||
public func withLock<R: Sendable>(_ body: @Sendable @escaping (inout T) async throws -> R) async rethrows -> R {
|
||||
while self.busy {
|
||||
await withCheckedContinuation { cc in
|
||||
self.queue.append(cc)
|
||||
}
|
||||
}
|
||||
|
||||
self.busy = true
|
||||
|
||||
defer {
|
||||
self.busy = false
|
||||
if let next = self.queue.popFirst() {
|
||||
next.resume(returning: ())
|
||||
} else {
|
||||
self.queue = []
|
||||
}
|
||||
}
|
||||
|
||||
return try await body(&self.box.value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Describes an IPv4 or IPv6 CIDR address block.
|
||||
@frozen
|
||||
public enum CIDR: CustomStringConvertible, Equatable, Sendable, Hashable {
|
||||
|
||||
case v4(IPv4Address, Prefix)
|
||||
case v6(IPv6Address, Prefix)
|
||||
|
||||
/// Create a CIDR address block.
|
||||
public init(_ cidr: String) throws {
|
||||
if let cidrV4 = try? CIDRv4(cidr) {
|
||||
self = .v4(cidrV4.address, cidrV4.prefix)
|
||||
} else if let cidrV6 = try? CIDRv6(cidr) {
|
||||
self = .v6(cidrV6.address, cidrV6.prefix)
|
||||
} else {
|
||||
throw Error.invalidCIDR(cidr: cidr)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a CIDR address from a member IP and a prefix length.
|
||||
public init(_ address: IPAddress, prefix: Prefix) throws {
|
||||
switch address {
|
||||
case .v4(let addr):
|
||||
guard prefix.length <= 32 else {
|
||||
throw Error.invalidCIDR(cidr: "\(address)/\(prefix)")
|
||||
}
|
||||
self = .v4(addr, prefix)
|
||||
case .v6(let addr):
|
||||
guard prefix.length <= 128 else {
|
||||
throw Error.invalidCIDR(cidr: "\(address)/\(prefix)")
|
||||
}
|
||||
self = .v6(addr, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the smallest CIDR block that includes the lower and upper bounds.
|
||||
///
|
||||
/// For type-safe construction, prefer `v4Range(lower:upper:)` or `v6Range(lower:upper:)`.
|
||||
public init(lower: IPAddress, upper: IPAddress) throws {
|
||||
switch (lower, upper) {
|
||||
case (.v4(let lowerAddr), .v4(let upperAddr)):
|
||||
let cidr = try CIDRv4(lower: lowerAddr, upper: upperAddr)
|
||||
self = .v4(cidr.address, cidr.prefix)
|
||||
case (.v6(let lowerAddr), .v6(let upperAddr)):
|
||||
let cidr = try CIDRv6(lower: lowerAddr, upper: upperAddr)
|
||||
self = .v6(cidr.address, cidr.prefix)
|
||||
default:
|
||||
throw Error.invalidAddressRange(lower: lower.description, upper: upper.description)
|
||||
}
|
||||
}
|
||||
|
||||
/// The IP component of this CIDR address.
|
||||
@inlinable
|
||||
public var address: IPAddress {
|
||||
switch self {
|
||||
case .v4(let addr, _):
|
||||
return .v4(addr)
|
||||
case .v6(let addr, _):
|
||||
return .v6(addr)
|
||||
}
|
||||
}
|
||||
|
||||
/// The prefix length of this CIDR address.
|
||||
@inlinable
|
||||
public var prefix: Prefix {
|
||||
switch self {
|
||||
case .v4(_, let prefix), .v6(_, let prefix):
|
||||
return prefix
|
||||
}
|
||||
}
|
||||
|
||||
/// The lowest address in this CIDR block
|
||||
@inlinable
|
||||
public var lower: IPAddress {
|
||||
switch self {
|
||||
case (.v4(let addr, let prefix)):
|
||||
return .v4(IPv4Address(addr.value & prefix.prefixMask32))
|
||||
case (.v6(let addr, let prefix)):
|
||||
return .v6(IPv6Address(addr.value & prefix.prefixMask128))
|
||||
}
|
||||
}
|
||||
|
||||
/// The highest address in this CIDR block (broadcast address).
|
||||
@inlinable
|
||||
public var upper: IPAddress {
|
||||
switch self {
|
||||
case .v4(let addr, let prefix):
|
||||
return .v4(IPv4Address(addr.value | prefix.suffixMask32))
|
||||
case .v6(let addr, let prefix):
|
||||
return .v6(IPv6Address(addr.value | prefix.suffixMask128, zone: addr.zone))
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if the CIDR block contains the specified address.
|
||||
///
|
||||
/// Compares network portion of the given IP address.
|
||||
@inlinable
|
||||
public func contains(_ ip: IPAddress) -> Bool {
|
||||
switch (self, ip) {
|
||||
case (.v4(let network, let prefix), .v4(let ip)):
|
||||
return network.value == (ip.value & prefix.prefixMask32)
|
||||
case (.v6(let network, let prefix), .v6(let ip)):
|
||||
return (network.zone == ip.zone) && (network.value == (ip.value & prefix.prefixMask128))
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve the text representation of the CIDR block.
|
||||
public var description: String {
|
||||
"\(address)/\(prefix)"
|
||||
}
|
||||
}
|
||||
|
||||
extension CIDR {
|
||||
public enum Error: Swift.Error {
|
||||
case invalidCIDR(cidr: String)
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Describes an IPv4 CIDR address block.
|
||||
@frozen
|
||||
public struct CIDRv4: CustomStringConvertible, Equatable, Sendable, Hashable {
|
||||
/// The IP component of this CIDR address.
|
||||
public let address: IPv4Address
|
||||
|
||||
/// The prefix length of this CIDR address.
|
||||
public let prefix: Prefix
|
||||
|
||||
/// Create a CIDR address block.
|
||||
public init(_ cidr: String) throws {
|
||||
let split = cidr.split(separator: "/")
|
||||
guard split.count == 2 else {
|
||||
throw CIDR.Error.invalidCIDR(cidr: cidr)
|
||||
}
|
||||
guard let prefixLength = UInt8(split[1]), let prefix = Prefix(length: prefixLength) else {
|
||||
throw CIDR.Error.invalidCIDR(cidr: cidr)
|
||||
}
|
||||
|
||||
let address = try IPv4Address(String(split[0]))
|
||||
try self.init(address, prefix: prefix)
|
||||
}
|
||||
|
||||
/// Create a CIDR address from a member IP and a prefix length.
|
||||
public init(_ address: IPv4Address, prefix: Prefix) throws {
|
||||
guard prefix.length <= 32 else {
|
||||
throw CIDR.Error.invalidCIDR(cidr: "\(address)/\(prefix)")
|
||||
}
|
||||
self.address = address
|
||||
self.prefix = prefix
|
||||
}
|
||||
|
||||
/// Create the smallest IPv4 CIDR block that includes the lower and upper bounds.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lower: The lower bound IPv4 address
|
||||
/// - upper: The upper bound IPv4 address
|
||||
/// - Returns: The smallest CIDR block containing both addresses
|
||||
/// - Throws: If lower > upper
|
||||
public init(lower: IPv4Address, upper: IPv4Address) throws {
|
||||
guard lower.value <= upper.value else {
|
||||
throw CIDR.Error.invalidAddressRange(lower: lower.description, upper: upper.description)
|
||||
}
|
||||
|
||||
for length in 1...32 {
|
||||
let prefixLength = Prefix(unchecked: UInt8(length))
|
||||
let mask = prefixLength.prefixMask32
|
||||
if (lower.value & mask) != (upper.value & mask) {
|
||||
let prefix = Prefix(unchecked: UInt8(length - 1))
|
||||
let networkAddr = IPv4Address(lower.value & prefix.prefixMask32)
|
||||
try self.init(networkAddr, prefix: prefix)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Same address - /32 block
|
||||
let prefix = Prefix(unchecked: 32)
|
||||
let networkAddr = IPv4Address(lower.value & prefix.prefixMask32)
|
||||
try self.init(networkAddr, prefix: prefix)
|
||||
}
|
||||
|
||||
/// The lowest address in this CIDR block
|
||||
@inlinable
|
||||
public var lower: IPv4Address {
|
||||
IPv4Address(address.value & prefix.prefixMask32)
|
||||
}
|
||||
|
||||
/// The highest address in this CIDR block (broadcast address).
|
||||
@inlinable
|
||||
public var upper: IPv4Address {
|
||||
IPv4Address(address.value | prefix.suffixMask32)
|
||||
}
|
||||
|
||||
/// Return true if the CIDR block contains the specified address.
|
||||
///
|
||||
/// Compares network portion of the given IP address.
|
||||
@inlinable
|
||||
public func contains(_ ip: IPv4Address) -> Bool {
|
||||
(address.value & prefix.prefixMask32) == (ip.value & prefix.prefixMask32)
|
||||
}
|
||||
|
||||
/// Retrieve the text representation of the CIDR block.
|
||||
public var description: String {
|
||||
"\(address)/\(prefix)"
|
||||
}
|
||||
}
|
||||
|
||||
extension CIDRv4: 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)
|
||||
}
|
||||
}
|
||||
|
||||
extension CIDRv4 {
|
||||
/// The gateway address of the network. Conventionally the first usable
|
||||
/// address in the subnet (`lower + 1`).
|
||||
public var gateway: IPv4Address {
|
||||
IPv4Address(self.lower.value + 1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Describes an IPv4 or IPv6 CIDR address block.
|
||||
@frozen
|
||||
public struct CIDRv6: CustomStringConvertible, Equatable, Sendable, Hashable {
|
||||
|
||||
/// The IP component of this CIDR address.
|
||||
public let address: IPv6Address
|
||||
|
||||
/// The prefix length of this CIDR address.
|
||||
public let prefix: Prefix
|
||||
|
||||
/// Create a CIDR address block.
|
||||
public init(_ cidr: String) throws {
|
||||
let split = cidr.split(separator: "/")
|
||||
guard split.count == 2 else {
|
||||
throw CIDR.Error.invalidCIDR(cidr: cidr)
|
||||
}
|
||||
guard let prefixLength = UInt8(split[1]), let prefix = Prefix(length: prefixLength) else {
|
||||
throw CIDR.Error.invalidCIDR(cidr: cidr)
|
||||
}
|
||||
|
||||
let address = try IPv6Address(String(split[0]))
|
||||
try self.init(address, prefix: prefix)
|
||||
}
|
||||
|
||||
/// Create a CIDR address from a member IP and a prefix length.
|
||||
public init(_ address: IPv6Address, prefix: Prefix) throws {
|
||||
guard prefix.length <= 128 else {
|
||||
throw CIDR.Error.invalidCIDR(cidr: "\(address)/\(prefix)")
|
||||
}
|
||||
self.address = address
|
||||
self.prefix = prefix
|
||||
}
|
||||
|
||||
/// Create the smallest IPv6 CIDR block that includes the lower and upper bounds.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - lower: The lower bound IPv6 address
|
||||
/// - upper: The upper bound IPv6 address
|
||||
/// - Returns: The smallest CIDR block containing both addresses
|
||||
/// - Throws: If lower > upper or zones don't match
|
||||
public init(lower: IPv6Address, upper: IPv6Address) throws {
|
||||
guard lower.value <= upper.value && lower.zone == upper.zone else {
|
||||
throw CIDR.Error.invalidAddressRange(lower: lower.description, upper: upper.description)
|
||||
}
|
||||
|
||||
for length in 1...128 {
|
||||
let prefixLength = Prefix(unchecked: UInt8(length))
|
||||
let mask = prefixLength.prefixMask128
|
||||
if (lower.value & mask) != (upper.value & mask) {
|
||||
let prefix = Prefix(unchecked: UInt8(length - 1))
|
||||
let networkAddr = IPv6Address(lower.value & prefix.prefixMask128, zone: lower.zone)
|
||||
try self.init(networkAddr, prefix: prefix)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Same address - /128 block
|
||||
let prefix = Prefix(unchecked: 128)
|
||||
let networkAddr = IPv6Address(lower.value & prefix.prefixMask128, zone: lower.zone)
|
||||
try self.init(networkAddr, prefix: prefix)
|
||||
}
|
||||
|
||||
/// The lowest address in this CIDR block
|
||||
@inlinable
|
||||
public var lower: IPv6Address {
|
||||
IPv6Address(address.value & prefix.prefixMask128)
|
||||
}
|
||||
|
||||
/// The highest address in this CIDR block (broadcast address).
|
||||
@inlinable
|
||||
public var upper: IPv6Address {
|
||||
IPv6Address(address.value | prefix.suffixMask128, zone: address.zone)
|
||||
}
|
||||
|
||||
/// Return true if the CIDR block contains the specified address.
|
||||
///
|
||||
/// Compares network portion of the given IP address.
|
||||
@inlinable
|
||||
public func contains(_ ip: IPv6Address) -> Bool {
|
||||
(address.zone == ip.zone) && ((address.value & prefix.prefixMask128) == (ip.value & prefix.prefixMask128))
|
||||
}
|
||||
|
||||
/// Retrieve the text representation of the CIDR block.
|
||||
public var description: String {
|
||||
"\(address)/\(prefix)"
|
||||
}
|
||||
}
|
||||
|
||||
extension CIDRv6: 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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
|
||||
|
||||
extension FileManager {
|
||||
/// Returns a unique temporary directory to use.
|
||||
public func uniqueTemporaryDirectory(create: Bool = true) -> URL {
|
||||
let tempDirectoryURL = temporaryDirectory
|
||||
let uniqueDirectoryURL = tempDirectoryURL.appendingPathComponent(UUID().uuidString)
|
||||
if create {
|
||||
try? createDirectory(at: uniqueDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
return uniqueDirectoryURL
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Represents an IP address that can be either IPv4 or IPv6.
|
||||
@frozen
|
||||
public enum IPAddress: Sendable, Hashable, CustomStringConvertible, Equatable {
|
||||
/// An IPv4 address
|
||||
case v4(IPv4Address)
|
||||
|
||||
/// An IPv6 address
|
||||
case v6(IPv6Address)
|
||||
|
||||
/// Parses an IP address string, automatically detecting IPv4 or IPv6 format.
|
||||
///
|
||||
/// - Parameter string: IP address string to parse
|
||||
/// - Returns: An `IPAddress` containing either an IPv4 or IPv6 address
|
||||
/// - Throws: `AddressError.unableToParse` if invalid
|
||||
public init(_ string: String) throws {
|
||||
let utf8 = string.utf8
|
||||
var hasColon = false
|
||||
var hasDot = false
|
||||
|
||||
for byte in utf8 {
|
||||
if byte == 58 { // ASCII ':'
|
||||
hasColon = true
|
||||
break
|
||||
}
|
||||
if byte == 46 { // ASCII '.'
|
||||
hasDot = true
|
||||
}
|
||||
}
|
||||
|
||||
if hasColon {
|
||||
let ipv6 = try IPv6Address.parse(string)
|
||||
self = .v6(ipv6)
|
||||
} else if hasDot {
|
||||
let ipv4 = try IPv4Address(string)
|
||||
self = .v4(ipv4)
|
||||
} else {
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
}
|
||||
|
||||
/// String representation of the IP address.
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .v4(let addr):
|
||||
return addr.description
|
||||
case .v6(let addr):
|
||||
return addr.description
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if this is an IPv4 address.
|
||||
@inlinable
|
||||
public var isV4: Bool {
|
||||
if case .v4 = self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/// Returns `true` if this is an IPv6 address.
|
||||
@inlinable
|
||||
public var isV6: Bool {
|
||||
if case .v6 = self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/// Returns the underlying IPv4 address if this is an IPv4 address, otherwise `nil`.
|
||||
@inlinable
|
||||
public var ipv4: IPv4Address? {
|
||||
if case .v4(let addr) = self {
|
||||
return addr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Returns the underlying IPv6 address if this is an IPv6 address, otherwise `nil`.
|
||||
@inlinable
|
||||
public var ipv6: IPv6Address? {
|
||||
if case .v6(let addr) = self {
|
||||
return addr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a loopback address (127.0.0.0/8 or ::1).
|
||||
@inlinable
|
||||
public var isLoopback: Bool {
|
||||
switch self {
|
||||
case .v4(let addr):
|
||||
return addr.isLoopback
|
||||
case .v6(let addr):
|
||||
return addr.isLoopback
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a multicast address.
|
||||
@inlinable
|
||||
public var isMulticast: Bool {
|
||||
switch self {
|
||||
case .v4(let addr):
|
||||
return addr.isMulticast
|
||||
case .v6(let addr):
|
||||
return addr.isMulticast
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if this is an unspecified address (0.0.0.0 or ::).
|
||||
@inlinable
|
||||
public var isUnspecified: Bool {
|
||||
switch self {
|
||||
case .v4(let addr):
|
||||
return addr.isUnspecified
|
||||
case .v6(let addr):
|
||||
return addr.isUnspecified
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
@frozen
|
||||
public struct IPv4Address: Sendable, Hashable, CustomStringConvertible, Equatable, Comparable {
|
||||
public let value: UInt32
|
||||
|
||||
/// Creates an IPv4Address from an unsigned integer.
|
||||
///
|
||||
/// - Parameter string: The integer representation of the address.
|
||||
@inlinable
|
||||
public init(_ value: UInt32) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
/// Creates an IPv4Address from 4 bytes.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - bytes: 4-byte array in network byte order representing the IPv4 address
|
||||
/// - Throws: `AddressError.unableToParse` if the byte array length is not 4
|
||||
@inlinable
|
||||
public init(_ bytes: [UInt8]) throws {
|
||||
guard bytes.count == 4 else {
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
self.value =
|
||||
(UInt32(bytes[0]) << 24)
|
||||
| (UInt32(bytes[1]) << 16)
|
||||
| (UInt32(bytes[2]) << 8)
|
||||
| UInt32(bytes[3])
|
||||
}
|
||||
|
||||
/// Creates an IPv4Address from a string representation.
|
||||
///
|
||||
/// - Parameter string: The IPv4 address string in dotted decimal notation (e.g., "192.168.1.1")
|
||||
/// - Throws: `AddressError.unableToParse` if the string is not a valid IPv4 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: UInt32) -> [UInt8] {
|
||||
var result = [UInt8](repeating: 0, count: 4)
|
||||
result[0] = UInt8((value >> 24) & 0xff)
|
||||
result[1] = UInt8((value >> 16) & 0xff)
|
||||
result[2] = UInt8((value >> 8) & 0xff)
|
||||
result[3] = UInt8(value & 0xff)
|
||||
return result
|
||||
}
|
||||
|
||||
// TODO: spans?
|
||||
@available(macOS 26.0, *)
|
||||
@usableFromInline
|
||||
static func bytes(_ value: UInt32) -> InlineArray<4, UInt8> {
|
||||
let result: InlineArray<4, UInt8> = [
|
||||
UInt8((value >> 24) & 0xff),
|
||||
UInt8((value >> 16) & 0xff),
|
||||
UInt8((value >> 8) & 0xff),
|
||||
UInt8(value & 0xff),
|
||||
]
|
||||
return result
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public var description: String {
|
||||
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
|
||||
}
|
||||
|
||||
/// Parses an IPv4 address string in dotted decimal notation into a UInt32 representation.
|
||||
///
|
||||
/// ## Validation Rules
|
||||
/// - Exactly 4 octets separated by dots
|
||||
/// - Each octet must be 0-255
|
||||
/// - No leading zeros (except for "0" itself)
|
||||
/// - No whitespace characters
|
||||
/// - Only digits and dots allowed
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```swift
|
||||
/// IPv4Address.parse("192.168.1.1") // Returns: 3232235777
|
||||
/// IPv4Address.parse("127.0.0.1") // Returns: 2130706433
|
||||
/// IPv4Address.parse("0.0.0.0") // Returns: 0
|
||||
/// IPv4Address.parse("255.255.255.255") // Returns: 4294967295
|
||||
///
|
||||
/// // Invalid examples:
|
||||
/// IPv4Address.parse("192.168.1") // Wrong number of octets
|
||||
/// IPv4Address.parse("192.168.1.256") // Octet out of range
|
||||
/// IPv4Address.parse("192.168.001.1") // Leading zeros
|
||||
/// IPv4Address.parse(" 192.168.1.1 ") // Whitespace
|
||||
/// ```
|
||||
///
|
||||
/// - Parameter s: The IPv4 address string to parse
|
||||
/// - Returns: The 32-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 -> UInt32 {
|
||||
guard !s.isEmpty, s.count >= 7, s.count <= 15 else {
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
|
||||
// IP addresses should only contain ASCII 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 32bit representation of the IPv4 address
|
||||
var result: UInt32 = 0
|
||||
|
||||
// tracking octet count, max 4 allowed
|
||||
var octetCount = 0
|
||||
var currentOctet = 0
|
||||
|
||||
// number of digits in the string representation of the octet, max 3
|
||||
var digitCount = 0
|
||||
|
||||
for byte in utf8 {
|
||||
if byte == 46 { // ASCII '.'
|
||||
// Validate octet before processing
|
||||
guard octetCount < 3, digitCount > 0, digitCount <= 3, currentOctet <= 255 else {
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
|
||||
// Shift result and add current octet
|
||||
result = (result << 8) | UInt32(currentOctet)
|
||||
|
||||
// Reset for next octet
|
||||
octetCount += 1
|
||||
currentOctet = 0
|
||||
digitCount = 0
|
||||
|
||||
} else if byte >= 48 && byte <= 57 { // ASCII '0'-'9'
|
||||
let digit = Int(byte - 48)
|
||||
|
||||
digitCount += 1
|
||||
|
||||
// Check for invalid leading zeros: "01", "001", etc.
|
||||
// Allow single "0" but reject multi-digit numbers starting with 0
|
||||
if digitCount == 1 && digit == 0 {
|
||||
// First digit is 0 - this is only valid if it's the only digit
|
||||
currentOctet = 0
|
||||
} else if digitCount > 1 && currentOctet == 0 {
|
||||
// We had a leading zero and now have more digits - invalid
|
||||
throw AddressError.unableToParse
|
||||
} else {
|
||||
// Normal case: build the octet value
|
||||
currentOctet = currentOctet * 10 + digit
|
||||
}
|
||||
|
||||
// Early termination if octet becomes too large
|
||||
guard currentOctet <= 255, digitCount <= 3 else {
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
|
||||
} else {
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
}
|
||||
|
||||
// Validate final octet
|
||||
guard octetCount == 3, digitCount > 0, digitCount <= 3, currentOctet <= 255 else {
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
|
||||
return (result << 8) | UInt32(currentOctet)
|
||||
}
|
||||
|
||||
// MARK: - Address Classification Methods
|
||||
|
||||
/// Returns `true` if this is the unspecified address (0.0.0.0).
|
||||
///
|
||||
/// Per RFC 791, 0.0.0.0 is the "this network" address.
|
||||
@inlinable
|
||||
public var isUnspecified: Bool {
|
||||
value == 0
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a loopback address (127.0.0.0/8).
|
||||
///
|
||||
/// Per RFC 1122 Section 3.2.1.3, the entire 127.0.0.0/8 block is reserved for loopback.
|
||||
@inlinable
|
||||
public var isLoopback: Bool {
|
||||
(value & 0xFF00_0000) == 0x7F00_0000
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a multicast address (224.0.0.0/4).
|
||||
///
|
||||
/// Per RFC 1112, addresses in the range 224.0.0.0 to 239.255.255.255 are multicast addresses.
|
||||
@inlinable
|
||||
public var isMulticast: Bool {
|
||||
(value & 0xF000_0000) == 0xE000_0000
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a link-local address (169.254.0.0/16).
|
||||
///
|
||||
/// Per RFC 3927, 169.254.0.0/16 is reserved for link-local addresses (APIPA/Auto-IP).
|
||||
@inlinable
|
||||
public var isLinkLocal: Bool {
|
||||
(value & 0xFFFF_0000) == 0xA9FE_0000
|
||||
}
|
||||
|
||||
/// Returns `true` if this is the limited broadcast address (255.255.255.255).
|
||||
///
|
||||
/// Per RFC 919/922, 255.255.255.255 is the limited broadcast address.
|
||||
@inlinable
|
||||
public var isBroadcast: Bool {
|
||||
value == 0xFFFF_FFFF
|
||||
}
|
||||
|
||||
/// Compares two IPv4 addresses numerically.
|
||||
@inlinable
|
||||
public static func < (lhs: IPv4Address, rhs: IPv4Address) -> Bool {
|
||||
lhs.value < rhs.value
|
||||
}
|
||||
}
|
||||
|
||||
extension IPv4Address: 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
extension IPv6Address {
|
||||
/// Parses an IPv6 address string into an IPv6Address instance.
|
||||
///
|
||||
/// Follows RFC 4291 and RFC 5952.
|
||||
///
|
||||
/// This function supports standard IPv6 notation including:
|
||||
/// - Full addresses: `2001:0db8:0000:0042:0000:8a2e:0370:7334`
|
||||
/// - Zero compression: `2001:db8::8a2e:370:7334`
|
||||
/// - Leading zero omission: `2001:db8:0:42:0:8a2e:370:7334`
|
||||
/// - Unspecified address: `::`
|
||||
/// - Zone identifiers: `fe80::1%eth0`
|
||||
///
|
||||
/// - Parameter input: IPv6 address string (with optional zone identifier after %)
|
||||
/// - Returns: An `IPv6Address` instance representing the parsed address
|
||||
///
|
||||
/// ## Example Usage
|
||||
/// ```swift
|
||||
/// let addr1 = try IPv6Address.parse("2001:db8::1")
|
||||
/// let addr2 = try IPv6Address.parse("::") // Unspecified address
|
||||
/// let addr3 = try IPv6Address.parse("fe80::1%eth0") // With zone identifier
|
||||
/// ```
|
||||
static func parse(_ input: String) throws -> IPv6Address {
|
||||
var ipBytes = [UInt8](repeating: 0, count: 16)
|
||||
var ellipsisPosition: Int?
|
||||
|
||||
// Extract zone identifier
|
||||
let (address, zone) = try extractZoneIdentifier(from: input)
|
||||
|
||||
// RFC 4291 Section 2.2.3: IPv4 suffix must be at the end (last 32 bits)
|
||||
var remainingAddress = address
|
||||
var ipv6ByteLimit = 16 // Maximum bytes available for IPv6 hex groups
|
||||
var hasIPv4Suffix = false
|
||||
|
||||
// check if the IPv6 address has IPv4 address in it.
|
||||
if let (ipv6Part, ipv4Bytes) = try extractIPv4Suffix(from: address) {
|
||||
// If IPv4 present, save directly to last 4 bytes.
|
||||
ipBytes[12] = ipv4Bytes[0]
|
||||
ipBytes[13] = ipv4Bytes[1]
|
||||
ipBytes[14] = ipv4Bytes[2]
|
||||
ipBytes[15] = ipv4Bytes[3]
|
||||
|
||||
// Update address and limit IPv6 parsing to first 12 bytes (6 groups max)
|
||||
remainingAddress = ipv6Part
|
||||
ipv6ByteLimit = 12
|
||||
hasIPv4Suffix = true
|
||||
}
|
||||
|
||||
// Handle leading ellipsis in the IPv6 part
|
||||
if remainingAddress.utf8.starts(with: [58, 58]) { // "::"
|
||||
ellipsisPosition = 0
|
||||
remainingAddress = String(remainingAddress.dropFirst(2))
|
||||
|
||||
// Special case: "::" represents the unspecified address (all zeros)
|
||||
// But if we have IPv4 suffix, the IPv4 bytes are already set correctly
|
||||
if remainingAddress.isEmpty {
|
||||
// If we have IPv4 suffix, ipBytes already has the IPv4 data, just return
|
||||
if hasIPv4Suffix {
|
||||
return try Self(ipBytes, zone: zone)
|
||||
}
|
||||
|
||||
// Pure "::" - Return the unspecified address, handling zone identifiers
|
||||
if let zone = zone, !zone.isEmpty {
|
||||
return try Self(ipBytes, zone: zone)
|
||||
}
|
||||
return .unspecified
|
||||
}
|
||||
}
|
||||
|
||||
// Parse IPv6 hex groups up to the byte limit
|
||||
var byteIndex = 0
|
||||
let utf8 = remainingAddress.utf8
|
||||
var currentPosition = utf8.startIndex
|
||||
|
||||
while byteIndex < ipv6ByteLimit && currentPosition < utf8.endIndex {
|
||||
let (hexValue, nextPosition) = try parseHexadecimal(
|
||||
from: utf8,
|
||||
startingAt: currentPosition
|
||||
)
|
||||
|
||||
// Store the UInt16 in network-byte order
|
||||
ipBytes[byteIndex] = UInt8(hexValue >> 8)
|
||||
ipBytes[byteIndex + 1] = UInt8(hexValue & 0xFF)
|
||||
byteIndex += 2
|
||||
currentPosition = nextPosition
|
||||
|
||||
// Terminate early if we have consumed the whole string
|
||||
if currentPosition == utf8.endIndex {
|
||||
break
|
||||
}
|
||||
|
||||
// Parse separator and handle ellipsis detection
|
||||
currentPosition = try skipColonSeparator(
|
||||
from: utf8,
|
||||
at: currentPosition,
|
||||
currentByteIndex: byteIndex,
|
||||
ellipsisPosition: &ellipsisPosition
|
||||
)
|
||||
}
|
||||
|
||||
// Validate complete consumption of input
|
||||
guard currentPosition >= utf8.endIndex else {
|
||||
throw AddressError.malformedAddress
|
||||
}
|
||||
|
||||
// Apply ellipsis expansion for the IPv6 portion
|
||||
try expandEllipsis(
|
||||
in: &ipBytes,
|
||||
parsedBytes: byteIndex,
|
||||
ellipsisPosition: ellipsisPosition,
|
||||
byteLimit: ipv6ByteLimit
|
||||
)
|
||||
let value = ipBytes.reduce(UInt128(0)) { ($0 << 8) | UInt128($1) }
|
||||
return Self(value, zone: zone)
|
||||
}
|
||||
|
||||
// MARK: - Helper Functions
|
||||
|
||||
/// Extracts IPv4 suffix if present at the end of the address
|
||||
///
|
||||
/// Follows: RFC 4291 Section 2.2.3: Alternative form x:x:x:x:x:x:d.d.d.d
|
||||
/// IPv4 must be the last 32 bits and preceded by a colon
|
||||
///
|
||||
/// - 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: `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 '.'
|
||||
return nil
|
||||
}
|
||||
|
||||
// IPv4 address must be present after last colon
|
||||
guard let lastColonIndex = input.lastIndex(of: ":") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: maybe refactor for performance
|
||||
let afterColon = input.index(after: lastColonIndex)
|
||||
guard afterColon < input.endIndex else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let possibleIPv4 = String(input[afterColon...])
|
||||
guard let ipv4Value = try? IPv4Address.parse(possibleIPv4) else {
|
||||
throw AddressError.invalidIPv4SuffixInIPv6Address
|
||||
}
|
||||
|
||||
// Check if lastColonIndex is the second ':' of '::'. If so, ensure to include it.
|
||||
let isDoubleColon = lastColonIndex > input.startIndex && input[input.index(before: lastColonIndex)] == ":"
|
||||
let ipv6Part = isDoubleColon ? String(input[...lastColonIndex]) : String(input[..<lastColonIndex])
|
||||
return (ipv6Part, IPv4Address.bytes(ipv4Value))
|
||||
}
|
||||
|
||||
/// Extracts zone identifier
|
||||
///
|
||||
/// - Parameter input: The full IPv6 address string with potential zone identifier
|
||||
/// - Returns: Tuple of (address part, optional zone identifier)
|
||||
/// - 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)
|
||||
}
|
||||
|
||||
let zoneStartIndex = input.index(after: percentIndex)
|
||||
guard zoneStartIndex < input.endIndex else {
|
||||
throw AddressError.invalidZoneIdentifier
|
||||
}
|
||||
|
||||
let addressPart = String(input[..<percentIndex])
|
||||
let zoneIdentifier = String(input[zoneStartIndex...])
|
||||
|
||||
return (addressPart, zoneIdentifier)
|
||||
}
|
||||
|
||||
/// Parses a hexadecimal group from an IPv6 address component.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - 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: `AddressError.invalidHexGroup` if no valid hex digits are found
|
||||
///
|
||||
/// ## Example
|
||||
/// ```swift
|
||||
/// let utf8 = "2001:db8::1".utf8
|
||||
/// let (value, nextPos) = try parseHexadecimal(from: utf8, startingAt: utf8.startIndex)
|
||||
/// // value = 0x2001, nextPos points to ':'
|
||||
/// ```
|
||||
@inlinable
|
||||
internal static func parseHexadecimal(
|
||||
from group: String.UTF8View,
|
||||
startingAt startIndex: String.UTF8View.Index
|
||||
) throws -> (UInt16, String.UTF8View.Index) {
|
||||
var accumulator: UInt16 = 0
|
||||
var digitCount = 0
|
||||
var currentIndex = startIndex
|
||||
|
||||
while currentIndex < group.endIndex && digitCount < 4 {
|
||||
let byte = group[currentIndex]
|
||||
|
||||
// Fast hex digit parsing using ASCII values
|
||||
let hexValue: UInt16
|
||||
if byte >= 48 && byte <= 57 { // '0'-'9'
|
||||
hexValue = UInt16(byte - 48)
|
||||
} else if byte >= 65 && byte <= 70 { // 'A'-'F'
|
||||
hexValue = UInt16(byte - 65 + 10)
|
||||
} else if byte >= 97 && byte <= 102 { // 'a'-'f'
|
||||
hexValue = UInt16(byte - 97 + 10)
|
||||
} else {
|
||||
break // Not a hex digit
|
||||
}
|
||||
|
||||
accumulator = (accumulator << 4) + hexValue
|
||||
digitCount += 1
|
||||
currentIndex = group.index(after: currentIndex)
|
||||
}
|
||||
|
||||
guard digitCount > 0 else {
|
||||
// No hex digits found
|
||||
throw AddressError.invalidHexGroup
|
||||
}
|
||||
return (accumulator, currentIndex)
|
||||
}
|
||||
|
||||
/// Parses a colon separator between IPv6 groups and detects ellipsis notation (::).
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - utf8: The UTF-8 view being parsed
|
||||
/// - position: Current position in the UTF-8 view (must point to a colon)
|
||||
/// - currentByteIndex: Current byte index in the IP array
|
||||
/// - ellipsisPosition: Inout parameter tracking ellipsis position
|
||||
/// - Returns: Next position in the UTF-8 view after parsing separator
|
||||
///
|
||||
/// ## Example
|
||||
/// ```swift
|
||||
/// let utf8 = "2001:db8::1".utf8
|
||||
/// var ellipsisPos: Int? = nil
|
||||
/// // After parsing "2001", position points to first ':'
|
||||
/// let nextPos = try skipColonSeparator(from: utf8, at: position,
|
||||
/// currentByteIndex: 2,
|
||||
/// ellipsisPosition: &ellipsisPos)
|
||||
/// // For single colon: nextPos points to 'd' in 'db8'
|
||||
/// // For double colon (::): ellipsisPos = 2, nextPos points to '1'
|
||||
/// ```
|
||||
private static func skipColonSeparator(
|
||||
from group: String.UTF8View,
|
||||
at position: String.UTF8View.Index,
|
||||
currentByteIndex: Int,
|
||||
ellipsisPosition: inout Int?
|
||||
) throws -> String.UTF8View.Index {
|
||||
// Expect colon separator
|
||||
guard group[position] == 58 else { // ASCII ':'
|
||||
throw AddressError.malformedAddress
|
||||
}
|
||||
|
||||
let afterFirstColon = group.index(after: position)
|
||||
guard afterFirstColon < group.endIndex else {
|
||||
// Trailing colon not allowed
|
||||
throw AddressError.malformedAddress
|
||||
}
|
||||
|
||||
// Check for double colon, return position after that
|
||||
if group[afterFirstColon] == 58 { // ASCII ':'
|
||||
guard ellipsisPosition == nil else {
|
||||
// Multiple :: not allowed
|
||||
throw AddressError.multipleEllipsis
|
||||
}
|
||||
ellipsisPosition = currentByteIndex
|
||||
let afterSecondColon = group.index(after: afterFirstColon)
|
||||
return afterSecondColon
|
||||
}
|
||||
return afterFirstColon
|
||||
}
|
||||
|
||||
/// Expands ellipsis for IPv6 addresses
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - ipBytes: Inout array of IP bytes to modify
|
||||
/// - 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: `AddressError.incompleteAddress` for invalid address lengths
|
||||
private static func expandEllipsis(
|
||||
in ipBytes: inout [UInt8],
|
||||
parsedBytes: Int,
|
||||
ellipsisPosition: Int?,
|
||||
byteLimit: Int = 16
|
||||
) throws {
|
||||
guard let ellipsisPosition = ellipsisPosition else {
|
||||
// No ellipsis - validate we have exactly filled the available bytes
|
||||
guard parsedBytes == byteLimit else {
|
||||
throw AddressError.incompleteAddress // Incomplete address without ellipsis
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate expansion within the byte limit
|
||||
let bytesToExpand = byteLimit - parsedBytes
|
||||
guard bytesToExpand > 0 else {
|
||||
throw AddressError.malformedAddress // No room for ellipsis expansion
|
||||
}
|
||||
|
||||
let suffixBytes = Array(ipBytes[ellipsisPosition..<parsedBytes])
|
||||
let targetStartIndex = byteLimit - suffixBytes.count
|
||||
|
||||
// Clear the expansion area using Swift's range-based assignment
|
||||
for i in ellipsisPosition..<targetStartIndex {
|
||||
ipBytes[i] = 0
|
||||
}
|
||||
|
||||
// Place suffix at the end of the IPv6 section using Swift's collection assignment
|
||||
for (offset, byte) in suffixBytes.enumerated() {
|
||||
ipBytes[targetStartIndex + offset] = byte
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Represents an IPv6 network address conforming to RFC 5952 and RFC 4291.
|
||||
public struct IPv6Address: Sendable, Hashable, CustomStringConvertible, Equatable, Comparable {
|
||||
public let value: UInt128
|
||||
|
||||
public let zone: String?
|
||||
|
||||
/// Creates an IPv6Address by parsing a string representation.
|
||||
///
|
||||
/// Supports standard IPv6 formats including compressed notation (::), mixed IPv4 notation, and zone identifiers.
|
||||
///
|
||||
/// - Parameter address: String representation of an IPv6 address
|
||||
/// - Throws: `AddressError` if the string is not a valid IPv6 address
|
||||
public init(_ address: String) throws {
|
||||
self = try Self.parse(address)
|
||||
}
|
||||
|
||||
/// Creates an IPv6Address from 16 bytes.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - bytes: 16-byte array in network byte order representing the IPv6 address
|
||||
/// - zone: Optional zone identifier (e.g., "eth0")
|
||||
/// - Throws: `AddressError.unableToParse` if the byte array length is not 16
|
||||
@inlinable
|
||||
public init(_ bytes: [UInt8], zone: String? = nil) throws {
|
||||
guard bytes.count == 16 else {
|
||||
throw AddressError.unableToParse
|
||||
}
|
||||
|
||||
// Build UInt128 value in chunks to avoid compiler complexity
|
||||
let hh =
|
||||
(UInt128(bytes[0]) << 120) | (UInt128(bytes[1]) << 112) | (UInt128(bytes[2]) << 104)
|
||||
| (UInt128(bytes[3]) << 96)
|
||||
let hl =
|
||||
(UInt128(bytes[4]) << 88) | (UInt128(bytes[5]) << 80) | (UInt128(bytes[6]) << 72)
|
||||
| (UInt128(bytes[7]) << 64)
|
||||
let lh =
|
||||
(UInt128(bytes[8]) << 56) | (UInt128(bytes[9]) << 48) | (UInt128(bytes[10]) << 40)
|
||||
| (UInt128(bytes[11]) << 32)
|
||||
let ll =
|
||||
(UInt128(bytes[12]) << 24) | (UInt128(bytes[13]) << 16) | (UInt128(bytes[14]) << 8) | UInt128(bytes[15])
|
||||
|
||||
self.value = hh | hl | lh | ll
|
||||
self.zone = zone
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public init(_ value: UInt128, zone: String? = nil) {
|
||||
self.value = value
|
||||
self.zone = zone
|
||||
}
|
||||
|
||||
/// Canonical string representation following RFC 5952.
|
||||
public var description: String {
|
||||
// Convert UInt128 value to 16-bit groups
|
||||
let groups: [UInt16] = [
|
||||
UInt16((value >> 112) & 0xFFFF),
|
||||
UInt16((value >> 96) & 0xFFFF),
|
||||
UInt16((value >> 80) & 0xFFFF),
|
||||
UInt16((value >> 64) & 0xFFFF),
|
||||
UInt16((value >> 48) & 0xFFFF),
|
||||
UInt16((value >> 32) & 0xFFFF),
|
||||
UInt16((value >> 16) & 0xFFFF),
|
||||
UInt16(value & 0xFFFF),
|
||||
]
|
||||
|
||||
// Find the longest run of consecutive zeros for :: compression
|
||||
var longestZeroStart = -1
|
||||
var longestZeroLength = 0
|
||||
var currentZeroStart = -1
|
||||
var currentZeroLength = 0
|
||||
|
||||
for (index, group) in groups.enumerated() {
|
||||
if group == 0 {
|
||||
if currentZeroStart == -1 {
|
||||
currentZeroStart = index
|
||||
currentZeroLength = 1
|
||||
} else {
|
||||
currentZeroLength += 1
|
||||
}
|
||||
} else {
|
||||
if currentZeroLength > longestZeroLength {
|
||||
longestZeroStart = currentZeroStart
|
||||
longestZeroLength = currentZeroLength
|
||||
}
|
||||
currentZeroStart = -1
|
||||
currentZeroLength = 0
|
||||
}
|
||||
}
|
||||
if currentZeroLength > longestZeroLength {
|
||||
longestZeroStart = currentZeroStart
|
||||
longestZeroLength = currentZeroLength
|
||||
}
|
||||
|
||||
let useCompression = longestZeroLength >= 2
|
||||
|
||||
var result = ""
|
||||
var index = 0
|
||||
|
||||
while index < 8 {
|
||||
if useCompression && index == longestZeroStart {
|
||||
if index == 0 {
|
||||
result += "::"
|
||||
} else {
|
||||
result += ":"
|
||||
}
|
||||
// Skip the compressed zeros
|
||||
index += longestZeroLength
|
||||
|
||||
// If we compressed to the end, we're done
|
||||
if index >= 8 {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// Add the group in lowercase hex without leading zeros
|
||||
result += String(groups[index], radix: 16, uppercase: false)
|
||||
index += 1
|
||||
|
||||
// Add colon if not at the end
|
||||
if index < 8 {
|
||||
result += ":"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let zone = zone {
|
||||
result += "%" + zone
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public var bytes: [UInt8] {
|
||||
Self.bytes(self.value)
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
internal static func bytes(_ value: UInt128) -> [UInt8] {
|
||||
var result = [UInt8](repeating: 0, count: 16)
|
||||
result[0] = UInt8((value >> 120) & 0xff)
|
||||
result[1] = UInt8((value >> 112) & 0xff)
|
||||
result[2] = UInt8((value >> 104) & 0xff)
|
||||
result[3] = UInt8((value >> 96) & 0xff)
|
||||
result[4] = UInt8((value >> 88) & 0xff)
|
||||
result[5] = UInt8((value >> 80) & 0xff)
|
||||
result[6] = UInt8((value >> 72) & 0xff)
|
||||
result[7] = UInt8((value >> 64) & 0xff)
|
||||
result[8] = UInt8((value >> 56) & 0xff)
|
||||
result[9] = UInt8((value >> 48) & 0xff)
|
||||
result[10] = UInt8((value >> 40) & 0xff)
|
||||
result[11] = UInt8((value >> 32) & 0xff)
|
||||
result[12] = UInt8((value >> 24) & 0xff)
|
||||
result[13] = UInt8((value >> 16) & 0xff)
|
||||
result[14] = UInt8((value >> 8) & 0xff)
|
||||
result[15] = UInt8(value & 0xff)
|
||||
return result
|
||||
}
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
@usableFromInline
|
||||
internal static func bytes(_ value: UInt128) -> InlineArray<16, UInt8> {
|
||||
let result: InlineArray<16, UInt8> = [
|
||||
UInt8((value >> 120) & 0xff),
|
||||
UInt8((value >> 112) & 0xff),
|
||||
UInt8((value >> 104) & 0xff),
|
||||
UInt8((value >> 96) & 0xff),
|
||||
UInt8((value >> 88) & 0xff),
|
||||
UInt8((value >> 80) & 0xff),
|
||||
UInt8((value >> 72) & 0xff),
|
||||
UInt8((value >> 64) & 0xff),
|
||||
UInt8((value >> 56) & 0xff),
|
||||
UInt8((value >> 48) & 0xff),
|
||||
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
|
||||
}
|
||||
|
||||
/// The unspecified IPv6 address (::)
|
||||
public static let unspecified = IPv6Address(0)
|
||||
|
||||
/// The loopback IPv6 address (::1)
|
||||
public static let loopback = IPv6Address(1)
|
||||
|
||||
// MARK: - Address Classification Methods
|
||||
|
||||
/// Returns `true` if this is the unspecified address (::).
|
||||
@inlinable
|
||||
public var isUnspecified: Bool {
|
||||
value == 0
|
||||
}
|
||||
|
||||
/// Returns `true` if this is the loopback address (::1).
|
||||
@inlinable
|
||||
public var isLoopback: Bool {
|
||||
value == 1
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a multicast address (ff00::/8).
|
||||
@inlinable
|
||||
public var isMulticast: Bool {
|
||||
(value >> 120) == 0xFF
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a link-local unicast address (fe80::/10).
|
||||
@inlinable
|
||||
public var isLinkLocal: Bool {
|
||||
(value >> 118) == 0x3FA // fe80::/10 = top 10 bits are 1111111010
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a unique local address (fc00::/7).
|
||||
@inlinable
|
||||
public var isUniqueLocal: Bool {
|
||||
(value >> 121) == 0x7E // fc00::/7 = top 7 bits are 1111110
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a global unicast address.
|
||||
@inlinable
|
||||
public var isGlobalUnicast: Bool {
|
||||
!isUnspecified && !isLoopback && !isMulticast && !isLinkLocal && !isUniqueLocal
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a documentation address (2001:db8::/32).
|
||||
@inlinable
|
||||
public var isDocumentation: Bool {
|
||||
(value >> 96) == 0x2001_0DB8 // 2001:db8::/32
|
||||
}
|
||||
/// Compares two IPv6 addresses numerically, then by zone if values are equal.
|
||||
@inlinable
|
||||
public static func < (lhs: IPv6Address, rhs: IPv6Address) -> Bool {
|
||||
if lhs.value != rhs.value {
|
||||
return lhs.value < rhs.value
|
||||
}
|
||||
// Same value, compare zones lexicographically
|
||||
return (lhs.zone ?? "") < (rhs.zone ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
extension IPv6Address: 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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 Collections
|
||||
import Synchronization
|
||||
|
||||
/// Maps a network address to an array index value, or nil in the case of a domain error.
|
||||
package typealias AddressToIndexTransform<AddressType> = @Sendable (AddressType) -> Int?
|
||||
|
||||
/// Maps an array index value to a network address, or nil in the case of a domain error.
|
||||
package typealias IndexToAddressTransform<AddressType> = @Sendable (Int) -> AddressType?
|
||||
|
||||
package final class IndexedAddressAllocator<AddressType: CustomStringConvertible & Sendable>: AddressAllocator {
|
||||
private class State {
|
||||
var allocations: BitArray
|
||||
var enabled: Bool
|
||||
var allocationCount: Int
|
||||
let addressToIndex: AddressToIndexTransform<AddressType>
|
||||
let indexToAddress: IndexToAddressTransform<AddressType>
|
||||
|
||||
init(
|
||||
size: Int,
|
||||
addressToIndex: @escaping AddressToIndexTransform<AddressType>,
|
||||
indexToAddress: @escaping IndexToAddressTransform<AddressType>
|
||||
) {
|
||||
self.allocations = BitArray.init(repeating: false, count: size)
|
||||
self.enabled = true
|
||||
self.allocationCount = 0
|
||||
self.addressToIndex = addressToIndex
|
||||
self.indexToAddress = indexToAddress
|
||||
}
|
||||
}
|
||||
|
||||
private let state: Mutex<State>
|
||||
|
||||
/// Create an allocator with specified size and index mappings.
|
||||
package init(
|
||||
size: Int,
|
||||
addressToIndex: @escaping AddressToIndexTransform<AddressType>,
|
||||
indexToAddress: @escaping IndexToAddressTransform<AddressType>
|
||||
) {
|
||||
let state = State(
|
||||
size: size,
|
||||
addressToIndex: addressToIndex,
|
||||
indexToAddress: indexToAddress
|
||||
)
|
||||
self.state = Mutex(state)
|
||||
}
|
||||
|
||||
public func allocate() throws -> AddressType {
|
||||
try self.state.withLock { state in
|
||||
guard state.enabled else {
|
||||
throw AllocatorError.allocatorDisabled
|
||||
}
|
||||
|
||||
guard let index = state.allocations.firstIndex(of: false) else {
|
||||
throw AllocatorError.allocatorFull
|
||||
}
|
||||
|
||||
guard let address = state.indexToAddress(index) else {
|
||||
throw AllocatorError.invalidIndex(index)
|
||||
}
|
||||
|
||||
state.allocations[index] = true
|
||||
state.allocationCount += 1
|
||||
return address
|
||||
}
|
||||
}
|
||||
|
||||
package func reserve(_ address: AddressType) throws {
|
||||
try self.state.withLock { state in
|
||||
guard state.enabled else {
|
||||
throw AllocatorError.allocatorDisabled
|
||||
}
|
||||
|
||||
guard let index = state.addressToIndex(address) else {
|
||||
throw AllocatorError.invalidAddress(address.description)
|
||||
}
|
||||
|
||||
guard !state.allocations[index] else {
|
||||
throw AllocatorError.alreadyAllocated("\(address.description)")
|
||||
}
|
||||
|
||||
state.allocations[index] = true
|
||||
state.allocationCount += 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
package func release(_ address: AddressType) throws {
|
||||
try self.state.withLock { state in
|
||||
guard let index = state.addressToIndex(address) else {
|
||||
throw AllocatorError.invalidAddress(address.description)
|
||||
}
|
||||
|
||||
guard state.allocations[index] else {
|
||||
throw AllocatorError.notAllocated("\(address.description)")
|
||||
}
|
||||
|
||||
state.allocations[index] = false
|
||||
state.allocationCount -= 1
|
||||
}
|
||||
}
|
||||
|
||||
package func disableAllocator() -> Bool {
|
||||
self.state.withLock { state in
|
||||
guard state.allocationCount == 0 else {
|
||||
return false
|
||||
}
|
||||
state.enabled = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
extension IPv4Address {
|
||||
/// Creates an allocator for IPv4 addresses.
|
||||
public static func allocator(lower: UInt32, size: Int) throws -> any AddressAllocator<IPv4Address> {
|
||||
// NOTE: 2^31 - 1 size limit in the very improbable case that we run on 32-bit.
|
||||
guard size > 0 && size < Int.max && 0xffff_ffff - lower >= size - 1 else {
|
||||
throw AllocatorError.rangeExceeded
|
||||
}
|
||||
return IndexedAddressAllocator(
|
||||
size: size,
|
||||
addressToIndex: { address in
|
||||
guard address.value >= lower && address.value - lower <= UInt32(size) else {
|
||||
return nil
|
||||
}
|
||||
return Int(address.value - lower)
|
||||
},
|
||||
indexToAddress: { IPv4Address(lower + UInt32($0)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension UInt16 {
|
||||
/// Creates an allocator for TCP/UDP ports and other UInt16 values.
|
||||
public static func allocator(lower: UInt16, size: Int) throws -> any AddressAllocator<UInt16> {
|
||||
guard 0xffff - lower + 1 >= size else {
|
||||
throw AllocatorError.rangeExceeded
|
||||
}
|
||||
|
||||
return IndexedAddressAllocator(
|
||||
size: size,
|
||||
addressToIndex: { address in
|
||||
guard address >= lower && address <= lower + UInt16(size) else {
|
||||
return nil
|
||||
}
|
||||
return Int(address - lower)
|
||||
},
|
||||
indexToAddress: { lower + UInt16($0) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension UInt32 {
|
||||
/// Creates an allocator for vsock ports, or any UInt32 values.
|
||||
public static func allocator(lower: UInt32, size: Int) throws -> any AddressAllocator<UInt32> {
|
||||
guard 0xffff_ffff - lower + 1 >= size else {
|
||||
throw AllocatorError.rangeExceeded
|
||||
}
|
||||
|
||||
return IndexedAddressAllocator(
|
||||
size: size,
|
||||
addressToIndex: { address in
|
||||
guard address >= lower && address <= lower + UInt32(size) else {
|
||||
return nil
|
||||
}
|
||||
return Int(address - lower)
|
||||
},
|
||||
indexToAddress: { lower + UInt32($0) }
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a rotating allocator for vsock ports, or any UInt32 values.
|
||||
public static func rotatingAllocator(lower: UInt32, size: UInt32) throws -> any AddressAllocator<UInt32> {
|
||||
guard 0xffff_ffff - lower + 1 >= size else {
|
||||
throw AllocatorError.rangeExceeded
|
||||
}
|
||||
|
||||
return RotatingAddressAllocator(
|
||||
size: size,
|
||||
addressToIndex: { address in
|
||||
guard address >= lower && address <= lower + UInt32(size) else {
|
||||
return nil
|
||||
}
|
||||
return Int(address - lower)
|
||||
},
|
||||
indexToAddress: { lower + UInt32($0) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Character {
|
||||
private static let deviceLetters = Array("abcdefghijklmnopqrstuvwxyz")
|
||||
|
||||
/// Creates an allocator for block device tags, or any character values.
|
||||
public static func blockDeviceTagAllocator() -> any AddressAllocator<Character> {
|
||||
IndexedAddressAllocator(
|
||||
size: Self.deviceLetters.count,
|
||||
addressToIndex: { address in
|
||||
Self.deviceLetters.firstIndex(of: address)
|
||||
},
|
||||
indexToAddress: { Self.deviceLetters[$0] }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// A network interface's addresses.
|
||||
public struct InterfaceAddress: Sendable, Hashable {
|
||||
public var ipv4Address: CIDRv4
|
||||
public var ipv6Address: CIDRv6?
|
||||
|
||||
public init(ipv4Address: CIDRv4, ipv6Address: CIDRv6? = nil) {
|
||||
self.ipv4Address = ipv4Address
|
||||
self.ipv6Address = ipv6Address
|
||||
}
|
||||
}
|
||||
|
||||
/// A link-scoped route — a destination directly reachable on an interface.
|
||||
public struct LinkRoute: Sendable, Hashable {
|
||||
public var ipv4Destination: IPv4Address?
|
||||
public var ipv4Source: IPv4Address?
|
||||
public var ipv6Destination: IPv6Address?
|
||||
public var ipv6Source: IPv6Address?
|
||||
|
||||
public init(
|
||||
ipv4Destination: IPv4Address? = nil,
|
||||
ipv4Source: IPv4Address? = nil,
|
||||
ipv6Destination: IPv6Address? = nil,
|
||||
ipv6Source: IPv6Address? = nil
|
||||
) {
|
||||
self.ipv4Destination = ipv4Destination
|
||||
self.ipv4Source = ipv4Source
|
||||
self.ipv6Destination = ipv6Destination
|
||||
self.ipv6Source = ipv6Source
|
||||
}
|
||||
}
|
||||
|
||||
/// The default-route gateway for a network interface.
|
||||
public struct DefaultRoute: Sendable, Hashable {
|
||||
public var ipv4Gateway: IPv4Address?
|
||||
public var ipv6Gateway: IPv6Address?
|
||||
|
||||
public init(ipv4Gateway: IPv4Address? = nil, ipv6Gateway: IPv6Address? = nil) {
|
||||
self.ipv4Gateway = ipv4Gateway
|
||||
self.ipv6Gateway = ipv6Gateway
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// CIDR prefix length (e.g., `/24` for a 24-bit network mask).
|
||||
@frozen
|
||||
public struct Prefix: Sendable, CustomStringConvertible, Hashable, Codable {
|
||||
public let length: UInt8
|
||||
|
||||
/// Create a prefix (0-128). Use `ipv4(_:)` or `ipv6(_:)` for version-specific validation.
|
||||
public init?(length: UInt8) {
|
||||
guard length <= 128 else { return nil }
|
||||
self.length = length
|
||||
}
|
||||
|
||||
/// Create an IPv4 prefix (0-32). Returns `nil` if length > 32.
|
||||
public static func ipv4(_ length: UInt8) -> Prefix? {
|
||||
guard length <= 32 else { return nil }
|
||||
return Prefix(unchecked: length)
|
||||
}
|
||||
|
||||
/// Create an IPv6 prefix (0-128). Returns `nil` if length > 128.
|
||||
public static func ipv6(_ length: UInt8) -> Prefix? {
|
||||
guard length <= 128 else { return nil }
|
||||
return Prefix(unchecked: length)
|
||||
}
|
||||
|
||||
/// Internal unchecked initializer for known-valid values.
|
||||
internal init(unchecked length: UInt8) {
|
||||
self.length = length
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
"\(length)"
|
||||
}
|
||||
}
|
||||
|
||||
extension Prefix {
|
||||
/// Computes a 32-bit mask for the suffix (host) portion of an IPv4 address.
|
||||
///
|
||||
/// Example: Prefix `/24` → `0x0000_00FF` (255 host addresses)
|
||||
@inlinable
|
||||
public var suffixMask32: UInt32 {
|
||||
if self.length <= 0 {
|
||||
return 0xffff_ffff
|
||||
}
|
||||
return self.length >= 32 ? 0x0000_0000 : (1 << (32 - self.length)) - 1
|
||||
}
|
||||
|
||||
/// Network portion mask (high-order bits) for IPv4.
|
||||
///
|
||||
/// Example: Prefix `/24` → `0xFFFF_FF00` (255.255.255.0)
|
||||
@inlinable
|
||||
public var prefixMask32: UInt32 {
|
||||
~self.suffixMask32
|
||||
}
|
||||
|
||||
/// Computes a 128-bit mask for the suffix (host) portion of an IPv6 address.
|
||||
///
|
||||
/// Example: Prefix `/64` → `0x0000_0000_0000_0000_FFFF_FFFF_FFFF_FFFF`
|
||||
@inlinable
|
||||
public var suffixMask128: UInt128 {
|
||||
if self.length <= 0 {
|
||||
return UInt128.max
|
||||
}
|
||||
return self.length >= 128 ? 0 : (1 << (128 - self.length)) - 1
|
||||
}
|
||||
|
||||
/// Network portion mask (high-order bits) for IPv6.
|
||||
///
|
||||
/// Example: Prefix `/64` → `0xFFFF_FFFF_FFFF_FFFF_0000_0000_0000_0000`
|
||||
@inlinable
|
||||
public var prefixMask128: UInt128 {
|
||||
~self.suffixMask128
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// A progress update event.
|
||||
public enum ProgressEvent: Sendable {
|
||||
/// The possible values:
|
||||
/// - `add-items`: Increment the number of processed items by `value`.
|
||||
/// - `add-total-items`: Increment the total number of items to process by `value`.
|
||||
/// - `add-size`: Increment the size of processed items by `value`.
|
||||
/// - `add-total-size`: Increment the total size of items to process by `value`.
|
||||
case addItems(Int)
|
||||
case addTotalItems(Int)
|
||||
case addSize(Int64)
|
||||
case addTotalSize(Int64)
|
||||
|
||||
/// The event name.
|
||||
public var event: String {
|
||||
switch self {
|
||||
case .addItems: "add-items"
|
||||
case .addTotalItems: "add-total-items"
|
||||
case .addSize: "add-size"
|
||||
case .addTotalSize: "add-total-size"
|
||||
}
|
||||
}
|
||||
|
||||
/// The event value.
|
||||
public var value: any Sendable {
|
||||
switch self {
|
||||
case .addItems(let value): value
|
||||
case .addTotalItems(let value): value
|
||||
case .addSize(let value): value
|
||||
case .addTotalSize(let value): value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The progress update handler.
|
||||
public typealias ProgressHandler = @Sendable (_ events: [ProgressEvent]) async -> Void
|
||||
@@ -0,0 +1,77 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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 ContainerizationError
|
||||
import Foundation
|
||||
|
||||
/// A small utility to resolve proxy settings (HTTP(S)_PROXY / NO_PROXY).
|
||||
public enum ProxyUtils {
|
||||
/// Resolves the proxy URL for a given host based on environment variables.
|
||||
/// Malformed http_proxy or https_proxy URLs are ignored.
|
||||
/// Uses Go-style handling rules:
|
||||
/// - Uppercase environment variables take priority over lowercase counterparts.
|
||||
/// - Leading dot on no_proxy component implies prefix matching.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scheme: The request scheme.
|
||||
/// - host: The request hostname.
|
||||
/// - env: Environment variables to check, dafaulting to the process environment.
|
||||
///
|
||||
/// - Returns: The proxy URL to use, or `nil` for transparent connection.
|
||||
public static func proxyFromEnvironment(
|
||||
scheme: String?,
|
||||
host: String,
|
||||
env: [String: String] = ProcessInfo.processInfo.environment
|
||||
) -> URL? {
|
||||
guard let scheme else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let httpProxy = env["HTTP_PROXY"] ?? env["http_proxy"]
|
||||
let httpsProxy = env["HTTPS_PROXY"] ?? env["https_proxy"]
|
||||
let noProxy = env["NO_PROXY"] ?? env["no_proxy"]
|
||||
|
||||
// If NO_PROXY matches → skip proxy
|
||||
if let noProxy, shouldBypassProxy(host: host, noProxy: noProxy) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Select proxy based on scheme, defaulting to http.
|
||||
let proxy = scheme == "https" ? httpsProxy : httpProxy
|
||||
guard let proxy, let proxyUrl = URL(string: proxy) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return proxyUrl
|
||||
}
|
||||
|
||||
/// Check if a host should bypass proxy according to NO_PROXY.
|
||||
/// - Example: NO_PROXY=".example.com,localhost,127.0.0.1"
|
||||
private static func shouldBypassProxy(host: String, noProxy: String) -> Bool {
|
||||
let entries = noProxy.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
|
||||
for entry in entries {
|
||||
if entry.isEmpty { continue }
|
||||
if entry == "*" { return true }
|
||||
if host == entry { return true }
|
||||
if entry.hasPrefix("*.") {
|
||||
let suffix = String(entry.dropFirst())
|
||||
if host.hasSuffix(suffix) { return true }
|
||||
}
|
||||
if entry.hasPrefix(".") && host.hasSuffix(entry) { return true }
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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 Synchronization
|
||||
|
||||
package final class RotatingAddressAllocator: AddressAllocator {
|
||||
package typealias AddressType = UInt32
|
||||
|
||||
private struct State {
|
||||
var allocations: [AddressType]
|
||||
var enabled: Bool
|
||||
var allocationCount: Int
|
||||
let addressToIndex: AddressToIndexTransform<AddressType>
|
||||
let indexToAddress: IndexToAddressTransform<AddressType>
|
||||
|
||||
init(
|
||||
size: UInt32,
|
||||
addressToIndex: @escaping AddressToIndexTransform<AddressType>,
|
||||
indexToAddress: @escaping IndexToAddressTransform<AddressType>
|
||||
) {
|
||||
self.allocations = [UInt32](0..<size)
|
||||
self.enabled = true
|
||||
self.allocationCount = 0
|
||||
self.addressToIndex = addressToIndex
|
||||
self.indexToAddress = indexToAddress
|
||||
}
|
||||
}
|
||||
|
||||
private let state: Mutex<State>
|
||||
|
||||
/// Create an allocator with specified size and index mappings.
|
||||
package init(
|
||||
size: UInt32,
|
||||
addressToIndex: @escaping AddressToIndexTransform<AddressType>,
|
||||
indexToAddress: @escaping IndexToAddressTransform<AddressType>
|
||||
) {
|
||||
let state = State(
|
||||
size: size,
|
||||
addressToIndex: addressToIndex,
|
||||
indexToAddress: indexToAddress
|
||||
)
|
||||
self.state = Mutex(state)
|
||||
}
|
||||
|
||||
public func allocate() throws -> AddressType {
|
||||
try self.state.withLock { state in
|
||||
guard state.enabled else {
|
||||
throw AllocatorError.allocatorDisabled
|
||||
}
|
||||
|
||||
guard state.allocations.count > 0 else {
|
||||
throw AllocatorError.allocatorFull
|
||||
}
|
||||
|
||||
let value = state.allocations.removeFirst()
|
||||
|
||||
guard let address = state.indexToAddress(Int(value)) else {
|
||||
throw AllocatorError.invalidIndex(Int(value))
|
||||
}
|
||||
|
||||
state.allocationCount += 1
|
||||
return address
|
||||
}
|
||||
}
|
||||
|
||||
package func reserve(_ address: AddressType) throws {
|
||||
try self.state.withLock { state in
|
||||
guard state.enabled else {
|
||||
throw AllocatorError.allocatorDisabled
|
||||
}
|
||||
|
||||
guard let index = state.addressToIndex(address) else {
|
||||
throw AllocatorError.invalidAddress(address.description)
|
||||
}
|
||||
|
||||
let i = state.allocations.firstIndex(of: UInt32(index))
|
||||
guard let i else {
|
||||
throw AllocatorError.alreadyAllocated("\(address.description)")
|
||||
}
|
||||
|
||||
_ = state.allocations.remove(at: i)
|
||||
state.allocationCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
package func release(_ address: AddressType) throws {
|
||||
try self.state.withLock { state in
|
||||
guard let index = (state.addressToIndex(address)) else {
|
||||
throw AllocatorError.invalidAddress(address.description)
|
||||
}
|
||||
let value = UInt32(index)
|
||||
|
||||
guard !state.allocations.contains(value) else {
|
||||
throw AllocatorError.notAllocated("\(address.description)")
|
||||
}
|
||||
|
||||
state.allocations.append(value)
|
||||
state.allocationCount -= 1
|
||||
}
|
||||
}
|
||||
|
||||
package func disableAllocator() -> Bool {
|
||||
self.state.withLock { state in
|
||||
guard state.allocationCount == 0 else {
|
||||
return false
|
||||
}
|
||||
state.enabled = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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 NIO
|
||||
import NIOSSL
|
||||
|
||||
public enum TLSUtils {
|
||||
|
||||
public static func makeEnvironmentAwareTLSConfiguration() -> TLSConfiguration {
|
||||
var tlsConfig = TLSConfiguration.makeClientConfiguration()
|
||||
|
||||
// Check standard SSL environment variables in priority order
|
||||
let customCAPath =
|
||||
ProcessInfo.processInfo.environment["SSL_CERT_FILE"]
|
||||
?? ProcessInfo.processInfo.environment["CURL_CA_BUNDLE"]
|
||||
?? ProcessInfo.processInfo.environment["REQUESTS_CA_BUNDLE"]
|
||||
|
||||
if let caPath = customCAPath {
|
||||
tlsConfig.trustRoots = .file(caPath)
|
||||
}
|
||||
// else: use .default
|
||||
|
||||
return tlsConfig
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// `Timeout` contains helpers to run an operation and error out if
|
||||
/// the operation does not finish within a provided time.
|
||||
public struct Timeout {
|
||||
/// Performs the passed in `operation` and throws a `CancellationError` if the operation
|
||||
/// doesn't finish in the provided `seconds` amount.
|
||||
public static func run<T: Sendable>(
|
||||
seconds: UInt32,
|
||||
operation: @escaping @Sendable () async throws -> T
|
||||
) async throws -> T {
|
||||
try await withThrowingTaskGroup(of: T.self) { group in
|
||||
group.addTask {
|
||||
try await operation()
|
||||
}
|
||||
|
||||
group.addTask {
|
||||
try await Task.sleep(for: .seconds(seconds))
|
||||
throw CancellationError()
|
||||
}
|
||||
|
||||
guard let result = try await group.next() else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
group.cancelAll()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
public static func run<T: Sendable>(
|
||||
for duration: Duration,
|
||||
operation: @escaping @Sendable () async throws -> T
|
||||
) async throws -> T {
|
||||
try await withThrowingTaskGroup(of: T.self) { group in
|
||||
group.addTask {
|
||||
try await operation()
|
||||
}
|
||||
|
||||
group.addTask {
|
||||
try await Task.sleep(for: duration)
|
||||
throw CancellationError()
|
||||
}
|
||||
|
||||
guard let result = try await group.next() else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
group.cancelAll()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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.
|
||||
// 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
|
||||
|
||||
package enum BindError: Error, CustomStringConvertible {
|
||||
case recvMarshalFailure(type: String, field: String)
|
||||
case sendMarshalFailure(type: String, field: String)
|
||||
|
||||
package var description: String {
|
||||
switch self {
|
||||
case .recvMarshalFailure(let type, let field):
|
||||
return "failed to unmarshal \(type).\(field)"
|
||||
case .sendMarshalFailure(let type, let field):
|
||||
return "failed to marshal \(type).\(field)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
package protocol Bindable: Sendable {
|
||||
static var size: Int { get }
|
||||
func appendBuffer(_ buffer: inout [UInt8], offset: Int) throws -> Int
|
||||
mutating func bindBuffer(_ buffer: inout [UInt8], offset: Int) throws -> Int
|
||||
}
|
||||
|
||||
extension ArraySlice<UInt8> {
|
||||
package func hexEncodedString() -> String {
|
||||
self.map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
}
|
||||
|
||||
extension [UInt8] {
|
||||
package func hexEncodedString() -> String {
|
||||
self.map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
|
||||
package mutating func bind<T>(as type: T.Type, offset: Int = 0, size: Int? = nil) -> UnsafeMutablePointer<T>? {
|
||||
guard self.count >= (size ?? MemoryLayout<T>.size) + offset else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.withUnsafeMutableBytes { $0.baseAddress?.advanced(by: offset).assumingMemoryBound(to: T.self) }
|
||||
}
|
||||
|
||||
package mutating func copyIn<T>(as type: T.Type, value: T, offset: Int = 0, size: Int? = nil) -> Int? {
|
||||
let size = size ?? MemoryLayout<T>.size
|
||||
guard self.count >= size + offset else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.withUnsafeMutableBytes {
|
||||
$0.baseAddress?.advanced(by: offset).assumingMemoryBound(to: T.self).pointee = value
|
||||
return offset + MemoryLayout<T>.size
|
||||
}
|
||||
}
|
||||
|
||||
package func copyOut<T>(as type: T.Type, offset: Int = 0, size: Int? = nil) -> (Int, T)? {
|
||||
guard self.count >= (size ?? MemoryLayout<T>.size) + offset else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.withUnsafeBytes {
|
||||
guard let value = $0.baseAddress?.advanced(by: offset).assumingMemoryBound(to: T.self).pointee else {
|
||||
return nil
|
||||
}
|
||||
return (offset + MemoryLayout<T>.size, value)
|
||||
}
|
||||
}
|
||||
|
||||
package mutating func copyIn(buffer: [UInt8], offset: Int = 0) -> Int? {
|
||||
guard offset + buffer.count <= self.count else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self[offset..<offset + buffer.count] = buffer[0..<buffer.count]
|
||||
return offset + buffer.count
|
||||
}
|
||||
|
||||
package func copyOut(buffer: inout [UInt8], offset: Int = 0) -> Int? {
|
||||
guard offset + buffer.count <= self.count else {
|
||||
return nil
|
||||
}
|
||||
|
||||
buffer[0..<buffer.count] = self[offset..<offset + buffer.count]
|
||||
return offset + buffer.count
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user