From 4bfa4bcebc5370510c440bc5107a14efe50503a1 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Fri, 23 Jan 2026 21:18:15 -0700 Subject: [PATCH 1/3] Sockets: sendmsg/recvmsg using Span and AncillaryMessageBuffer Adds in sendmsg and recvmsg using Span types for the API and URBP for ABI. Uses AncillaryMessageBuffer (previously unused by a syscall). --- .../Sockets/SocketOperations.swift | 164 ++++++++ .../SocketMessagesTests.swift | 357 ++++++++++++++++++ 2 files changed, 521 insertions(+) create mode 100644 Tests/SystemSocketsTests/SocketMessagesTests.swift diff --git a/Sources/SystemSockets/Sockets/SocketOperations.swift b/Sources/SystemSockets/Sockets/SocketOperations.swift index 83ccee01..1d7d3c68 100644 --- a/Sources/SystemSockets/Sockets/SocketOperations.swift +++ b/Sources/SystemSockets/Sockets/SocketOperations.swift @@ -345,6 +345,170 @@ extension SocketDescriptor { } } +// MARK: - Message-based Send and Receive + +@available(System 99, *) +extension SocketDescriptor { + /// Sends a message with optional ancillary data. + /// + /// - Parameters: + /// - data: The data to send. + /// - ancillaryMessages: Optional ancillary (control) messages to send. + /// - address: Optional destination address (for datagram sockets). + /// - flags: Message flags. + /// - retryOnInterrupt: Whether to retry if interrupted. + /// - Returns: The number of bytes sent. + /// + /// The corresponding C function is `sendmsg`. + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) + @_alwaysEmitIntoClient + public func sendMessage( + _ data: RawSpan, + ancillaryMessages: AncillaryMessageBuffer? = nil, + to address: SocketAddress? = nil, + flags: MessageFlags = .none, + retryOnInterrupt: Bool = true + ) throws(Errno) -> Int { + try data.withUnsafeBytes { bytes throws(Errno) -> Int in + try _sendMessage( + bytes, + ancillaryMessages: ancillaryMessages, + to: address, + flags: flags, + retryOnInterrupt: retryOnInterrupt + ).get() + } + } + + @usableFromInline + internal func _sendMessage( + _ buffer: UnsafeRawBufferPointer, + ancillaryMessages: AncillaryMessageBuffer?, + to address: SocketAddress?, + flags: MessageFlags, + retryOnInterrupt: Bool + ) -> Result { + var iov = CInterop.IOVec( + iov_base: UnsafeMutableRawPointer(mutating: buffer.baseAddress), + iov_len: buffer.count + ) + + var msg = CInterop.MsgHdr() + msg.msg_iov = withUnsafeMutablePointer(to: &iov) { $0 } + msg.msg_iovlen = 1 + + // Helper to perform the actual syscall + func doSend() -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_sendmsg(self.rawValue, &msg, flags.rawValue) + } + } + + // Set up ancillary data if provided + if let ancillary = ancillaryMessages { + return ancillary._withUnsafeBytes { controlBuffer in + msg.msg_control = UnsafeMutableRawPointer(mutating: controlBuffer.baseAddress) + msg.msg_controllen = CInterop.SockLen(controlBuffer.count) + + // Set up destination address if provided + if let address = address { + return address.withUnsafePointer { addr, len in + msg.msg_name = UnsafeMutableRawPointer(mutating: addr) + msg.msg_namelen = len + return doSend() + } + } else { + return doSend() + } + } + } else { + // No ancillary messages - set up address if provided + if let address = address { + return address.withUnsafePointer { addr, len in + msg.msg_name = UnsafeMutableRawPointer(mutating: addr) + msg.msg_namelen = len + return doSend() + } + } else { + return doSend() + } + } + } + + /// Receives a message with optional ancillary data. + /// + /// - Parameters: + /// - buffer: The buffer to receive data into. + /// - ancillaryMessages: Buffer to receive ancillary (control) messages. + /// Must have sufficient capacity pre-allocated. + /// - sender: Optional buffer to receive the sender's address. + /// - flags: Message flags. + /// - retryOnInterrupt: Whether to retry if interrupted. + /// - Returns: The number of bytes received. + /// + /// The corresponding C function is `recvmsg`. + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) + @_alwaysEmitIntoClient + public func receiveMessage( + into buffer: inout OutputRawSpan, + ancillaryMessages: inout AncillaryMessageBuffer, + sender: inout SocketAddress?, + flags: MessageFlags = .none, + retryOnInterrupt: Bool = true + ) throws(Errno) -> Int { + try buffer.withUnsafeMutableBytes { buf, count throws(Errno) -> Int in + let bytesRead = try _receiveMessage( + into: buf, + ancillaryMessages: &ancillaryMessages, + sender: &sender, + flags: flags, + retryOnInterrupt: retryOnInterrupt + ).get() + count = bytesRead // Set initialized count to bytes received + return bytesRead + } + } + + @usableFromInline + internal func _receiveMessage( + into buffer: UnsafeMutableRawBufferPointer, + ancillaryMessages: inout AncillaryMessageBuffer, + sender: inout SocketAddress?, + flags: MessageFlags, + retryOnInterrupt: Bool + ) -> Result { + var iov = CInterop.IOVec( + iov_base: buffer.baseAddress, + iov_len: buffer.count + ) + + var msg = CInterop.MsgHdr() + msg.msg_iov = withUnsafeMutablePointer(to: &iov) { $0 } + msg.msg_iovlen = 1 + + return ancillaryMessages._withMutableCInterop(entireCapacity: true) { + controlPtr, controlLen in + msg.msg_control = controlPtr + msg.msg_controllen = controlLen + + // Set up sender address if provided + if sender != nil { + return sender!._withUnsafeMutablePointer { addr, len in + msg.msg_name = UnsafeMutableRawPointer(addr) + msg.msg_namelen = len + return valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_recvmsg(self.rawValue, &msg, flags.rawValue) + } + } + } else { + return valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_recvmsg(self.rawValue, &msg, flags.rawValue) + } + } + } + } +} + // MARK: - Socket Information @available(System 99, *) diff --git a/Tests/SystemSocketsTests/SocketMessagesTests.swift b/Tests/SystemSocketsTests/SocketMessagesTests.swift new file mode 100644 index 00000000..90f3898b --- /dev/null +++ b/Tests/SystemSocketsTests/SocketMessagesTests.swift @@ -0,0 +1,357 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2021 - 2025 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +#if !os(Windows) + +import Testing + +#if SYSTEM_PACKAGE_DARWIN +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(Android) +import Android +#else +#error("Unsupported Platform") +#endif + +@testable import SystemSockets +@testable import SystemPackage + +@Suite("Socket Message Operations") +private struct SocketMessagesTests { + + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) + @Test func sendReceiveMessageBasic() throws { + // Test basic sendMessage/receiveMessage with TCP sockets + let server = try SocketDescriptor.open(.ipv4, .stream, protocol: .tcp) + defer { try? server.close() } + + let serverAddr = SocketAddress(ipv4: IPv4Address.loopback(port: 0)) + try server.bind(to: serverAddr) + try server.listen(backlog: 1) + + var boundAddr = SocketAddress() + try server.getLocalAddress(into: &boundAddr) + let port = boundAddr.ipv4!.port + + let client = try SocketDescriptor.open(.ipv4, .stream, protocol: .tcp) + defer { try? client.close() } + + let connectAddr = SocketAddress(ipv4: IPv4Address.loopback(port: port)) + try client.connect(to: connectAddr) + + let accepted = try server.accept() + defer { try? accepted.close() } + + // Send message without ancillary data + let message = "Hello via sendMessage!" + let messageBytes = Array(message.utf8) + let sent = try messageBytes.withUnsafeBytes { bytes in + let span = RawSpan(_unsafeBytes: bytes) + return try client.sendMessage(span) + } + #expect(sent == messageBytes.count) + + // Receive message + var buffer = [UInt8](repeating: 0, count: 1024) + var recvAncillary = SocketDescriptor.AncillaryMessageBuffer(minimumCapacity: 256) + var sender: SocketAddress? = nil + + let received = try buffer.withUnsafeMutableBytes { buf in + var recvOutput = OutputRawSpan(buffer: buf, initializedCount: 0) + return try accepted.receiveMessage( + into: &recvOutput, + ancillaryMessages: &recvAncillary, + sender: &sender + ) + } + #expect(received == messageBytes.count) + + let receivedMessage = String(decoding: buffer.prefix(received), as: UTF8.self) + #expect(receivedMessage == message) + } + + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) + @Test func sendReceiveMessageUDP() throws { + // Test sendMessage/receiveMessage with UDP datagram sockets + let receiver = try SocketDescriptor.open(.ipv4, .datagram, protocol: .udp) + defer { try? receiver.close() } + + let receiverAddr = SocketAddress(ipv4: IPv4Address.loopback(port: 0)) + try receiver.bind(to: receiverAddr) + + var boundAddr = SocketAddress() + try receiver.getLocalAddress(into: &boundAddr) + let port = boundAddr.ipv4!.port + + let sender = try SocketDescriptor.open(.ipv4, .datagram, protocol: .udp) + defer { try? sender.close() } + + // Send datagram via sendMessage + let message = "UDP via sendMessage" + let messageBytes = Array(message.utf8) + let targetAddr = SocketAddress(ipv4: IPv4Address.loopback(port: port)) + let sent = try messageBytes.withUnsafeBytes { bytes in + let span = RawSpan(_unsafeBytes: bytes) + return try sender.sendMessage(span, to: targetAddr) + } + #expect(sent == messageBytes.count) + + // Receive datagram + var buffer = [UInt8](repeating: 0, count: 1024) + var recvAncillary = SocketDescriptor.AncillaryMessageBuffer(minimumCapacity: 256) + var fromAddr: SocketAddress? = SocketAddress() + + let received = try buffer.withUnsafeMutableBytes { buf in + var recvOutput = OutputRawSpan(buffer: buf, initializedCount: 0) + return try receiver.receiveMessage( + into: &recvOutput, + ancillaryMessages: &recvAncillary, + sender: &fromAddr + ) + } + #expect(received == messageBytes.count) + #expect(fromAddr?.family == .ipv4) + + let receivedMessage = String(decoding: buffer.prefix(received), as: UTF8.self) + #expect(receivedMessage == message) + } + + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) + @Test func sendReceiveMessageWithFileDescriptor() throws { + try withTemporaryFilePath(basename: "socket-fd-test") { tempDir in + // Test file descriptor passing via SCM_RIGHTS over Unix domain sockets + let server = try SocketDescriptor.open(.local, .stream) + defer { try? server.close() } + + let client = try SocketDescriptor.open(.local, .stream) + defer { try? client.close() } + + let socketPath = tempDir.appending("test.sock") + let unixAddr = UnixAddress(socketPath.string)! + let address = SocketAddress(unix: unixAddr) + try server.bind(to: address) + try server.listen(backlog: 1) + + try client.connect(to: address) + let accepted = try server.accept() + defer { try? accepted.close() } + + // Create a temporary file to send + let tempFile = tempDir.appending("test-data.txt") + let fd = try FileDescriptor.open( + tempFile, + .writeOnly, + options: [.create, .truncate], + permissions: [.ownerReadWrite] + ) + + // Write test data to the file + let testData = "File descriptor test data" + _ = try testData.utf8.withContiguousStorageIfAvailable { buffer in + try fd.write(UnsafeRawBufferPointer(buffer)) + } + try fd.close() + + // Reopen for reading to send + let fileToSend = try FileDescriptor.open(tempFile, .readOnly) + + // Build ancillary message with SCM_RIGHTS + var ancillary = SocketDescriptor.AncillaryMessageBuffer() + withUnsafeBytes(of: fileToSend.rawValue) { bytes in + ancillary.appendMessage( + level: SocketDescriptor.ProtocolID(rawValue: SOL_SOCKET), + type: .init(rawValue: CInt(SCM_RIGHTS)), + bytes: bytes + ) + } + + // Send message with file descriptor + let message = "FD attached" + let messageBytes = Array(message.utf8) + let sent = try messageBytes.withUnsafeBytes { bytes in + let span = RawSpan(_unsafeBytes: bytes) + return try client.sendMessage(span, ancillaryMessages: ancillary) + } + #expect(sent == messageBytes.count) + + // Receive message and file descriptor + var buffer = [UInt8](repeating: 0, count: 1024) + var recvAncillary = SocketDescriptor.AncillaryMessageBuffer(minimumCapacity: 256) + var sender: SocketAddress? = nil + + let received = try buffer.withUnsafeMutableBytes { buf in + var recvOutput = OutputRawSpan(buffer: buf, initializedCount: 0) + return try accepted.receiveMessage( + into: &recvOutput, + ancillaryMessages: &recvAncillary, + sender: &sender + ) + } + #expect(received == messageBytes.count) + + // Extract the received file descriptor using CMSG_FIRSTHDR/CMSG_NXTHDR pattern + var receivedFD: CInt? = nil + recvAncillary._withUnsafeBytes { controlData in + guard controlData.count >= MemoryLayout.size else { return } + + let header = controlData.baseAddress!.assumingMemoryBound(to: CInterop.CMsgHdr.self) + if header.pointee.cmsg_level == SOL_SOCKET && + header.pointee.cmsg_type == CInt(SCM_RIGHTS) { + let dataOffset = MemoryLayout.size + let fdPtr = (controlData.baseAddress! + dataOffset).assumingMemoryBound(to: CInt.self) + receivedFD = fdPtr.pointee + } + } + + #expect(receivedFD != nil, "Should have received a file descriptor") + + if let fd = receivedFD { + let receivedFile = FileDescriptor(rawValue: fd) + defer { try? receivedFile.close() } + + // Verify we can read from the received file descriptor + var readBuffer = [UInt8](repeating: 0, count: 1024) + let bytesRead = try readBuffer.withUnsafeMutableBytes { buf in + try receivedFile.read(into: buf) + } + + let fileContent = String(decoding: readBuffer.prefix(bytesRead), as: UTF8.self) + #expect(fileContent == testData, "File content should match") + } + + try fileToSend.close() + } + } + + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) + @Test func sendReceiveMessageWithMultipleFileDescriptors() throws { + try withTemporaryFilePath(basename: "socket-multi-fd") { tempDir in + // Test passing multiple file descriptors at once + let server = try SocketDescriptor.open(.local, .stream) + defer { try? server.close() } + + let client = try SocketDescriptor.open(.local, .stream) + defer { try? client.close() } + + let socketPath = tempDir.appending("test.sock") + let unixAddr = UnixAddress(socketPath.string)! + let address = SocketAddress(unix: unixAddr) + try server.bind(to: address) + try server.listen(backlog: 1) + + try client.connect(to: address) + let accepted = try server.accept() + defer { try? accepted.close() } + + // Create three temporary files + let file1 = tempDir.appending("file1.txt") + let file2 = tempDir.appending("file2.txt") + let file3 = tempDir.appending("file3.txt") + + // Write different content to each file + let testData1 = "First file" + let testData2 = "Second file" + let testData3 = "Third file" + + for (path, data) in [(file1, testData1), (file2, testData2), (file3, testData3)] { + let fd = try FileDescriptor.open(path, .writeOnly, options: [.create, .truncate], permissions: [.ownerReadWrite]) + _ = try data.utf8.withContiguousStorageIfAvailable { buffer in + try fd.write(UnsafeRawBufferPointer(buffer)) + } + try fd.close() + } + + // Open all three for reading to send + let fd1 = try FileDescriptor.open(file1, .readOnly) + let fd2 = try FileDescriptor.open(file2, .readOnly) + let fd3 = try FileDescriptor.open(file3, .readOnly) + + // Build ancillary message with three FDs + var ancillary = SocketDescriptor.AncillaryMessageBuffer() + var fds = [fd1.rawValue, fd2.rawValue, fd3.rawValue] + fds.withUnsafeBytes { bytes in + ancillary.appendMessage( + level: SocketDescriptor.ProtocolID(rawValue: SOL_SOCKET), + type: .init(rawValue: CInt(SCM_RIGHTS)), + bytes: bytes + ) + } + + // Send message with three file descriptors + let message = "Three FDs attached" + let messageBytes = Array(message.utf8) + let sent = try messageBytes.withUnsafeBytes { bytes in + let span = RawSpan(_unsafeBytes: bytes) + return try client.sendMessage(span, ancillaryMessages: ancillary) + } + #expect(sent == messageBytes.count) + + // Receive message and file descriptors + var buffer = [UInt8](repeating: 0, count: 1024) + var recvAncillary = SocketDescriptor.AncillaryMessageBuffer(minimumCapacity: 256) + var sender: SocketAddress? = nil + + let received = try buffer.withUnsafeMutableBytes { buf in + var recvOutput = OutputRawSpan(buffer: buf, initializedCount: 0) + return try accepted.receiveMessage( + into: &recvOutput, + ancillaryMessages: &recvAncillary, + sender: &sender + ) + } + #expect(received == messageBytes.count) + + // Extract the three received file descriptors + var receivedFDs: [CInt] = [] + recvAncillary._withUnsafeBytes { controlData in + guard controlData.count >= MemoryLayout.size else { return } + + let header = controlData.baseAddress!.assumingMemoryBound(to: CInterop.CMsgHdr.self) + if header.pointee.cmsg_level == SOL_SOCKET && + header.pointee.cmsg_type == CInt(SCM_RIGHTS) { + let dataOffset = MemoryLayout.size + let dataSize = Int(header.pointee.cmsg_len) - dataOffset + let fdCount = dataSize / MemoryLayout.size + + let fdsPtr = (controlData.baseAddress! + dataOffset).assumingMemoryBound(to: CInt.self) + for i in 0.. Date: Fri, 23 Jan 2026 22:58:49 -0700 Subject: [PATCH 2/3] Migrate to Span types in socket I/O --- Sources/Samples/Connect.swift | 11 +- Sources/Samples/Listen.swift | 11 +- Sources/System/FileOperations.swift | 24 +-- .../Sockets/SocketMessages.swift | 27 +-- .../Sockets/SocketOperations.swift | 177 ++++++++++-------- .../AncillaryMessageBufferTests.swift | 83 ++++++-- .../SocketMessagesTests.swift | 8 +- .../SocketOperationsTests.swift | 119 ++++++++++-- 8 files changed, 311 insertions(+), 149 deletions(-) diff --git a/Sources/Samples/Connect.swift b/Sources/Samples/Connect.swift index 889da894..c0aebdb1 100644 --- a/Sources/Samples/Connect.swift +++ b/Sources/Samples/Connect.swift @@ -24,6 +24,7 @@ struct Connect: ParsableCommand { @Option(name: .shortAndLong, help: "Message to send") var message: String = "Hello from swift-system sockets!" + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) func run() throws { print("Resolving \(host)...") @@ -48,15 +49,17 @@ struct Connect: ParsableCommand { // Send message let messageBytes = Array(message.utf8) - let sent = try messageBytes.withUnsafeBytes { buffer in - try socket.send(UnsafeRawBufferPointer(buffer)) + let sent = try messageBytes.withUnsafeBytes { bytes in + let span = RawSpan(_unsafeBytes: bytes) + return try socket.send(span) } print("Sent \(sent) bytes: \(message)") // Receive response var buffer = [UInt8](repeating: 0, count: 4096) - let received = try buffer.withUnsafeMutableBytes { buffer in - try socket.receive(into: buffer) + let received = try buffer.withUnsafeMutableBytes { buf in + var output = OutputRawSpan(buffer: buf, initializedCount: 0) + return try socket.receive(into: &output) } if received > 0 { diff --git a/Sources/Samples/Listen.swift b/Sources/Samples/Listen.swift index b1c789bb..c0d116d3 100644 --- a/Sources/Samples/Listen.swift +++ b/Sources/Samples/Listen.swift @@ -21,6 +21,7 @@ struct Listen: ParsableCommand { @Option(name: .shortAndLong, help: "Maximum number of connections to accept (0 for unlimited)") var maxConnections: Int = 0 + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) func run() throws { // Create socket let server = try SocketDescriptor.open(.ipv4, .stream, protocol: .tcp) @@ -56,8 +57,9 @@ struct Listen: ParsableCommand { var buffer = [UInt8](repeating: 0, count: 4096) while true { - let received = try buffer.withUnsafeMutableBytes { buffer in - try client.receive(into: buffer) + let received = try buffer.withUnsafeMutableBytes { buf in + var output = OutputRawSpan(buffer: buf, initializedCount: 0) + return try client.receive(into: &output) } if received == 0 { print("[\(connectionCount)] Client disconnected") @@ -68,8 +70,9 @@ struct Listen: ParsableCommand { print("[\(connectionCount)] Received \(received) bytes: \(message)") // Echo back - let sent = try buffer.prefix(received).withUnsafeBytes { buffer in - try client.send(UnsafeRawBufferPointer(buffer)) + let sent = try buffer.prefix(received).withUnsafeBytes { bytes in + let span = RawSpan(_unsafeBytes: bytes) + return try client.send(span) } print("[\(connectionCount)] Echoed \(sent) bytes") } diff --git a/Sources/System/FileOperations.swift b/Sources/System/FileOperations.swift index 539a15f2..5c62f113 100644 --- a/Sources/System/FileOperations.swift +++ b/Sources/System/FileOperations.swift @@ -492,21 +492,15 @@ extension FileDescriptor { into buffer: inout OutputRawSpan, retryOnInterrupt: Bool = true ) throws(Errno) -> Int { - do { - return try buffer.withUnsafeMutableBytes { buf, count in - // Read into the uninitialized portion (starting at offset 'count') - let uninitializedPortion = UnsafeMutableRawBufferPointer( - start: buf.baseAddress?.advanced(by: count), - count: buf.count - count - ) - let bytesRead = try read(fromAbsoluteOffset: offset, into: uninitializedPortion, retryOnInterrupt: retryOnInterrupt) - count += bytesRead // Add to existing count, don't replace it! - return bytesRead - } - } catch let error as Errno { - throw error - } catch { - fatalError("Unexpected error type") + try buffer.withUnsafeMutableBytes { buf, count throws(Errno) -> Int in + // Read into the uninitialized portion (starting at offset 'count') + let uninitializedPortion = UnsafeMutableRawBufferPointer( + start: buf.baseAddress?.advanced(by: count), + count: buf.count - count + ) + let bytesRead = try _read(fromAbsoluteOffset: offset, into: uninitializedPortion, retryOnInterrupt: retryOnInterrupt).get() + count += bytesRead // Add to existing count, don't replace it! + return bytesRead } } } diff --git a/Sources/SystemSockets/Sockets/SocketMessages.swift b/Sources/SystemSockets/Sockets/SocketMessages.swift index ec488d4d..7e505691 100644 --- a/Sources/SystemSockets/Sockets/SocketMessages.swift +++ b/Sources/SystemSockets/Sockets/SocketMessages.swift @@ -70,23 +70,26 @@ extension SocketDescriptor { /// - Complexity: Amortized O(`data.count`), when averaged over multiple /// calls. This method reallocates the buffer if there isn't enough /// capacity or if the storage is shared with another value. + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) public mutating func appendMessage( level: SocketDescriptor.ProtocolID, type: SocketDescriptor.Option, - bytes: UnsafeRawBufferPointer + bytes: RawSpan ) { - appendMessage( - level: level, - type: type, - unsafeUninitializedCapacity: bytes.count - ) { buffer in - assert(buffer.count >= bytes.count) - if bytes.count > 0 { - buffer.baseAddress!.copyMemory( - from: bytes.baseAddress!, - byteCount: bytes.count) + bytes.withUnsafeBytes { buffer in + appendMessage( + level: level, + type: type, + unsafeUninitializedCapacity: buffer.count + ) { dest in + assert(dest.count >= buffer.count) + if buffer.count > 0 { + dest.baseAddress!.copyMemory( + from: buffer.baseAddress!, + byteCount: buffer.count) + } + return buffer.count } - return bytes.count } } diff --git a/Sources/SystemSockets/Sockets/SocketOperations.swift b/Sources/SystemSockets/Sockets/SocketOperations.swift index 1d7d3c68..7bb2bf66 100644 --- a/Sources/SystemSockets/Sockets/SocketOperations.swift +++ b/Sources/SystemSockets/Sockets/SocketOperations.swift @@ -204,24 +204,6 @@ extension SocketDescriptor { @available(System 99, *) extension SocketDescriptor { - /// Sends data on the socket. - /// - /// - Parameters: - /// - buffer: The data to send. - /// - flags: Message flags. - /// - retryOnInterrupt: Whether to retry if interrupted. - /// - Returns: The number of bytes sent. - /// - /// The corresponding C function is `send`. - @_alwaysEmitIntoClient - public func send( - _ buffer: UnsafeRawBufferPointer, - flags: MessageFlags = .none, - retryOnInterrupt: Bool = true - ) throws -> Int { - try _send(buffer, flags: flags, retryOnInterrupt: retryOnInterrupt).get() - } - @usableFromInline internal func _send( _ buffer: UnsafeRawBufferPointer, @@ -233,22 +215,25 @@ extension SocketDescriptor { } } - /// Receives data from the socket. + /// Sends data on the socket. /// /// - Parameters: - /// - buffer: The buffer to receive data into. + /// - data: The data to send. /// - flags: Message flags. /// - retryOnInterrupt: Whether to retry if interrupted. - /// - Returns: The number of bytes received. + /// - Returns: The number of bytes sent. /// - /// The corresponding C function is `recv`. + /// The corresponding C function is `send`. + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) @_alwaysEmitIntoClient - public func receive( - into buffer: UnsafeMutableRawBufferPointer, + public func send( + _ data: RawSpan, flags: MessageFlags = .none, retryOnInterrupt: Bool = true - ) throws -> Int { - try _receive(into: buffer, flags: flags, retryOnInterrupt: retryOnInterrupt).get() + ) throws(Errno) -> Int { + try data.withUnsafeBytes { bytes throws(Errno) -> Int in + try _send(bytes, flags: flags, retryOnInterrupt: retryOnInterrupt).get() + } } @usableFromInline @@ -262,24 +247,30 @@ extension SocketDescriptor { } } - /// Sends data to a specific address. + /// Receives data from the socket. /// /// - Parameters: - /// - buffer: The data to send. - /// - address: The destination address. + /// - buffer: The buffer to receive data into. /// - flags: Message flags. /// - retryOnInterrupt: Whether to retry if interrupted. - /// - Returns: The number of bytes sent. + /// - Returns: The number of bytes received. /// - /// The corresponding C function is `sendto`. + /// After receiving, + /// this method sets the buffer's initialized count to the number of bytes received. + /// + /// The corresponding C function is `recv`. + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) @_alwaysEmitIntoClient - public func send( - _ buffer: UnsafeRawBufferPointer, - to address: SocketAddress, + public func receive( + into buffer: inout OutputRawSpan, flags: MessageFlags = .none, retryOnInterrupt: Bool = true - ) throws -> Int { - try _send(buffer, to: address, flags: flags, retryOnInterrupt: retryOnInterrupt).get() + ) throws(Errno) -> Int { + try buffer.withUnsafeMutableBytes { buf, count throws(Errno) -> Int in + let bytesRead = try _receive(into: buf, flags: flags, retryOnInterrupt: retryOnInterrupt).get() + count = bytesRead + return bytesRead + } } @usableFromInline @@ -303,24 +294,27 @@ extension SocketDescriptor { } } - /// Receives data and returns the sender's address. + /// Sends data to a specific address. /// /// - Parameters: - /// - buffer: The buffer to receive data into. - /// - sender: A buffer to receive the sender's address. + /// - data: The data to send. + /// - address: The destination address. /// - flags: Message flags. /// - retryOnInterrupt: Whether to retry if interrupted. - /// - Returns: The number of bytes received. + /// - Returns: The number of bytes sent. /// - /// The corresponding C function is `recvfrom`. + /// The corresponding C function is `sendto`. + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) @_alwaysEmitIntoClient - public func receive( - into buffer: UnsafeMutableRawBufferPointer, - sender: inout SocketAddress, + public func send( + _ data: RawSpan, + to address: SocketAddress, flags: MessageFlags = .none, retryOnInterrupt: Bool = true - ) throws -> Int { - try _receive(into: buffer, sender: &sender, flags: flags, retryOnInterrupt: retryOnInterrupt).get() + ) throws(Errno) -> Int { + try data.withUnsafeBytes { bytes throws(Errno) -> Int in + try _send(bytes, to: address, flags: flags, retryOnInterrupt: retryOnInterrupt).get() + } } @usableFromInline @@ -343,6 +337,34 @@ extension SocketDescriptor { } } } + + /// Receives data and returns the sender's address. + /// + /// - Parameters: + /// - buffer: The buffer to receive data into. + /// - sender: A buffer to receive the sender's address. + /// - flags: Message flags. + /// - retryOnInterrupt: Whether to retry if interrupted. + /// - Returns: The number of bytes received. + /// + /// After receiving, + /// this method sets the buffer's initialized count to the number of bytes received. + /// + /// The corresponding C function is `recvfrom`. + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) + @_alwaysEmitIntoClient + public func receive( + into buffer: inout OutputRawSpan, + sender: inout SocketAddress, + flags: MessageFlags = .none, + retryOnInterrupt: Bool = true + ) throws(Errno) -> Int { + try buffer.withUnsafeMutableBytes { buf, count throws(Errno) -> Int in + let bytesRead = try _receive(into: buf, sender: &sender, flags: flags, retryOnInterrupt: retryOnInterrupt).get() + count = bytesRead + return bytesRead + } + } } // MARK: - Message-based Send and Receive @@ -397,42 +419,29 @@ extension SocketDescriptor { msg.msg_iov = withUnsafeMutablePointer(to: &iov) { $0 } msg.msg_iovlen = 1 - // Helper to perform the actual syscall - func doSend() -> Result { - valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + func doSend(address: SocketAddress?) -> Result { + if let address = address { + return address.withUnsafePointer { addr, len in + msg.msg_name = UnsafeMutableRawPointer(mutating: addr) + msg.msg_namelen = len + return valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_sendmsg(self.rawValue, &msg, flags.rawValue) + } + } + } + return valueOrErrno(retryOnInterrupt: retryOnInterrupt) { system_sendmsg(self.rawValue, &msg, flags.rawValue) } } - // Set up ancillary data if provided if let ancillary = ancillaryMessages { return ancillary._withUnsafeBytes { controlBuffer in msg.msg_control = UnsafeMutableRawPointer(mutating: controlBuffer.baseAddress) msg.msg_controllen = CInterop.SockLen(controlBuffer.count) - - // Set up destination address if provided - if let address = address { - return address.withUnsafePointer { addr, len in - msg.msg_name = UnsafeMutableRawPointer(mutating: addr) - msg.msg_namelen = len - return doSend() - } - } else { - return doSend() - } - } - } else { - // No ancillary messages - set up address if provided - if let address = address { - return address.withUnsafePointer { addr, len in - msg.msg_name = UnsafeMutableRawPointer(mutating: addr) - msg.msg_namelen = len - return doSend() - } - } else { - return doSend() + return doSend(address: address) } } + return doSend(address: address) } /// Receives a message with optional ancillary data. @@ -446,6 +455,9 @@ extension SocketDescriptor { /// - retryOnInterrupt: Whether to retry if interrupted. /// - Returns: The number of bytes received. /// + /// After receiving, + /// this method sets the buffer's initialized count to the number of bytes received. + /// /// The corresponding C function is `recvmsg`. @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) @_alwaysEmitIntoClient @@ -486,12 +498,7 @@ extension SocketDescriptor { msg.msg_iov = withUnsafeMutablePointer(to: &iov) { $0 } msg.msg_iovlen = 1 - return ancillaryMessages._withMutableCInterop(entireCapacity: true) { - controlPtr, controlLen in - msg.msg_control = controlPtr - msg.msg_controllen = controlLen - - // Set up sender address if provided + func doRecv(sender: inout SocketAddress?) -> Result { if sender != nil { return sender!._withUnsafeMutablePointer { addr, len in msg.msg_name = UnsafeMutableRawPointer(addr) @@ -500,11 +507,17 @@ extension SocketDescriptor { system_recvmsg(self.rawValue, &msg, flags.rawValue) } } - } else { - return valueOrErrno(retryOnInterrupt: retryOnInterrupt) { - system_recvmsg(self.rawValue, &msg, flags.rawValue) - } } + return valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_recvmsg(self.rawValue, &msg, flags.rawValue) + } + } + + return ancillaryMessages._withMutableCInterop(entireCapacity: true) { + controlPtr, controlLen in + msg.msg_control = controlPtr + msg.msg_controllen = controlLen + return doRecv(sender: &sender) } } } diff --git a/Tests/SystemSocketsTests/AncillaryMessageBufferTests.swift b/Tests/SystemSocketsTests/AncillaryMessageBufferTests.swift index 520a0884..2b3fbb2f 100644 --- a/Tests/SystemSocketsTests/AncillaryMessageBufferTests.swift +++ b/Tests/SystemSocketsTests/AncillaryMessageBufferTests.swift @@ -8,36 +8,89 @@ */ import Testing + +#if SYSTEM_PACKAGE_DARWIN +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(Android) +import Android +#else +#error("Unsupported Platform") +#endif + import SystemPackage @testable import SystemSockets @Suite("Ancillary Message Buffer") struct AncillaryMessageBufferTests { + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) @Test("Append and iterate messages") func testAppend() { - // Create a buffer of 100 messages, with varying payload lengths. + // Create a buffer of 100 messages with varying payload lengths (0-99 bytes). + // Use a realistic level/type combination for all messages. + let level = SocketDescriptor.ProtocolID(rawValue: SOL_SOCKET) + let type = SocketDescriptor.Option(rawValue: CInt(SCM_RIGHTS)) + var buffer = SocketDescriptor.AncillaryMessageBuffer(minimumCapacity: 0) for i in 0 ..< 100 { let bytes = UnsafeMutableRawBufferPointer.allocate(byteCount: i, alignment: 1) defer { bytes.deallocate() } system_memset(bytes, to: UInt8(i)) - buffer.appendMessage(level: .init(rawValue: CInt(100 * i)), - type: .init(rawValue: CInt(1000 * i)), - bytes: UnsafeRawBufferPointer(bytes)) + let span = RawSpan(_unsafeBytes: UnsafeRawBufferPointer(bytes)) + buffer.appendMessage(level: level, type: type, bytes: span) } + // Check that we can access appended messages. - var i = 0 - for message in buffer { - #expect(Int(message.level.rawValue) == 100 * i) - #expect(Int(message.type.rawValue) == 1000 * i) - message.withUnsafeBytes { buffer in - #expect(buffer.count == i) - for idx in buffer.indices { - #expect(buffer[idx] == UInt8(i), "byte #\(idx)") - } + for (i, message) in buffer.enumerated() { + #expect(message.level == level) + #expect(message.type == type) + message.withUnsafeBytes { bytes in + #expect(bytes.count == i) + #expect(bytes.allSatisfy { $0 == UInt8(i) }) + } + } + #expect(buffer.count == 100) + } + + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) + @Test("Append messages using Span") + func testAppendSpan() { + struct TestMessage { + let level: SocketDescriptor.ProtocolID + let type: SocketDescriptor.Option + let fillByte: UInt8 + let size: Int + } + + let testMessages = [ + TestMessage(level: .init(rawValue: 1), type: .init(rawValue: 100), fillByte: 42, size: 8), + TestMessage(level: .init(rawValue: 2), type: .init(rawValue: 200), fillByte: 99, size: 16), + TestMessage(level: .init(rawValue: 3), type: .init(rawValue: 300), fillByte: 123, size: 4), + ] + + var buffer = SocketDescriptor.AncillaryMessageBuffer(minimumCapacity: 0) + + // Append all messages using RawSpan API + for testMsg in testMessages { + let testData = [UInt8](repeating: testMsg.fillByte, count: testMsg.size) + testData.withUnsafeBytes { bytes in + let span = RawSpan(_unsafeBytes: bytes) + buffer.appendMessage(level: testMsg.level, type: testMsg.type, bytes: span) + } + } + + // Verify all messages were appended correctly + #expect(buffer.count == testMessages.count) + for (message, expected) in zip(buffer, testMessages) { + #expect(message.level == expected.level) + #expect(message.type == expected.type) + message.withUnsafeBytes { bytes in + #expect(bytes.count == expected.size) + #expect(bytes.allSatisfy { $0 == expected.fillByte }) } - i += 1 } - #expect(i == 100, "Too many messages in buffer") } } diff --git a/Tests/SystemSocketsTests/SocketMessagesTests.swift b/Tests/SystemSocketsTests/SocketMessagesTests.swift index 90f3898b..f5459138 100644 --- a/Tests/SystemSocketsTests/SocketMessagesTests.swift +++ b/Tests/SystemSocketsTests/SocketMessagesTests.swift @@ -168,10 +168,11 @@ private struct SocketMessagesTests { // Build ancillary message with SCM_RIGHTS var ancillary = SocketDescriptor.AncillaryMessageBuffer() withUnsafeBytes(of: fileToSend.rawValue) { bytes in + let span = RawSpan(_unsafeBytes: bytes) ancillary.appendMessage( level: SocketDescriptor.ProtocolID(rawValue: SOL_SOCKET), type: .init(rawValue: CInt(SCM_RIGHTS)), - bytes: bytes + bytes: span ) } @@ -278,12 +279,13 @@ private struct SocketMessagesTests { // Build ancillary message with three FDs var ancillary = SocketDescriptor.AncillaryMessageBuffer() - var fds = [fd1.rawValue, fd2.rawValue, fd3.rawValue] + let fds = [fd1.rawValue, fd2.rawValue, fd3.rawValue] fds.withUnsafeBytes { bytes in + let span = RawSpan(_unsafeBytes: bytes) ancillary.appendMessage( level: SocketDescriptor.ProtocolID(rawValue: SOL_SOCKET), type: .init(rawValue: CInt(SCM_RIGHTS)), - bytes: bytes + bytes: span ) } diff --git a/Tests/SystemSocketsTests/SocketOperationsTests.swift b/Tests/SystemSocketsTests/SocketOperationsTests.swift index 7e396b8e..0bcf6a5c 100644 --- a/Tests/SystemSocketsTests/SocketOperationsTests.swift +++ b/Tests/SystemSocketsTests/SocketOperationsTests.swift @@ -106,7 +106,7 @@ private struct SocketOperationsTests { // MARK: - Send and Receive Tests - @available(System 99, *) + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) @Test func tcpSendReceive() throws { // Create server socket let server = try SocketDescriptor.open(.ipv4, .stream, protocol: .tcp) @@ -131,18 +131,66 @@ private struct SocketOperationsTests { let acceptedSocket = try server.accept() defer { try? acceptedSocket.close() } - // Send data from client + // Send data from client using Span let message = "Hello, World!" let messageBytes = Array(message.utf8) - let sent = try messageBytes.withUnsafeBytes { buffer in - try client.send(UnsafeRawBufferPointer(buffer)) + let sent = try messageBytes.withUnsafeBytes { bytes in + let span = RawSpan(_unsafeBytes: bytes) + return try client.send(span) } #expect(sent == messageBytes.count) - // Receive data on server + // Receive data on server using OutputRawSpan var buffer = [UInt8](repeating: 0, count: 1024) - let received = try buffer.withUnsafeMutableBytes { buffer in - try acceptedSocket.receive(into: buffer) + let received = try buffer.withUnsafeMutableBytes { buf in + var output = OutputRawSpan(buffer: buf, initializedCount: 0) + return try acceptedSocket.receive(into: &output) + } + #expect(received == messageBytes.count) + + let receivedMessage = String(decoding: buffer.prefix(received), as: UTF8.self) + #expect(receivedMessage == message) + } + + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) + @Test func tcpSendReceiveSpan() throws { + // Create server socket + let server = try SocketDescriptor.open(.ipv4, .stream, protocol: .tcp) + defer { try? server.close() } + + let serverAddr = SocketAddress(ipv4: IPv4Address.loopback(port: 0)) + try server.bind(to: serverAddr) + try server.listen(backlog: 1) + + var boundAddr = SocketAddress() + try server.getLocalAddress(into: &boundAddr) + let port = boundAddr.ipv4!.port + + // Create client socket and connect + let client = try SocketDescriptor.open(.ipv4, .stream, protocol: .tcp) + defer { try? client.close() } + + let connectAddr = SocketAddress(ipv4: IPv4Address.loopback(port: port)) + try client.connect(to: connectAddr) + + // Accept connection + let acceptedSocket = try server.accept() + defer { try? acceptedSocket.close() } + + // Send data from client using Span + let message = "Hello, Span World!" + let messageBytes = Array(message.utf8) + let sent = try messageBytes.withUnsafeBytes { bytes in + let span = RawSpan(_unsafeBytes: bytes) + return try client.send(span) + } + #expect(sent == messageBytes.count) + + // Receive data on server using OutputRawSpan + var buffer = [UInt8](repeating: 0, count: 1024) + let received = try buffer.withUnsafeMutableBytes { buf in + var output = OutputRawSpan(buffer: buf, initializedCount: 0) + return try acceptedSocket.receive(into: &output) } #expect(received == messageBytes.count) @@ -152,7 +200,7 @@ private struct SocketOperationsTests { // MARK: - UDP Tests - @available(System 99, *) + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) @Test func udpSendReceive() throws { // Create receiver socket let receiver = try SocketDescriptor.open(.ipv4, .datagram, protocol: .udp) @@ -169,20 +217,63 @@ private struct SocketOperationsTests { let sender = try SocketDescriptor.open(.ipv4, .datagram, protocol: .udp) defer { try? sender.close() } - // Send datagram + // Send datagram using Span let message = "UDP Message" let messageBytes = Array(message.utf8) let targetAddr = SocketAddress(ipv4: IPv4Address.loopback(port: port)) - let sent = try messageBytes.withUnsafeBytes { buffer in - try sender.send(UnsafeRawBufferPointer(buffer), to: targetAddr) + let sent = try messageBytes.withUnsafeBytes { bytes in + let span = RawSpan(_unsafeBytes: bytes) + return try sender.send(span, to: targetAddr) + } + #expect(sent == messageBytes.count) + + // Receive datagram using OutputRawSpan + var buffer = [UInt8](repeating: 0, count: 1024) + var fromAddr = SocketAddress() + let received = try buffer.withUnsafeMutableBytes { buf in + var output = OutputRawSpan(buffer: buf, initializedCount: 0) + return try receiver.receive(into: &output, sender: &fromAddr) + } + #expect(received == messageBytes.count) + #expect(fromAddr.family == SocketDescriptor.Domain.ipv4) + + let receivedMessage = String(decoding: buffer.prefix(received), as: UTF8.self) + #expect(receivedMessage == message) + } + + @available(macOS 15, iOS 18, watchOS 11, tvOS 18, visionOS 2, *) + @Test func udpSendReceiveSpan() throws { + // Create receiver socket + let receiver = try SocketDescriptor.open(.ipv4, .datagram, protocol: .udp) + defer { try? receiver.close() } + + let receiverAddr = SocketAddress(ipv4: IPv4Address.loopback(port: 0)) + try receiver.bind(to: receiverAddr) + + var boundAddr = SocketAddress() + try receiver.getLocalAddress(into: &boundAddr) + let port = boundAddr.ipv4!.port + + // Create sender socket + let sender = try SocketDescriptor.open(.ipv4, .datagram, protocol: .udp) + defer { try? sender.close() } + + // Send datagram using Span + let message = "UDP Span Message" + let messageBytes = Array(message.utf8) + let targetAddr = SocketAddress(ipv4: IPv4Address.loopback(port: port)) + let sent = try messageBytes.withUnsafeBytes { bytes in + let span = RawSpan(_unsafeBytes: bytes) + return try sender.send(span, to: targetAddr) } #expect(sent == messageBytes.count) - // Receive datagram + // Receive datagram using OutputRawSpan var buffer = [UInt8](repeating: 0, count: 1024) var fromAddr = SocketAddress() - let received = try buffer.withUnsafeMutableBytes { buffer in - try receiver.receive(into: buffer, sender: &fromAddr) + let received = try buffer.withUnsafeMutableBytes { buf in + var output = OutputRawSpan(buffer: buf, initializedCount: 0) + return try receiver.receive(into: &output, sender: &fromAddr) } #expect(received == messageBytes.count) #expect(fromAddr.family == SocketDescriptor.Domain.ipv4) From 10506ea9c3b932e5d0d5c6c57b40aef1e0f2e188 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Sun, 25 Jan 2026 21:02:23 -0700 Subject: [PATCH 3/3] Expand/enhance tests for socket addresses/messages --- .../SocketAddressTests.swift | 43 +++++++++---------- .../SocketMessagesTests.swift | 38 ++++++++++------ 2 files changed, 44 insertions(+), 37 deletions(-) diff --git a/Tests/SystemSocketsTests/SocketAddressTests.swift b/Tests/SystemSocketsTests/SocketAddressTests.swift index 202ec53e..a3953d89 100644 --- a/Tests/SystemSocketsTests/SocketAddressTests.swift +++ b/Tests/SystemSocketsTests/SocketAddressTests.swift @@ -32,11 +32,10 @@ private struct SocketAddressTests { // MARK: - IPv4 Address Tests @available(System 99, *) - @Test func ipv4BasicCreation() { - let addr = IPv4Address("127.0.0.1", port: 8080) - #expect(addr != nil) - #expect(addr?.port == 8080) - #expect(addr?.addressString == "127.0.0.1") + @Test func ipv4BasicCreation() throws { + let addr = try #require(IPv4Address("127.0.0.1", port: 8080)) + #expect(addr.port == 8080) + #expect(addr.addressString == "127.0.0.1") } @available(System 99, *) @@ -87,11 +86,10 @@ private struct SocketAddressTests { // MARK: - IPv6 Address Tests @available(System 99, *) - @Test func ipv6BasicCreation() { - let addr = IPv6Address("::1", port: 8080) - #expect(addr != nil) - #expect(addr?.port == 8080) - #expect(addr?.addressString == "::1") + @Test func ipv6BasicCreation() throws { + let addr = try #require(IPv6Address("::1", port: 8080)) + #expect(addr.port == 8080) + #expect(addr.addressString == "::1") } @available(System 99, *) @@ -130,10 +128,9 @@ private struct SocketAddressTests { // MARK: - Unix Address Tests @available(System 99, *) - @Test func unixBasicCreation() { - let addr = UnixAddress("/tmp/test.sock") - #expect(addr != nil) - #expect(addr?.path == "/tmp/test.sock") + @Test func unixBasicCreation() throws { + let addr = try #require(UnixAddress("/tmp/test.sock")) + #expect(addr.path == "/tmp/test.sock") } @available(System 99, *) @@ -155,37 +152,37 @@ private struct SocketAddressTests { // MARK: - SocketAddress Container Tests @available(System 99, *) - @Test func socketAddressFromIPv4() { + @Test func socketAddressFromIPv4() throws { let ipv4 = IPv4Address.loopback(port: 8080) let sockAddr = SocketAddress(ipv4: ipv4) #expect(sockAddr.family == SocketDescriptor.Domain.ipv4) - #expect(sockAddr.ipv4 != nil) - #expect(sockAddr.ipv4?.port == 8080) + let sockIPv4 = try #require(sockAddr.ipv4) + #expect(sockIPv4.port == 8080) #expect(sockAddr.ipv6 == nil) #expect(sockAddr.unix == nil) } @available(System 99, *) - @Test func socketAddressFromIPv6() { + @Test func socketAddressFromIPv6() throws { let ipv6 = IPv6Address.loopback(port: 443) let sockAddr = SocketAddress(ipv6: ipv6) #expect(sockAddr.family == SocketDescriptor.Domain.ipv6) - #expect(sockAddr.ipv6 != nil) - #expect(sockAddr.ipv6?.port == 443) + let sockIPv6 = try #require(sockAddr.ipv6) + #expect(sockIPv6.port == 443) #expect(sockAddr.ipv4 == nil) #expect(sockAddr.unix == nil) } @available(System 99, *) - @Test func socketAddressFromUnix() { + @Test func socketAddressFromUnix() throws { let unix = UnixAddress("/tmp/test.sock")! let sockAddr = SocketAddress(unix: unix) #expect(sockAddr.family == SocketDescriptor.Domain.local) - #expect(sockAddr.unix != nil) - #expect(sockAddr.unix?.path == "/tmp/test.sock") + let sockUnix = try #require(sockAddr.unix) + #expect(sockUnix.path == "/tmp/test.sock") #expect(sockAddr.ipv4 == nil) #expect(sockAddr.ipv6 == nil) } diff --git a/Tests/SystemSocketsTests/SocketMessagesTests.swift b/Tests/SystemSocketsTests/SocketMessagesTests.swift index f5459138..2c92962b 100644 --- a/Tests/SystemSocketsTests/SocketMessagesTests.swift +++ b/Tests/SystemSocketsTests/SocketMessagesTests.swift @@ -64,7 +64,7 @@ private struct SocketMessagesTests { // Receive message var buffer = [UInt8](repeating: 0, count: 1024) var recvAncillary = SocketDescriptor.AncillaryMessageBuffer(minimumCapacity: 256) - var sender: SocketAddress? = nil + var sender: SocketAddress? = SocketAddress() let received = try buffer.withUnsafeMutableBytes { buf in var recvOutput = OutputRawSpan(buffer: buf, initializedCount: 0) @@ -75,6 +75,8 @@ private struct SocketMessagesTests { ) } #expect(received == messageBytes.count) + // Note: For TCP connections, recvmsg doesn't populate msg_name with the peer address + // Use getpeername() instead for connection-oriented sockets let receivedMessage = String(decoding: buffer.prefix(received), as: UTF8.self) #expect(receivedMessage == message) @@ -106,6 +108,10 @@ private struct SocketMessagesTests { } #expect(sent == messageBytes.count) + // Get sender's actual local address for verification + var senderLocalAddr = SocketAddress() + try sender.getLocalAddress(into: &senderLocalAddr) + // Receive datagram var buffer = [UInt8](repeating: 0, count: 1024) var recvAncillary = SocketDescriptor.AncillaryMessageBuffer(minimumCapacity: 256) @@ -120,7 +126,14 @@ private struct SocketMessagesTests { ) } #expect(received == messageBytes.count) + + // Verify the sender address was correctly populated by recvmsg #expect(fromAddr?.family == .ipv4) + let fromIPv4 = try #require(fromAddr?.ipv4, "Sender address should be populated for UDP") + let senderIPv4 = try #require(senderLocalAddr.ipv4, "Sender socket should have IPv4 address") + + // Verify the port matches the sender's ephemeral port + #expect(fromIPv4.port == senderIPv4.port) let receivedMessage = String(decoding: buffer.prefix(received), as: UTF8.self) #expect(receivedMessage == message) @@ -214,22 +227,19 @@ private struct SocketMessagesTests { } } - #expect(receivedFD != nil, "Should have received a file descriptor") - - if let fd = receivedFD { - let receivedFile = FileDescriptor(rawValue: fd) - defer { try? receivedFile.close() } - - // Verify we can read from the received file descriptor - var readBuffer = [UInt8](repeating: 0, count: 1024) - let bytesRead = try readBuffer.withUnsafeMutableBytes { buf in - try receivedFile.read(into: buf) - } + let receivedFd = try #require(receivedFD, "Should have received a file descriptor") + let receivedFile = FileDescriptor(rawValue: receivedFd) + defer { try? receivedFile.close() } - let fileContent = String(decoding: readBuffer.prefix(bytesRead), as: UTF8.self) - #expect(fileContent == testData, "File content should match") + // Verify we can read from the received file descriptor + var readBuffer = [UInt8](repeating: 0, count: 1024) + let bytesRead = try readBuffer.withUnsafeMutableBytes { buf in + try receivedFile.read(into: buf) } + let fileContent = String(decoding: readBuffer.prefix(bytesRead), as: UTF8.self) + #expect(fileContent == testData, "File content should match") + try fileToSend.close() } }