diff --git a/Sources/GRPCOTelTracingInterceptors/Tracing/SpanAttributes+GRPCTracingKeys.swift b/Sources/GRPCOTelTracingInterceptors/Tracing/SpanAttributes+GRPCTracingKeys.swift index 34f8b0b..23a5296 100644 --- a/Sources/GRPCOTelTracingInterceptors/Tracing/SpanAttributes+GRPCTracingKeys.swift +++ b/Sources/GRPCOTelTracingInterceptors/Tracing/SpanAttributes+GRPCTracingKeys.swift @@ -136,47 +136,91 @@ package enum PeerAddress: Equatable { // - ipv4:: for ipv4 addresses // - ipv6:[]: for ipv6 addresses // - unix: for UNIX domain sockets + let addressUTF8View = address.utf8 // First get the first component so that we know what type of address we're dealing with - let addressComponents = address.split(separator: ":", maxSplits: 1) + let firstColonIndex = addressUTF8View.firstIndex(of: UInt8(ascii: ":")) - guard addressComponents.count > 1 else { + guard let firstColonIndex else { // This is some unexpected/unknown format return nil } + let addressType = addressUTF8View[..]: - // We drop the first character ('[') and split by ']:' to keep two components: the address - // and the port. - let ipv6AddressComponents = addressComponents[1].dropFirst().split(separator: "]:") - if ipv6AddressComponents.count == 2, let port = Int(ipv6AddressComponents[1]) { - self = .ipv6(address: String(ipv6AddressComponents[0]), port: port) - } else { - return nil - } + let hostComponent = addressWithoutType[.. - self = .unixDomainSocket(path: String(addressComponents[1])) + var hostComponent = addressWithoutType[.. + self = .unixDomainSocket(path: String(addressWithoutType) ?? "") + } else { // This is some unexpected/unknown format return nil } } } + +extension Int { + package init?(ipAddressPortStringBytes: some Collection) { + guard (1 ... 5).contains(ipAddressPortStringBytes.count) else { + // Valid IP port values go up to 2^16-1 (65535), which is 5 digits long. + // If the string we get is over 5 characters, we know for sure that this is an invalid port. + // If it's empty, we also know it's invalid as we need at least one digit. + return nil + } + + var value = 0 + for utf8Char in ipAddressPortStringBytes { + value &*= 10 + guard (UInt8(ascii: "0") ... UInt8(ascii: "9")).contains(utf8Char) else { + // non-digit character + return nil + } + value &+= Int(utf8Char - UInt8(ascii: "0")) + } + + guard value <= Int(UInt16.max) else { + // Valid IP port values go up to 2^16-1. + // If a number greater than this was given, it can't be a valid port. + return nil + } + + self = value + } +} diff --git a/Tests/GRPCOTelTracingInterceptorsTests/PeerAddressTests.swift b/Tests/GRPCOTelTracingInterceptorsTests/PeerAddressTests.swift index 70b1dfe..cf24b77 100644 --- a/Tests/GRPCOTelTracingInterceptorsTests/PeerAddressTests.swift +++ b/Tests/GRPCOTelTracingInterceptorsTests/PeerAddressTests.swift @@ -56,4 +56,26 @@ struct PeerAddressTests { let address = PeerAddress(address) #expect(address == nil) } + + @Test( + "Int.init(utf8View:)", + arguments: [ + ("1", 1), + ("21", 21), + ("321", 321), + ("4321", 4321), + ("54321", 54321), + ("65536", nil), // Invalid: over 65535 IP port limit + ("654321", nil), // Invalid: over 5 digits + ("abc", nil), // Invalid: no digits + ("a123", nil), // Invalid: mixed digits and chars outside the valid ascii range for digits + ("123a", nil), // Invalid: mixed digits and chars outside the valid ascii range for digits + ("(123", nil), // Invalid: mixed digits and chars outside the valid ascii range for digits + ("123(", nil), // Invalid: mixed digits and chars outside the valid ascii range for digits + ("", nil), // Invalid: empty string + ] + ) + func testIntInitFromUTF8View(string: String, expectedInt: Int?) async throws { + #expect(expectedInt == Int(ipAddressPortStringBytes: string.utf8)) + } }