334 lines
13 KiB
Swift
334 lines
13 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// 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
|
|
}
|
|
}
|
|
|
|
}
|