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