Skip to content

Add containerization extras tests #148

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 79 additions & 5 deletions Sources/ContainerizationExtras/AsyncLock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,95 @@

import Foundation

/// `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.
/// An async-safe mutual exclusion lock for coordinating access to shared resources.
///
/// `AsyncLock` provides a familiar locking API with the key benefit that it's safe to call
/// async methods while holding the lock. This addresses scenarios where traditional actors
/// might suffer from reentrancy issues or where you need explicit sequential access control.
///
/// ## Use Cases
/// - Protecting shared mutable state that requires async operations
/// - Coordinating access to resources that don't support concurrent operations
/// - Avoiding actor reentrancy issues in complex async workflows
/// - Ensuring sequential execution of async operations
///
/// ## Example usage:
/// ```swift
/// actor ResourceManager {
/// private let lock = AsyncLock()
/// private var resources: [String] = []
///
/// func addResource(_ name: String) async {
/// await lock.withLock { context in
/// // Async operations are safe within the lock
/// let processedName = await processResourceName(name)
/// resources.append(processedName)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add func appendResource(_ resource: String, context: AsyncLock.Context) to demonstrate how context is used? The required context parameter ensures that the function is called only inside a lock.

/// await notifyObservers(about: processedName)
/// }
/// }
///
/// func getResourceCount() async -> Int {
/// await lock.withLock { context in
/// return resources.count
/// }
/// }
/// }
/// ```
///
/// ## Threading Safety
/// This lock is designed for use within actors or other async contexts and provides
/// mutual exclusion without blocking threads. Operations are queued and resumed
/// sequentially as the lock becomes available.
public actor AsyncLock {
private var busy = false
private var queue: ArraySlice<CheckedContinuation<(), Never>> = []

/// A context object provided to closures executed within the lock.
///
/// The context serves as proof that the code is executing within the lock's
/// critical section. While currently empty, it may be extended in the future
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is empty by design because this is sufficient to serve as proof that the code is executed within the lock.

/// to provide lock-specific functionality.
public struct Context: Sendable {
fileprivate init() {}
}

/// Creates a new AsyncLock instance.
///
/// The lock starts in an unlocked state and is ready for immediate use.
public init() {}

/// withLock provides a scoped locking API to run a function while holding the lock.
/// Executes a closure while holding the lock, ensuring exclusive access.
///
/// - Parameter body: An async closure to execute while holding the lock.
/// The closure receives a `Context` parameter as proof of lock ownership.
/// - Returns: The value returned by the closure
/// - Throws: Any error thrown by the closure
///
/// This method provides scoped locking - the lock is automatically acquired before
/// the closure executes and released when the closure completes (either normally
/// or by throwing an error).
///
/// If the lock is already held, the current operation will suspend until the lock
/// becomes available. Operations are queued and executed in FIFO order.
///
/// ## Example:
/// ```swift
/// let lock = AsyncLock()
/// var counter = 0
///
/// // Safely increment counter with async work
/// let result = await lock.withLock { context in
/// let oldValue = counter
/// await Task.sleep(nanoseconds: 1_000_000) // Simulate async work
/// counter = oldValue + 1
/// return counter
/// }
/// ```
///
/// ## Performance Notes
/// - The lock uses actor isolation, so there's no thread blocking
/// - Suspended operations consume minimal memory
/// - Lock contention is resolved in first-in-first-out order
public func withLock<T: Sendable>(_ body: @Sendable @escaping (Context) async throws -> T) async rethrows -> T {
while self.busy {
await withCheckedContinuation { cc in
Expand Down
158 changes: 143 additions & 15 deletions Sources/ContainerizationExtras/CIDRAddress.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,56 @@
// limitations under the License.
//===----------------------------------------------------------------------===//

/// Describes an IPv4 CIDR address block.
/// Represents an IPv4 CIDR (Classless Inter-Domain Routing) address block.
///
/// A CIDR block defines a range of IP addresses using a base address and a prefix length.
/// This struct provides functionality for subnet calculations, address containment checks,
/// and network overlap detection.
///
/// ## Example usage:
/// ```swift
/// // Create from CIDR notation
/// let cidr = try CIDRAddress("192.168.1.0/24")
/// print(cidr.lower) // 192.168.1.0
/// print(cidr.upper) // 192.168.1.255
///
/// // Check if an address is in the block
/// let testAddr = try IPv4Address("192.168.1.100")
/// print(cidr.contains(ipv4: testAddr)) // true
///
/// // Get address index within the block
/// if let index = cidr.getIndex(testAddr) {
/// print("Address index: \(index)") // 100
/// }
/// ```
public struct CIDRAddress: CustomStringConvertible, Equatable, Sendable {

/// The base IPv4 address of the CIDR block.
/// The base (network) IPv4 address of the CIDR block.
/// This is the lowest address in the range with all host bits set to 0.
public let lower: IPv4Address

/// The last IPv4 address of the CIDR block
/// The broadcast IPv4 address of the CIDR block.
/// This is the highest address in the range with all host bits set to 1.
public let upper: IPv4Address

/// The IPv4 address component of the CIDR block.
/// The IPv4 address component used to create this CIDR block.
/// This may be any address within the block, not necessarily the network address.
public let address: IPv4Address

/// The address prefix length for the CIDR block, which determines its size.
/// The prefix length (subnet mask) for the CIDR block, which determines its size.
/// Valid range is 0-32, where 32 represents a single host and 0 represents all IPv4 addresses.
public let prefixLength: PrefixLength

/// Create an CIDR address block from its text representation.
/// Create a CIDR address block from its text representation.
///
/// - Parameter cidr: A string in CIDR notation (e.g., "192.168.1.0/24")
/// - Throws: `NetworkAddressError.invalidCIDR` if the format is invalid
///
/// ## Example:
/// ```swift
/// let cidr = try CIDRAddress("10.0.0.0/8") // 10.0.0.0 - 10.255.255.255
/// let host = try CIDRAddress("192.168.1.1/32") // Single host
/// ```
public init(_ cidr: String) throws {
let split = cidr.components(separatedBy: "/")
guard split.count == 2 else {
Expand All @@ -48,7 +82,20 @@ public struct CIDRAddress: CustomStringConvertible, Equatable, Sendable {
upper = IPv4Address(fromValue: lower.value + prefixLength.suffixMask32)
}

/// Create a CIDR address from a member IP and a prefix length.
/// Create a CIDR address block from an IP address and prefix length.
///
/// - Parameters:
/// - address: Any IPv4 address within the desired network
/// - prefixLength: The subnet mask length (0-32)
/// - Throws: `NetworkAddressError.invalidCIDR` if the prefix length is invalid
///
/// ## Example:
/// ```swift
/// let addr = try IPv4Address("192.168.1.150")
/// let cidr = try CIDRAddress(addr, prefixLength: 24)
/// print(cidr.description) // "192.168.1.150/24"
/// print(cidr.lower) // "192.168.1.0"
/// ```
public init(_ address: IPv4Address, prefixLength: PrefixLength) throws {
guard prefixLength >= 0 && prefixLength <= 32 else {
throw NetworkAddressError.invalidCIDR(cidr: "\(address)/\(prefixLength)")
Expand All @@ -60,7 +107,23 @@ public struct CIDRAddress: CustomStringConvertible, Equatable, Sendable {
upper = IPv4Address(fromValue: lower.value + prefixLength.suffixMask32)
}

/// Create the smallest CIDR block that includes the lower and upper bounds.
/// Create the smallest CIDR block that encompasses the given address range.
///
/// - Parameters:
/// - lower: The lowest IPv4 address that must be included
/// - upper: The highest IPv4 address that must be included
/// - Throws: `NetworkAddressError.invalidAddressRange` if lower > upper
///
/// This initializer finds the minimal prefix length that creates a CIDR block
/// containing both the lower and upper addresses.
///
/// ## Example:
/// ```swift
/// let start = try IPv4Address("192.168.1.100")
/// let end = try IPv4Address("192.168.1.200")
/// let cidr = try CIDRAddress(lower: start, upper: end)
/// // Results in a block that contains both addresses
/// ```
public init(lower: IPv4Address, upper: IPv4Address) throws {
guard lower.value <= upper.value else {
throw NetworkAddressError.invalidAddressRange(lower: lower.description, upper: upper.description)
Expand All @@ -85,9 +148,25 @@ public struct CIDRAddress: CustomStringConvertible, Equatable, Sendable {
self.upper = upper
}

/// Get the offset of the specified address, relative to the
/// base address for the CIDR block, returning nil if the block
/// does not contain the address.
/// Get the zero-based index of the specified address within this CIDR block.
///
/// - Parameter address: The IPv4 address to find the index for
/// - Returns: The index of the address within the block, or `nil` if not contained
///
/// The index represents the offset from the network base address (lower bound).
/// This is useful for address allocation and iteration within a subnet.
///
/// ## Example:
/// ```swift
/// let cidr = try CIDRAddress("192.168.1.0/24")
/// let addr = try IPv4Address("192.168.1.10")
/// if let index = cidr.getIndex(addr) {
/// print("Address index: \(index)") // 10
/// }
///
/// let outOfRange = try IPv4Address("192.168.2.1")
/// print(cidr.getIndex(outOfRange)) // nil
/// ```
public func getIndex(_ address: IPv4Address) -> UInt32? {
guard address.value >= lower.value && address.value <= upper.value else {
return nil
Expand All @@ -96,35 +175,84 @@ public struct CIDRAddress: CustomStringConvertible, Equatable, Sendable {
return address.value - lower.value
}

/// Return true if the CIDR block contains the specified address.
/// Check if the CIDR block contains the specified IPv4 address.
///
/// - Parameter ipv4: The IPv4 address to test for containment
/// - Returns: `true` if the address is within this CIDR block's range
///
/// ## Example:
/// ```swift
/// let cidr = try CIDRAddress("10.0.0.0/8")
/// print(cidr.contains(ipv4: try IPv4Address("10.5.1.1"))) // true
/// print(cidr.contains(ipv4: try IPv4Address("192.168.1.1"))) // false
/// ```
public func contains(ipv4: IPv4Address) -> Bool {
lower.value <= ipv4.value && ipv4.value <= upper.value
}

/// Return true if the CIDR block contains all addresses of another CIDR block.
/// Check if this CIDR block completely contains another CIDR block.
///
/// - Parameter cidr: The other CIDR block to test for containment
/// - Returns: `true` if the other block is entirely within this block
///
/// ## Example:
/// ```swift
/// let large = try CIDRAddress("192.168.0.0/16") // /16 network
/// let small = try CIDRAddress("192.168.1.0/24") // /24 subnet
/// print(large.contains(cidr: small)) // true
/// print(small.contains(cidr: large)) // false
/// ```
public func contains(cidr: CIDRAddress) -> Bool {
lower.value <= cidr.lower.value && cidr.upper.value <= upper.value
}

/// Return true if the CIDR block shares any addresses with another CIDR block.
/// Check if this CIDR block shares any addresses with another CIDR block.
///
/// - Parameter cidr: The other CIDR block to test for overlap
/// - Returns: `true` if the blocks have any addresses in common
///
/// This method detects any form of overlap: partial overlap, complete containment,
/// or identical ranges.
///
/// ## Example:
/// ```swift
/// let cidr1 = try CIDRAddress("192.168.1.0/24")
/// let cidr2 = try CIDRAddress("192.168.1.128/25")
/// let cidr3 = try CIDRAddress("192.168.2.0/24")
///
/// print(cidr1.overlaps(cidr: cidr2)) // true (cidr2 is subset)
/// print(cidr1.overlaps(cidr: cidr3)) // false (different networks)
/// ```
public func overlaps(cidr: CIDRAddress) -> Bool {
(lower.value <= cidr.lower.value && upper.value >= cidr.lower.value)
|| (upper.value >= cidr.upper.value && lower.value <= cidr.upper.value)
}

/// Retrieve the text representation of the CIDR block.
/// Returns the text representation of the CIDR block in standard notation.
///
/// The format is "address/prefix_length" where address is the original address
/// used to create the block (not necessarily the network address).
public var description: String {
"\(address)/\(prefixLength)"
}
}

// MARK: - Codable Conformance
extension CIDRAddress: Codable {
/// Creates a CIDRAddress from a JSON string representation.
///
/// - Parameter decoder: The decoder to read data from
/// - Throws: `DecodingError` if the string is not valid CIDR notation
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let text = try container.decode(String.self)
try self.init(text)
}

/// Encodes the CIDRAddress as a JSON string in CIDR notation.
///
/// - Parameter encoder: The encoder to write data to
/// - Throws: `EncodingError` if encoding fails
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.description)
Expand Down
Loading
Loading