diff --git a/Package.swift b/Package.swift index 00b668e..d27322f 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,10 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/PureSwift/swift-system.git", .branch("master")), + .package( + url: "https://github.com/apple/swift-system", + from: "1.0.0" + ), ], targets: [ .target( diff --git a/Sources/Socket/Extensions/FileEvents.swift b/Sources/Socket/Extensions/FileEvents.swift deleted file mode 100644 index c74b50d..0000000 --- a/Sources/Socket/Extensions/FileEvents.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// FileEvent.swift -// -// -// Created by Alsey Coleman Miller on 4/1/22. -// - -import Foundation -import SystemPackage - -internal extension FileEvents { - - static var socket: FileEvents { - [ - .read, - .write, - .error, - .hangup, - .invalidRequest - ] - } -} diff --git a/Sources/Socket/Socket.swift b/Sources/Socket/Socket.swift index 626a59b..b473dab 100644 --- a/Sources/Socket/Socket.swift +++ b/Sources/Socket/Socket.swift @@ -6,7 +6,7 @@ // import Foundation -import SystemPackage +@_exported import SystemPackage /// Socket public struct Socket { @@ -16,8 +16,8 @@ public struct Socket { /// Configuration for fine-tuning socket performance. public static var configuration = Socket.Configuration() - /// Underlying file descriptor - public let fileDescriptor: FileDescriptor + /// Underlying native socket handle. + public let fileDescriptor: SocketDescriptor public let event: Socket.Event.Stream @@ -27,7 +27,7 @@ public struct Socket { /// Starts monitoring a socket. public init( - fileDescriptor: FileDescriptor + fileDescriptor: SocketDescriptor ) async { let manager = SocketManager.shared self.fileDescriptor = fileDescriptor @@ -35,6 +35,44 @@ public struct Socket { self.event = await manager.add(fileDescriptor) } + /// Initialize + public init( + _ protocolID: T + ) async throws { + let fileDescriptor = try SocketDescriptor(protocolID) + await self.init(fileDescriptor: fileDescriptor) + } + + /// + public init( + _ protocolID: Address.ProtocolID, + bind address: Address + ) async throws { + let fileDescriptor = try SocketDescriptor(protocolID, bind: address) + await self.init(fileDescriptor: fileDescriptor) + } + + #if os(Linux) + /// + public init( + _ protocolID: T, + flags: SocketFlags + ) async throws { + let fileDescriptor = try SocketDescriptor(protocolID, flags: flags) + await self.init(fileDescriptor: fileDescriptor) + } + + /// + public init( + _ protocolID: Address.ProtocolID, + bind address: Address, + flags: SocketFlags + ) async throws { + let fileDescriptor = try SocketDescriptor(protocolID, bind: address, flags: flags) + await self.init(fileDescriptor: fileDescriptor) + } + #endif + // MARK: - Methods /// Write to socket diff --git a/Sources/Socket/SocketContinuation.swift b/Sources/Socket/SocketContinuation.swift index ca82c79..8ca4456 100644 --- a/Sources/Socket/SocketContinuation.swift +++ b/Sources/Socket/SocketContinuation.swift @@ -15,12 +15,12 @@ internal struct SocketContinuation where E: Error { private let continuation: CheckedContinuation - private let fileDescriptor: FileDescriptor + private let fileDescriptor: SocketDescriptor fileprivate init( continuation: UnsafeContinuation, function: String, - fileDescriptor: FileDescriptor + fileDescriptor: SocketDescriptor ) { self.continuation = CheckedContinuation(continuation: continuation, function: function) self.function = function @@ -50,7 +50,7 @@ extension SocketContinuation where T == Void { } internal func withContinuation( - for fileDescriptor: FileDescriptor, + for fileDescriptor: SocketDescriptor, function: String = #function, _ body: (SocketContinuation) -> Void ) async -> T { @@ -60,7 +60,7 @@ internal func withContinuation( } internal func withThrowingContinuation( - for fileDescriptor: FileDescriptor, + for fileDescriptor: SocketDescriptor, function: String = #function, _ body: (SocketContinuation) -> Void ) async throws -> T { @@ -73,7 +73,7 @@ internal typealias SocketContinuation = UnsafeContinuation where E: @inline(__always) internal func withContinuation( - for fileDescriptor: FileDescriptor, + for fileDescriptor: SocketDescriptor, function: String = #function, _ body: (SocketContinuation) -> Void ) async -> T { @@ -82,7 +82,7 @@ internal func withContinuation( @inline(__always) internal func withThrowingContinuation( - for fileDescriptor: FileDescriptor, + for fileDescriptor: SocketDescriptor, function: String = #function, _ body: (SocketContinuation) -> Void ) async throws -> T { diff --git a/Sources/Socket/SocketManager.swift b/Sources/Socket/SocketManager.swift index 7f0d1fe..dcdb9b9 100644 --- a/Sources/Socket/SocketManager.swift +++ b/Sources/Socket/SocketManager.swift @@ -13,9 +13,9 @@ internal actor SocketManager { static let shared = SocketManager() - private var sockets = [FileDescriptor: SocketState]() + private var sockets = [SocketDescriptor: SocketState]() - private var pollDescriptors = [FileDescriptor.Poll]() + private var pollDescriptors = [SocketDescriptor.Poll]() private var isMonitoring = false @@ -45,12 +45,12 @@ internal actor SocketManager { } } - func contains(_ fileDescriptor: FileDescriptor) -> Bool { + func contains(_ fileDescriptor: SocketDescriptor) -> Bool { return sockets.keys.contains(fileDescriptor) } func add( - _ fileDescriptor: FileDescriptor + _ fileDescriptor: SocketDescriptor ) -> Socket.Event.Stream { guard sockets.keys.contains(fileDescriptor) == false else { fatalError("Another socket for file descriptor \(fileDescriptor) already exists.") @@ -83,7 +83,7 @@ internal actor SocketManager { return event } - func remove(_ fileDescriptor: FileDescriptor, error: Error? = nil) async { + func remove(_ fileDescriptor: SocketDescriptor, error: Error? = nil) async { guard let socket = sockets[fileDescriptor] else { return // could have been removed by `poll()` } @@ -101,7 +101,7 @@ internal actor SocketManager { } @discardableResult - internal nonisolated func write(_ data: Data, for fileDescriptor: FileDescriptor) async throws -> Int { + internal nonisolated func write(_ data: Data, for fileDescriptor: SocketDescriptor) async throws -> Int { guard let socket = await sockets[fileDescriptor] else { log("Unable to write unknown socket \(fileDescriptor).") assertionFailure("\(#function) Unknown socket \(fileDescriptor)") @@ -112,7 +112,7 @@ internal actor SocketManager { return try await socket.write(data) } - internal nonisolated func read(_ length: Int, for fileDescriptor: FileDescriptor) async throws -> Data { + internal nonisolated func read(_ length: Int, for fileDescriptor: SocketDescriptor) async throws -> Data { guard let socket = await sockets[fileDescriptor] else { log("Unable to read unknown socket \(fileDescriptor).") assertionFailure("\(#function) Unknown socket \(fileDescriptor)") @@ -123,14 +123,14 @@ internal actor SocketManager { return try await socket.read(length) } - private func events(for fileDescriptor: FileDescriptor) throws -> FileEvents { - guard let poll = pollDescriptors.first(where: { $0.fileDescriptor == fileDescriptor }) else { + private func events(for fileDescriptor: SocketDescriptor) throws -> FileEvents { + guard let poll = pollDescriptors.first(where: { $0.socket == fileDescriptor }) else { throw Errno.connectionAbort } return poll.returnedEvents } - private nonisolated func wait(for event: FileEvents, fileDescriptor: FileDescriptor) async throws { + private nonisolated func wait(for event: FileEvents, fileDescriptor: SocketDescriptor) async throws { guard let socket = await sockets[fileDescriptor] else { log("Unable to wait for unknown socket \(fileDescriptor).") assertionFailure("\(#function) Unknown socket \(fileDescriptor)") @@ -161,7 +161,7 @@ internal actor SocketManager { pollDescriptors = sockets.keys .lazy .sorted(by: { $0.rawValue < $1.rawValue }) - .map { FileDescriptor.Poll(fileDescriptor: $0, events: .socket) } + .map { SocketDescriptor.Poll(socket: $0, events: .socketManager) } } private func poll() async throws { @@ -177,29 +177,29 @@ internal actor SocketManager { // wait for concurrent handling for poll in pollDescriptors { if poll.returnedEvents.contains(.write) { - await self.canWrite(poll.fileDescriptor) + await self.canWrite(poll.socket) } if poll.returnedEvents.contains(.read) { - await self.shouldRead(poll.fileDescriptor) + await self.shouldRead(poll.socket) } if poll.returnedEvents.contains(.invalidRequest) { - assertionFailure("Polled for invalid socket \(poll.fileDescriptor)") - await self.error(.badFileDescriptor, for: poll.fileDescriptor) + assertionFailure("Polled for invalid socket \(poll.socket)") + await self.error(.badFileDescriptor, for: poll.socket) } if poll.returnedEvents.contains(.hangup) { - await self.error(.connectionReset, for: poll.fileDescriptor) + await self.error(.connectionReset, for: poll.socket) } if poll.returnedEvents.contains(.error) { - await self.error(.connectionAbort, for: poll.fileDescriptor) + await self.error(.connectionAbort, for: poll.socket) } } } - private func error(_ error: Errno, for fileDescriptor: FileDescriptor) async { + private func error(_ error: Errno, for fileDescriptor: SocketDescriptor) async { await self.remove(fileDescriptor, error: error) } - private func shouldRead(_ fileDescriptor: FileDescriptor) async { + private func shouldRead(_ fileDescriptor: SocketDescriptor) async { guard let socket = self.sockets[fileDescriptor] else { log("Pending read for unknown socket \(fileDescriptor).") assertionFailure("\(#function) Unknown socket \(fileDescriptor)") @@ -211,7 +211,7 @@ internal actor SocketManager { socket.event.yield(.pendingRead) } - private func canWrite(_ fileDescriptor: FileDescriptor) async { + private func canWrite(_ fileDescriptor: SocketDescriptor) async { guard let socket = self.sockets[fileDescriptor] else { log("Can write for unknown socket \(fileDescriptor).") assertionFailure("\(#function) Unknown socket \(fileDescriptor)") @@ -228,13 +228,13 @@ extension SocketManager { actor SocketState { - let fileDescriptor: FileDescriptor + let fileDescriptor: SocketDescriptor let event: Socket.Event.Stream.Continuation private var pendingEvent = [FileEvents: [SocketContinuation<(), Error>]]() - init(fileDescriptor: FileDescriptor, + init(fileDescriptor: SocketDescriptor, event: Socket.Event.Stream.Continuation ) { self.fileDescriptor = fileDescriptor @@ -287,3 +287,16 @@ extension SocketManager.SocketState { return data } } + +private extension FileEvents { + + static var socketManager: FileEvents { + [ + .read, + .write, + .error, + .hangup, + .invalidRequest + ] + } +} diff --git a/Sources/Socket/System/AsyncSocketOperations.swift b/Sources/Socket/System/AsyncSocketOperations.swift new file mode 100644 index 0000000..f3db64e --- /dev/null +++ b/Sources/Socket/System/AsyncSocketOperations.swift @@ -0,0 +1,78 @@ +import SystemPackage + +#if swift(>=5.5) +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +public extension SocketDescriptor { + + /// Accept a connection on a socket. + /// + /// - Parameters: + /// - retryOnInterrupt: Whether to retry the receive operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - sleep: The number of nanoseconds to sleep if the operation + /// throws ``Errno/wouldBlock`` or other async I/O errors.. + /// - Returns: The file descriptor of the new connection. + /// + /// The corresponding C function is `accept`. + @_alwaysEmitIntoClient + func accept( + retryOnInterrupt: Bool = true, + sleep: UInt64 = 10_000_000 + ) async throws -> SocketDescriptor { + try await retry(sleep: sleep) { + _accept(retryOnInterrupt: retryOnInterrupt) + }.get() + } + + /// Accept a connection on a socket. + /// + /// - Parameters: + /// - address: The type of the `SocketAddress` expected for the new connection. + /// - retryOnInterrupt: Whether to retry the receive operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - sleep: The number of nanoseconds to sleep if the operation + /// throws ``Errno/wouldBlock`` or other async I/O errors. + /// - Returns: A tuple containing the file descriptor and address of the new connection. + /// + /// The corresponding C function is `accept`. + @_alwaysEmitIntoClient + func accept( + _ address: Address.Type, + retryOnInterrupt: Bool = true, + sleep: UInt64 = 10_000_000 + ) async throws -> (SocketDescriptor, Address) { + try await retry(sleep: sleep) { + _accept(address, retryOnInterrupt: retryOnInterrupt) + }.get() + } + + /// Initiate a connection on a socket. + /// + /// - Parameters: + /// - address: The peer address. + /// - retryOnInterrupt: Whether to retry the receive operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - sleep: The number of nanoseconds to sleep if the operation + /// throws ``Errno/wouldBlock`` or other async I/O errors. + /// - Returns: The file descriptor of the new connection. + /// + /// The corresponding C function is `connect`. + @_alwaysEmitIntoClient + func connect( + to address: Address, + retryOnInterrupt: Bool = true, + sleep: UInt64 = 10_000_000 + ) async throws { + try await retry(sleep: sleep) { + _connect(to: address, retryOnInterrupt: retryOnInterrupt) + }.get() + } +} + +#endif diff --git a/Sources/Socket/System/CInternetAddress.swift b/Sources/Socket/System/CInternetAddress.swift new file mode 100644 index 0000000..7792057 --- /dev/null +++ b/Sources/Socket/System/CInternetAddress.swift @@ -0,0 +1,69 @@ +import SystemPackage + +@usableFromInline +internal protocol CInternetAddress { + + static var stringLength: Int { get } + + static var family: SocketAddressFamily { get } + + init() +} + +internal extension CInternetAddress { + + @usableFromInline + init?(_ string: String) { + self.init() + /** + inet_pton() returns 1 on success (network address was successfully converted). 0 is returned if src does not contain a character string representing a valid network address in the specified address family. If af does not contain a valid address family, -1 is returned and errno is set to EAFNOSUPPORT. + */ + let result = string.withCString { + system_inet_pton(Self.family.rawValue, $0, &self) + } + guard result == 1 else { + assert(result != -1, "Invalid address family") + return nil + } + } +} + +internal extension String { + + @usableFromInline + init(_ cInternetAddress: T) throws { + let cString = UnsafeMutablePointer.allocate(capacity: T.stringLength) + defer { cString.deallocate() } + let success = withUnsafePointer(to: cInternetAddress) { + system_inet_ntop( + T.family.rawValue, + $0, + cString, + numericCast(T.stringLength) + ) != nil + } + guard success else { + throw Errno.current + } + + self.init(cString: cString) + } +} + +extension CInterop.IPv4Address: CInternetAddress { + + @usableFromInline + static var stringLength: Int { return numericCast(_INET_ADDRSTRLEN) } + + @usableFromInline + static var family: SocketAddressFamily { .ipv4 } +} + +extension CInterop.IPv6Address: CInternetAddress { + + @usableFromInline + static var stringLength: Int { return numericCast(_INET6_ADDRSTRLEN) } + + @usableFromInline + static var family: SocketAddressFamily { .ipv6 } +} diff --git a/Sources/Socket/System/CInterop.swift b/Sources/Socket/System/CInterop.swift new file mode 100644 index 0000000..dd20e09 --- /dev/null +++ b/Sources/Socket/System/CInterop.swift @@ -0,0 +1,68 @@ +import SystemPackage + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import CSystem +import Glibc +#elseif os(Windows) +import CSystem +import ucrt +#else +#error("Unsupported Platform") +#endif + +/// A namespace for C and platform types +public extension CInterop { + + /// The platform file descriptor set. + typealias FileDescriptorSet = fd_set + + typealias PollFileDescriptor = pollfd + + typealias FileDescriptorCount = nfds_t + + typealias FileEvent = Int16 + + #if os(Windows) + /// The platform socket descriptor. + typealias SocketDescriptor = SOCKET + #else + /// The platform socket descriptor, which is the same as a file desciptor on Unix systems. + typealias SocketDescriptor = CInt + #endif + + /// The C `msghdr` type + typealias MessageHeader = msghdr + + /// The C `sa_family_t` type + typealias SocketAddressFamily = sa_family_t + + /// Socket Type + #if os(Linux) + typealias SocketType = __socket_type + #else + typealias SocketType = CInt + #endif + + /// The C `addrinfo` type + typealias AddressInfo = addrinfo + + /// The C `in_addr` type + typealias IPv4Address = in_addr + + /// The C `in6_addr` type + typealias IPv6Address = in6_addr + + /// The C `sockaddr_in` type + typealias SocketAddress = sockaddr + + /// The C `sockaddr_in` type + typealias UnixSocketAddress = sockaddr_un + + /// The C `sockaddr_in` type + typealias IPv4SocketAddress = sockaddr_in + + /// The C `sockaddr_in6` type + typealias IPv6SocketAddress = sockaddr_in6 +} diff --git a/Sources/Socket/System/CSocketAddress.swift b/Sources/Socket/System/CSocketAddress.swift new file mode 100644 index 0000000..d48e99b --- /dev/null +++ b/Sources/Socket/System/CSocketAddress.swift @@ -0,0 +1,30 @@ +import SystemPackage + +@usableFromInline +internal protocol CSocketAddress { + + static var family: SocketAddressFamily { get } + + init() +} + +internal extension CSocketAddress { + + @usableFromInline + func withUnsafePointer( + _ body: (UnsafePointer, UInt32) throws -> Result + ) rethrows -> Result { + return try Swift.withUnsafeBytes(of: self) { + return try body($0.baseAddress!.assumingMemoryBound(to: CInterop.SocketAddress.self), UInt32(MemoryLayout.size)) + } + } + + @usableFromInline + mutating func withUnsafeMutablePointer( + _ body: (UnsafeMutablePointer, UInt32) throws -> Result + ) rethrows -> Result { + return try Swift.withUnsafeMutableBytes(of: &self) { + return try body($0.baseAddress!.assumingMemoryBound(to: CInterop.SocketAddress.self), UInt32(MemoryLayout.size)) + } + } +} diff --git a/Sources/Socket/System/Constants.swift b/Sources/Socket/System/Constants.swift new file mode 100644 index 0000000..2970975 --- /dev/null +++ b/Sources/Socket/System/Constants.swift @@ -0,0 +1,457 @@ +import SystemPackage + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import CSystem +import Glibc +#elseif os(Windows) +import CSystem +import ucrt +#else +#error("Unsupported Platform") +#endif + +// MARK: File Operations + +@_alwaysEmitIntoClient +internal var _O_RDONLY: CInt { O_RDONLY } + +@_alwaysEmitIntoClient +internal var _O_WRONLY: CInt { O_WRONLY } + +@_alwaysEmitIntoClient +internal var _O_RDWR: CInt { O_RDWR } + +#if !os(Windows) +// TODO: API? +@_alwaysEmitIntoClient +internal var _O_ACCMODE: CInt { O_ACCMODE } + +@_alwaysEmitIntoClient +internal var _O_NONBLOCK: CInt { O_NONBLOCK } +#endif + +@_alwaysEmitIntoClient +internal var _O_APPEND: CInt { O_APPEND } + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +@_alwaysEmitIntoClient +internal var _O_SHLOCK: CInt { O_SHLOCK } + +@_alwaysEmitIntoClient +internal var _O_EXLOCK: CInt { O_EXLOCK } +#endif + +#if !os(Windows) +// TODO: API? +@_alwaysEmitIntoClient +internal var _O_ASYNC: CInt { O_ASYNC } + +@_alwaysEmitIntoClient +internal var _O_NOFOLLOW: CInt { O_NOFOLLOW } +#endif + +@_alwaysEmitIntoClient +internal var _O_CREAT: CInt { O_CREAT } + +@_alwaysEmitIntoClient +internal var _O_TRUNC: CInt { O_TRUNC } + +@_alwaysEmitIntoClient +internal var _O_EXCL: CInt { O_EXCL } + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +@_alwaysEmitIntoClient +internal var _O_EVTONLY: CInt { O_EVTONLY } +#endif + +#if !os(Windows) +// TODO: API? +@_alwaysEmitIntoClient +internal var _O_NOCTTY: CInt { O_NOCTTY } + +@_alwaysEmitIntoClient +internal var _O_DIRECTORY: CInt { O_DIRECTORY } +#endif + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +@_alwaysEmitIntoClient +internal var _O_SYMLINK: CInt { O_SYMLINK } +#endif + +#if !os(Windows) +@_alwaysEmitIntoClient +internal var _O_CLOEXEC: CInt { O_CLOEXEC } +#endif + +@_alwaysEmitIntoClient +internal var _SEEK_SET: CInt { SEEK_SET } + +@_alwaysEmitIntoClient +internal var _SEEK_CUR: CInt { SEEK_CUR } + +@_alwaysEmitIntoClient +internal var _SEEK_END: CInt { SEEK_END } + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +@_alwaysEmitIntoClient +internal var _SEEK_HOLE: CInt { SEEK_HOLE } + +@_alwaysEmitIntoClient +internal var _SEEK_DATA: CInt { SEEK_DATA } +#endif + +@_alwaysEmitIntoClient +internal var _FD_CLOEXEC: CInt { FD_CLOEXEC } + +@_alwaysEmitIntoClient +internal var _F_DUPFD: CInt { F_DUPFD } + +@_alwaysEmitIntoClient +internal var _F_DUPFD_CLOEXEC: CInt { F_DUPFD_CLOEXEC } + +@_alwaysEmitIntoClient +internal var _F_GETFD: CInt { F_GETFD } + +@_alwaysEmitIntoClient +internal var _F_SETFD: CInt { F_SETFD } + +@_alwaysEmitIntoClient +internal var _F_GETFL: CInt { F_GETFL } + +@_alwaysEmitIntoClient +internal var _F_SETFL: CInt { F_SETFL } + +@_alwaysEmitIntoClient +internal var _POLLIN: CInt { POLLIN } + +@_alwaysEmitIntoClient +internal var _POLLPRI: CInt { POLLPRI } + +@_alwaysEmitIntoClient +internal var _POLLOUT: CInt { POLLOUT } + +@_alwaysEmitIntoClient +internal var _POLLRDNORM: CInt { POLLRDNORM } + +@_alwaysEmitIntoClient +internal var _POLLWRNORM: CInt { POLLWRNORM } + +@_alwaysEmitIntoClient +internal var _POLLRDBAND: CInt { POLLRDBAND } + +@_alwaysEmitIntoClient +internal var _POLLWRBAND: CInt { POLLWRBAND } + +@_alwaysEmitIntoClient +internal var _POLLERR: CInt { POLLERR } + +@_alwaysEmitIntoClient +internal var _POLLHUP: CInt { POLLHUP } + +@_alwaysEmitIntoClient +internal var _POLLNVAL: CInt { POLLNVAL } + +@_alwaysEmitIntoClient +internal var _INET_ADDRSTRLEN: CInt { INET_ADDRSTRLEN } + +@_alwaysEmitIntoClient +internal var _INET6_ADDRSTRLEN: CInt { INET6_ADDRSTRLEN } + +@_alwaysEmitIntoClient +internal var _INADDR_ANY: CInterop.IPv4Address { CInterop.IPv4Address(s_addr: INADDR_ANY) } + +@_alwaysEmitIntoClient +internal var _INADDR_LOOPBACK: CInterop.IPv4Address { CInterop.IPv4Address(s_addr: INADDR_LOOPBACK) } + +@_alwaysEmitIntoClient +internal var _INADDR6_ANY: CInterop.IPv6Address { in6addr_any } + +@_alwaysEmitIntoClient +internal var _INADDR6_LOOPBACK: CInterop.IPv6Address { in6addr_loopback } + +@_alwaysEmitIntoClient +internal var _AF_UNIX: CInt { AF_UNIX } + +@_alwaysEmitIntoClient +internal var _AF_INET: CInt { AF_INET } + +@_alwaysEmitIntoClient +internal var _AF_INET6: CInt { AF_INET6 } + +@_alwaysEmitIntoClient +internal var _AF_IPX: CInt { AF_IPX } + +@_alwaysEmitIntoClient +internal var _AF_APPLETALK: CInt { AF_APPLETALK } + +#if !os(Windows) +@_alwaysEmitIntoClient +internal var _AF_DECnet: CInt { AF_DECnet } + +@_alwaysEmitIntoClient +internal var _AF_VSOCK: CInt { AF_VSOCK } + +@_alwaysEmitIntoClient +internal var _AF_ISDN: CInt { AF_ISDN } +#endif + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +@_alwaysEmitIntoClient +internal var _AF_IMPLINK: CInt { AF_IMPLINK } + +@_alwaysEmitIntoClient +internal var _AF_PUP: CInt { AF_PUP } + +@_alwaysEmitIntoClient +internal var _AF_CHAOS: CInt { AF_CHAOS } + +@_alwaysEmitIntoClient +internal var _AF_NS: CInt { AF_NS } + +@_alwaysEmitIntoClient +internal var _AF_ISO: CInt { AF_ISO } + +@_alwaysEmitIntoClient +internal var _AF_PPP: CInt { AF_PPP } + +@_alwaysEmitIntoClient +internal var _AF_LINK: CInt { AF_LINK } + +@_alwaysEmitIntoClient +internal var _AF_NETBIOS: CInt { AF_NETBIOS } +#endif + +#if os(Linux) +@_alwaysEmitIntoClient +internal var _AF_AX25: CInt { AF_AX25 } + +@_alwaysEmitIntoClient +internal var _AF_X25: CInt { AF_X25 } + +@_alwaysEmitIntoClient +internal var _AF_KEY: CInt { AF_KEY } + +@_alwaysEmitIntoClient +internal var _AF_NETLINK: CInt { AF_NETLINK } + +@_alwaysEmitIntoClient +internal var _AF_PACKET: CInt { AF_PACKET } + +@_alwaysEmitIntoClient +internal var _AF_ATMSVC: CInt { AF_ATMSVC } + +@_alwaysEmitIntoClient +internal var _AF_RDS: CInt { AF_RDS } + +@_alwaysEmitIntoClient +internal var _AF_PPPOX: CInt { AF_PPPOX } + +@_alwaysEmitIntoClient +internal var _AF_WANPIPE: CInt { AF_WANPIPE } + +@_alwaysEmitIntoClient +internal var _AF_LLC: CInt { AF_LLC } + +@_alwaysEmitIntoClient +internal var _AF_IB: CInt { AF_IB } + +@_alwaysEmitIntoClient +internal var _AF_MPLS: CInt { AF_MPLS } + +@_alwaysEmitIntoClient +internal var _AF_CAN: CInt { AF_CAN } + +@_alwaysEmitIntoClient +internal var _AF_TIPC: CInt { AF_TIPC } + +@_alwaysEmitIntoClient +internal var _AF_BLUETOOTH: CInt { AF_BLUETOOTH } + +@_alwaysEmitIntoClient +internal var _AF_IUCV: CInt { AF_IUCV } + +@_alwaysEmitIntoClient +internal var _AF_RXRPC: CInt { AF_RXRPC } + +@_alwaysEmitIntoClient +internal var _AF_PHONET: CInt { AF_PHONET } + +@_alwaysEmitIntoClient +internal var _AF_IEEE802154: CInt { AF_IEEE802154 } + +@_alwaysEmitIntoClient +internal var _AF_CAIF: CInt { AF_CAIF } + +@_alwaysEmitIntoClient +internal var _AF_ALG: CInt { AF_ALG } + +@_alwaysEmitIntoClient +internal var _AF_KCM: CInt { AF_KCM } + +@_alwaysEmitIntoClient +internal var _AF_QIPCRTR: CInt { AF_QIPCRTR } + +@_alwaysEmitIntoClient +internal var _AF_SMC: CInt { AF_SMC } + +@_alwaysEmitIntoClient +internal var _AF_XDP: CInt { AF_XDP } +#endif + +#if os(Windows) +@_alwaysEmitIntoClient +internal var _AF_IRDA: CInt { AF_IRDA } + +@_alwaysEmitIntoClient +internal var _AF_BTH: CInt { AF_BTH } +#endif + +@_alwaysEmitIntoClient +internal var _SOCK_STREAM: CInterop.SocketType { SOCK_STREAM } + +@_alwaysEmitIntoClient +internal var _SOCK_DGRAM: CInterop.SocketType { SOCK_DGRAM } + +@_alwaysEmitIntoClient +internal var _SOCK_RAW: CInterop.SocketType { SOCK_RAW } + +@_alwaysEmitIntoClient +internal var _SOCK_RDM: CInterop.SocketType { SOCK_RDM } + +@_alwaysEmitIntoClient +internal var _SOCK_SEQPACKET: CInterop.SocketType { SOCK_SEQPACKET } + +#if os(Linux) +@_alwaysEmitIntoClient +internal var _SOCK_DCCP: CInterop.SocketType { SOCK_DCCP } + +@_alwaysEmitIntoClient +internal var _SOCK_CLOEXEC: CInterop.SocketType { SOCK_CLOEXEC } + +@_alwaysEmitIntoClient +internal var _SOCK_NONBLOCK: CInterop.SocketType { SOCK_NONBLOCK } +#endif + +@_alwaysEmitIntoClient +internal var _IPPROTO_RAW: CInt { numericCast(IPPROTO_RAW) } + +@_alwaysEmitIntoClient +internal var _IPPROTO_TCP: CInt { numericCast(IPPROTO_TCP) } + +@_alwaysEmitIntoClient +internal var _IPPROTO_UDP: CInt { numericCast(IPPROTO_UDP) } + +@_alwaysEmitIntoClient +internal var _SOL_SOCKET: CInt { SOL_SOCKET } + +@_alwaysEmitIntoClient +internal var _SO_DEBUG: CInt { SO_DEBUG } + +@_alwaysEmitIntoClient +internal var _SO_ACCEPTCONN: CInt { SO_ACCEPTCONN } + +@_alwaysEmitIntoClient +internal var _SO_REUSEADDR: CInt { SO_REUSEADDR } + +@_alwaysEmitIntoClient +internal var _SO_KEEPALIVE: CInt { SO_KEEPALIVE } + +@_alwaysEmitIntoClient +internal var _SO_DONTROUTE: CInt { SO_DONTROUTE } + +@_alwaysEmitIntoClient +internal var _SO_BROADCAST: CInt { SO_BROADCAST } + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +@_alwaysEmitIntoClient +internal var _SO_USELOOPBACK: CInt { SO_USELOOPBACK } +#endif + +@_alwaysEmitIntoClient +internal var _SO_LINGER: CInt { SO_LINGER } + +#if os(Linux) +@_alwaysEmitIntoClient +internal var _SOL_NETLINK: CInt { SOL_NETLINK } + +@_alwaysEmitIntoClient +internal var _SOL_BLUETOOTH: CInt { SOL_BLUETOOTH } + +@_alwaysEmitIntoClient +internal var _SOL_L2CAP: CInt { 6 } +#endif + +@_alwaysEmitIntoClient +internal var _MSG_DONTROUTE: CInt { numericCast(MSG_DONTROUTE) } /* send without using routing tables */ + +@_alwaysEmitIntoClient +internal var _MSG_EOR: CInt { numericCast(MSG_EOR) } /* data completes record */ + +@_alwaysEmitIntoClient +internal var _MSG_OOB: CInt { numericCast(MSG_OOB) } /* process out-of-band data */ + +@_alwaysEmitIntoClient +internal var _MSG_PEEK: CInt { numericCast(MSG_PEEK) } /* peek at incoming message */ +@_alwaysEmitIntoClient +internal var _MSG_TRUNC: CInt { numericCast(MSG_TRUNC) } /* data discarded before delivery */ +@_alwaysEmitIntoClient +internal var _MSG_CTRUNC: CInt { numericCast(MSG_CTRUNC) } /* control data lost before delivery */ +@_alwaysEmitIntoClient +internal var _MSG_WAITALL: CInt { numericCast(MSG_WAITALL) } /* wait for full request or error */ + +@_alwaysEmitIntoClient +internal var _MSG_DONTWAIT: CInt { numericCast(MSG_DONTWAIT) } /* this message should be nonblocking */ + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +@_alwaysEmitIntoClient +internal var _MSG_EOF: CInt { numericCast(MSG_EOF) } /* data completes connection */ + +@_alwaysEmitIntoClient +internal var _MSG_WAITSTREAM: CInt { numericCast(MSG_WAITSTREAM) } /* wait up to full request.. may return partial */ + +@_alwaysEmitIntoClient +internal var _MSG_FLUSH: CInt { numericCast(MSG_FLUSH) } /* Start of 'hold' seq; dump so_temp, deprecated */ +@_alwaysEmitIntoClient +internal var _MSG_HOLD: CInt { numericCast(MSG_HOLD) } /* Hold frag in so_temp, deprecated */ +@_alwaysEmitIntoClient +internal var _MSG_SEND: CInt { numericCast(MSG_SEND) } /* Send the packet in so_temp, deprecated */ +@_alwaysEmitIntoClient +internal var _MSG_HAVEMORE: CInt { numericCast(MSG_HAVEMORE) } /* Data ready to be read */ +@_alwaysEmitIntoClient +internal var _MSG_RCVMORE: CInt { numericCast(MSG_RCVMORE) } /* Data remains in current pkt */ + +@_alwaysEmitIntoClient +internal var _MSG_NEEDSA: CInt { numericCast(MSG_NEEDSA) } /* Fail receive if socket address cannot be allocated */ + +@_alwaysEmitIntoClient +internal var _MSG_NOSIGNAL: CInt { numericCast(MSG_NOSIGNAL) } /* do not generate SIGPIPE on EOF */ +#endif + +#if os(Linux) +@_alwaysEmitIntoClient +internal var _MSG_CONFIRM: CInt { numericCast(MSG_CONFIRM) } + +@_alwaysEmitIntoClient +internal var _MSG_MORE: CInt { numericCast(MSG_MORE) } +#endif + +@_alwaysEmitIntoClient +internal var _fd_set_count: Int { +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + // __DARWIN_FD_SETSIZE is number of *bits*, so divide by number bits in each element to get element count + // at present this is 1024 / 32 == 32 + return Int(__DARWIN_FD_SETSIZE) / 32 +#elseif os(Linux) || os(FreeBSD) || os(Android) +#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le) + return 32 +#elseif arch(i386) || arch(arm) + return 16 +#else +#error("This architecture isn't known. Add it to the 32-bit or 64-bit line.") +#endif +#elseif os(Windows) + return 32 +#endif +} diff --git a/Sources/Socket/System/Errno.swift b/Sources/Socket/System/Errno.swift new file mode 100644 index 0000000..33bdcf1 --- /dev/null +++ b/Sources/Socket/System/Errno.swift @@ -0,0 +1,15 @@ +// +// Errno.swift +// +// +// Created by Alsey Coleman Miller on 4/26/22. +// + +import SystemPackage + +extension Errno { + internal static var current: Errno { + get { Errno(rawValue: system_errno) } + set { system_errno = newValue.rawValue } + } +} diff --git a/Sources/Socket/System/FileChange.swift b/Sources/Socket/System/FileChange.swift new file mode 100644 index 0000000..f720734 --- /dev/null +++ b/Sources/Socket/System/FileChange.swift @@ -0,0 +1,138 @@ +import SystemPackage + +public extension SocketDescriptor { + + /// Duplicate + @_alwaysEmitIntoClient + func duplicate( + closeOnExec: Bool, + retryOnInterrupt: Bool = true + ) throws -> FileDescriptor { + let fileDescriptor = try _change( + closeOnExec ? .duplicateCloseOnExec : .duplicate, + self.rawValue, + retryOnInterrupt: retryOnInterrupt + ).get() + return FileDescriptor(rawValue: fileDescriptor) + } + + /// Get Flags + @_alwaysEmitIntoClient + func getFlags(retryOnInterrupt: Bool = true) throws -> FileDescriptor.Flags { + let rawValue = try _change( + .getFileDescriptorFlags, + retryOnInterrupt: retryOnInterrupt + ).get() + return FileDescriptor.Flags(rawValue: rawValue) + } + + /// Set Flags + @_alwaysEmitIntoClient + func setFlags( + _ newValue: FileDescriptor.Flags, + retryOnInterrupt: Bool = true + ) throws { + let _ = try _change( + .setFileDescriptorFlags, + newValue.rawValue, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + /// Get Status + @_alwaysEmitIntoClient + func getStatus(retryOnInterrupt: Bool = true) throws -> FileDescriptor.OpenOptions { + let rawValue = try _change( + .getStatusFlags, + retryOnInterrupt: retryOnInterrupt + ).get() + return FileDescriptor.OpenOptions(rawValue: rawValue) + } + + /// Set Status + @_alwaysEmitIntoClient + func setStatus( + _ newValue: FileDescriptor.OpenOptions, + retryOnInterrupt: Bool = true + ) throws { + let _ = try _change( + .setStatusFlags, + newValue.rawValue, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal func _change( + _ operation: FileChangeID, + retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fcntl(self.rawValue, operation.rawValue) + } + } + + @usableFromInline + internal func _change( + _ operation: FileChangeID, + _ value: CInt, + retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fcntl(self.rawValue, operation.rawValue, value) + } + } + + @usableFromInline + internal func _change( + _ operation: FileChangeID, + _ pointer: UnsafeMutableRawPointer, + retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fcntl(self.rawValue, operation.rawValue, pointer) + } + } +} + +@usableFromInline +internal struct FileChangeID: RawRepresentable, Hashable, Codable { + + /// The raw C file handle. + @_alwaysEmitIntoClient + public let rawValue: CInt + + /// Creates a strongly-typed file handle from a raw C file handle. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + @_alwaysEmitIntoClient + private init(_ raw: CInt) { self.init(rawValue: raw) } +} + +internal extension FileChangeID { + + /// Duplicate a file descriptor. + @_alwaysEmitIntoClient + static var duplicate: FileChangeID { FileChangeID(_F_DUPFD) } + + /// Duplicate a file descriptor and additionally set the close-on-exec flag for the duplicate descriptor. + @_alwaysEmitIntoClient + static var duplicateCloseOnExec: FileChangeID { FileChangeID(_F_DUPFD_CLOEXEC) } + + /// Read the file descriptor flags. + @_alwaysEmitIntoClient + static var getFileDescriptorFlags: FileChangeID { FileChangeID(_F_GETFD) } + + /// Set the file descriptor flags. + @_alwaysEmitIntoClient + static var setFileDescriptorFlags: FileChangeID { FileChangeID(_F_SETFD) } + + /// Get the file access mode and the file status flags. + @_alwaysEmitIntoClient + static var getStatusFlags: FileChangeID { FileChangeID(_F_GETFL) } + + /// Set the file status flags. + @_alwaysEmitIntoClient + static var setStatusFlags: FileChangeID { FileChangeID(_F_SETFL) } +} diff --git a/Sources/Socket/System/FileEvent.swift b/Sources/Socket/System/FileEvent.swift new file mode 100644 index 0000000..934c207 --- /dev/null +++ b/Sources/Socket/System/FileEvent.swift @@ -0,0 +1,69 @@ +import SystemPackage + +/// File Events bitmask +@frozen +// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +public struct FileEvents: OptionSet, Hashable, Codable { + + /// The raw C file events. + @_alwaysEmitIntoClient + public let rawValue: CInterop.FileEvent + + /// Create a strongly-typed file events from a raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.FileEvent) { self.rawValue = rawValue } + + @_alwaysEmitIntoClient + private init(_ raw: CInt) { self.init(rawValue: numericCast(raw)) } +} + +public extension FileEvents { + + /// There is data to read. + @_alwaysEmitIntoClient + static var read: FileEvents { FileEvents(_POLLIN) } + + /// There is urgent data to read (e.g., out-of-band data on TCP socket; + /// pseudoterminal master in packet mode has seen state change in slave). + @_alwaysEmitIntoClient + static var readUrgent: FileEvents { FileEvents(_POLLPRI) } + + /// Writing now will not block. + @_alwaysEmitIntoClient + static var write: FileEvents { FileEvents(_POLLOUT) } + + /// Error condition. + @_alwaysEmitIntoClient + static var error: FileEvents { FileEvents(_POLLERR) } + + /// Hang up. + @_alwaysEmitIntoClient + static var hangup: FileEvents { FileEvents(_POLLHUP) } + + /// Error condition. + @_alwaysEmitIntoClient + static var invalidRequest: FileEvents { FileEvents(_POLLNVAL) } +} + + +extension FileEvents + : CustomStringConvertible, CustomDebugStringConvertible +{ + /// A textual representation of the file permissions. + @inline(never) + public var description: String { + let descriptions: [(Element, StaticString)] = [ + (.read, ".read"), + (.readUrgent, ".readUrgent"), + (.write, ".write"), + (.error, ".error"), + (.hangup, ".hangup"), + (.invalidRequest, ".invalidRequest") + ] + + return _buildDescription(descriptions) + } + + /// A textual representation of the file permissions, suitable for debugging. + public var debugDescription: String { self.description } +} diff --git a/Sources/Socket/System/FileFlags.swift b/Sources/Socket/System/FileFlags.swift new file mode 100644 index 0000000..df768ae --- /dev/null +++ b/Sources/Socket/System/FileFlags.swift @@ -0,0 +1,59 @@ +// +// Flags.swift +// +// +// Created by Alsey Coleman Miller on 4/26/22. +// + +import SystemPackage + +public extension FileDescriptor { + + /// Options that specify behavior for file descriptors. + @frozen + struct Flags: OptionSet, Hashable, Codable { + + /// The raw C options. + @_alwaysEmitIntoClient + public var rawValue: CInt + + /// Create a strongly-typed options value from raw C options. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + @_alwaysEmitIntoClient + private init(_ raw: CInt) { self.init(rawValue: raw) } + + /// Indicates that executing a program closes the file. + /// + /// Normally, file descriptors remain open + /// across calls to the `exec(2)` family of functions. + /// If you specify this option, + /// the file descriptor is closed when replacing this process + /// with another process. + /// + /// The state of the file + /// descriptor flags can be inspected using `F_GETFD`, + /// as described in the `fcntl(2)` man page. + /// + /// The corresponding C constant is `FD_CLOEXEC`. + @_alwaysEmitIntoClient + public static var closeOnExec: Flags { Flags(_FD_CLOEXEC) } + } +} + +extension FileDescriptor.Flags + : CustomStringConvertible, CustomDebugStringConvertible +{ + /// A textual representation of the access mode. + @inline(never) + public var description: String { + switch self { + case .closeOnExec: return "closeOnExec" + default: return "\(Self.self)(rawValue: \(self.rawValue))" + } + } + + /// A textual representation of the access mode, suitable for debugging + public var debugDescription: String { self.description } +} diff --git a/Sources/Socket/System/InputOutput/IOControl.swift b/Sources/Socket/System/InputOutput/IOControl.swift new file mode 100644 index 0000000..7a24056 --- /dev/null +++ b/Sources/Socket/System/InputOutput/IOControl.swift @@ -0,0 +1,95 @@ +/// Input / Output Request identifier for manipulating underlying device parameters of special files. +public protocol IOControlID: RawRepresentable { + + /// Create a strongly-typed I/O request from a raw C IO request. + init?(rawValue: CUnsignedLong) + + /// The raw C IO request ID. + var rawValue: CUnsignedLong { get } +} + +public protocol IOControlInteger { + + associatedtype ID: IOControlID + + static var id: ID { get } + + var intValue: Int32 { get } +} + +public protocol IOControlValue { + + associatedtype ID: IOControlID + + static var id: ID { get } + + mutating func withUnsafeMutablePointer(_ body: (UnsafeMutableRawPointer) throws -> (Result)) rethrows -> Result +} + +#if os(Linux) +public extension IOControlID { + + init?(type: IOType, direction: IODirection, code: CInt, size: CInt) { + self.init(rawValue: _IOC(direction, type, code, size)) + } +} + +/// #define _IOC(dir,type,nr,size) \ +/// (((dir) << _IOC_DIRSHIFT) | \ +/// ((type) << _IOC_TYPESHIFT) | \ +/// ((nr) << _IOC_NRSHIFT) | \ +/// ((size) << _IOC_SIZESHIFT)) +@usableFromInline +internal func _IOC( + _ direction: IODirection, + _ type: IOType, + _ nr: CInt, + _ size: CInt +) -> CUnsignedLong { + + let dir = CInt(direction.rawValue) + let dirValue = dir << _DIRSHIFT + let typeValue = type.rawValue << _TYPESHIFT + let nrValue = nr << _NRSHIFT + let sizeValue = size << _SIZESHIFT + let value = CLong(dirValue | typeValue | nrValue | sizeValue) + return CUnsignedLong(bitPattern: value) +} + +@_alwaysEmitIntoClient +internal var _NRBITS: CInt { CInt(8) } + +@_alwaysEmitIntoClient +internal var _TYPEBITS: CInt { CInt(8) } + +@_alwaysEmitIntoClient +internal var _SIZEBITS: CInt { CInt(14) } + +@_alwaysEmitIntoClient +internal var _DIRBITS: CInt { CInt(2) } + +@_alwaysEmitIntoClient +internal var _NRMASK: CInt { CInt((1 << _NRBITS)-1) } + +@_alwaysEmitIntoClient +internal var _TYPEMASK: CInt { CInt((1 << _TYPEBITS)-1) } + +@_alwaysEmitIntoClient +internal var _SIZEMASK: CInt { CInt((1 << _SIZEBITS)-1) } + +@_alwaysEmitIntoClient +internal var _DIRMASK: CInt { CInt((1 << _DIRBITS)-1) } + +@_alwaysEmitIntoClient +internal var _NRSHIFT: CInt { CInt(0) } + +@_alwaysEmitIntoClient +internal var _TYPESHIFT: CInt { CInt(_NRSHIFT+_NRBITS) } + +@_alwaysEmitIntoClient +internal var _SIZESHIFT: CInt { CInt(_TYPESHIFT+_TYPEBITS) } + +@_alwaysEmitIntoClient +internal var _DIRSHIFT: CInt { CInt(_SIZESHIFT+_SIZEBITS) } + +#endif diff --git a/Sources/Socket/System/InputOutput/IODirection.swift b/Sources/Socket/System/InputOutput/IODirection.swift new file mode 100644 index 0000000..22590da --- /dev/null +++ b/Sources/Socket/System/InputOutput/IODirection.swift @@ -0,0 +1,31 @@ +#if os(Linux) + +/// `ioctl` Data Direction +@frozen +public struct IODirection: OptionSet, Hashable, Codable { + + /// The raw C file permissions. + @_alwaysEmitIntoClient + public let rawValue: CInt + + /// Create a strongly-typed file permission from a raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + @_alwaysEmitIntoClient + private init(_ raw: CInt) { self.init(rawValue: raw) } +} + +public extension IODirection { + + @_alwaysEmitIntoClient + static var none: IODirection { IODirection(0x00) } + + @_alwaysEmitIntoClient + static var read: IODirection { IODirection(0x01) } + + @_alwaysEmitIntoClient + static var write: IODirection { IODirection(0x02) } +} + +#endif diff --git a/Sources/Socket/System/InputOutput/IOOperations.swift b/Sources/Socket/System/InputOutput/IOOperations.swift new file mode 100644 index 0000000..70cec98 --- /dev/null +++ b/Sources/Socket/System/InputOutput/IOOperations.swift @@ -0,0 +1,66 @@ +import SystemPackage + +extension SocketDescriptor { + + /// Manipulates the underlying device parameters of special files. + @_alwaysEmitIntoClient + public func inputOutput( + _ request: T, + retryOnInterrupt: Bool = true + ) throws { + try _inputOutput(request, retryOnInterrupt: true).get() + } + + /// Manipulates the underlying device parameters of special files. + @usableFromInline + internal func _inputOutput( + _ request: T, + retryOnInterrupt: Bool + ) -> Result<(), Errno> { + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_ioctl(self.rawValue, request.rawValue) + } + } + + /// Manipulates the underlying device parameters of special files. + @_alwaysEmitIntoClient + public func inputOutput( + _ request: T, + retryOnInterrupt: Bool = true + ) throws { + try _inputOutput(request, retryOnInterrupt: retryOnInterrupt).get() + } + + /// Manipulates the underlying device parameters of special files. + @usableFromInline + internal func _inputOutput( + _ request: T, + retryOnInterrupt: Bool + ) -> Result<(), Errno> { + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_ioctl(self.rawValue, T.id.rawValue, request.intValue) + } + } + + /// Manipulates the underlying device parameters of special files. + @_alwaysEmitIntoClient + public func inputOutput( + _ request: inout T, + retryOnInterrupt: Bool = true + ) throws { + try _inputOutput(&request, retryOnInterrupt: retryOnInterrupt).get() + } + + /// Manipulates the underlying device parameters of special files. + @usableFromInline + internal func _inputOutput( + _ request: inout T, + retryOnInterrupt: Bool + ) -> Result<(), Errno> { + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + request.withUnsafeMutablePointer { pointer in + system_ioctl(self.rawValue, T.id.rawValue, pointer) + } + } + } +} diff --git a/Sources/Socket/System/InputOutput/IOType.swift b/Sources/Socket/System/InputOutput/IOType.swift new file mode 100644 index 0000000..fd4be9b --- /dev/null +++ b/Sources/Socket/System/InputOutput/IOType.swift @@ -0,0 +1,82 @@ +#if os(Linux) + +/// `ioctl` Device driver identifier code +@frozen +public struct IOType: RawRepresentable, Equatable, Hashable { + + @_alwaysEmitIntoClient + public let rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { + self.rawValue = rawValue + } +} + +// MARK: - ExpressibleByUnicodeScalarLiteral + +extension IOType: ExpressibleByUnicodeScalarLiteral { + + @_alwaysEmitIntoClient + public init(unicodeScalarLiteral character: Unicode.Scalar) { + self.init(rawValue: CInt(character.value)) + } +} + +// MARK: - ExpressibleByIntegerLiteral + +extension IOType: ExpressibleByIntegerLiteral { + + @_alwaysEmitIntoClient + public init(integerLiteral value: Int32) { + self.init(rawValue: value) + } +} + +// MARK: - Definitions + +// https://01.org/linuxgraphics/gfx-docs/drm/ioctl/ioctl-number.html +public extension IOType { + + /// Floppy Disk + @_alwaysEmitIntoClient + static var floppyDisk: IOType { 0x02 } // linux/fd.h + + /// InfiniBand Subsystem + @_alwaysEmitIntoClient + static var infiniBand: IOType { 0x1b } + + /// IEEE 1394 Subsystem Block for the entire subsystem + @_alwaysEmitIntoClient + static var ieee1394: IOType { "#" } + + /// Performance counter + @_alwaysEmitIntoClient + static var performance: IOType { "$" } // linux/perf_counter.h, linux/perf_event.h + + /// System Trace Module subsystem + @_alwaysEmitIntoClient + static var systemTrace: IOType { "%" } // include/uapi/linux/stm.h + + /// Kernel-based Virtual Machine + @_alwaysEmitIntoClient + static var kvm: IOType { 0xAF } // linux/kvm.h + + /// Freescale hypervisor + @_alwaysEmitIntoClient + static var freescaleHypervisor: IOType { 0xAF } // linux/fsl_hypervisor.h + + /// GPIO + @_alwaysEmitIntoClient + static var gpio: IOType { 0xB4 } // linux/gpio.h + + /// Linux FUSE + @_alwaysEmitIntoClient + static var fuse: IOType { 0xE5 } // linux/fuse.h + + /// ChromeOS EC driver + @_alwaysEmitIntoClient + static var chromeEC: IOType { 0xEC } // drivers/platform/chrome/cros_ec_dev.h +} + +#endif diff --git a/Sources/Socket/System/InternetProtocol.swift b/Sources/Socket/System/InternetProtocol.swift new file mode 100644 index 0000000..88575c7 --- /dev/null +++ b/Sources/Socket/System/InternetProtocol.swift @@ -0,0 +1,201 @@ +import SystemPackage + +/// Internet Protocol Address +@frozen +public enum IPAddress: Equatable, Hashable, Codable { + + /// IPv4 + case v4(IPv4Address) + + /// IPv6 + case v6(IPv6Address) +} + +public extension IPAddress { + + @_alwaysEmitIntoClient + func withUnsafeBytes(_ body: ((UnsafeRawBufferPointer) -> (Result))) -> Result { + switch self { + case let .v4(address): + return address.withUnsafeBytes(body) + case let .v6(address): + return address.withUnsafeBytes(body) + } + } +} + +extension IPAddress: RawRepresentable { + + public init?(rawValue: String) { + + if let address = IPv4Address(rawValue: rawValue) { + self = .v4(address) + } else if let address = IPv6Address(rawValue: rawValue) { + self = .v6(address) + } else { + return nil + } + } + + public var rawValue: String { + switch self { + case let .v4(address): return address.rawValue + case let .v6(address): return address.rawValue + } + } +} + +extension IPAddress: CustomStringConvertible, CustomDebugStringConvertible { + + public var description: String { + return rawValue + } + + public var debugDescription: String { + return description + } +} + +/// IPv4 Socket Address +@frozen +public struct IPv4Address: Equatable, Hashable, Codable { + + @usableFromInline + internal let bytes: CInterop.IPv4Address + + @_alwaysEmitIntoClient + public init(_ bytes: CInterop.IPv4Address) { + self.bytes = bytes + } + + @_alwaysEmitIntoClient + public func withUnsafeBytes(_ body: ((UnsafeRawBufferPointer) -> (Result))) -> Result { + Swift.withUnsafeBytes(of: bytes, body) + } +} + +public extension IPv4Address { + + /// Initialize with raw bytes. + @_alwaysEmitIntoClient + init(_ byte0: UInt8, _ byte1: UInt8, _ byte2: UInt8, _ byte3: UInt8) { + self.init(unsafeBitCast((byte0, byte1, byte2, byte3), to: CInterop.IPv4Address.self)) + } +} + +public extension IPAddress { + + /// Initialize with a IP v4 address. + @_alwaysEmitIntoClient + init(_ byte0: UInt8, _ byte1: UInt8, _ byte2: UInt8, _ byte3: UInt8) { + self = .v4(IPv4Address(byte0, byte1, byte2, byte3)) + } +} + +public extension IPv4Address { + + @_alwaysEmitIntoClient + static var any: IPv4Address { IPv4Address(_INADDR_ANY) } + + @_alwaysEmitIntoClient + static var loopback: IPv4Address { IPv4Address(_INADDR_LOOPBACK) } +} + +extension IPv4Address: RawRepresentable { + + @_alwaysEmitIntoClient + public init?(rawValue: String) { + guard let bytes = CInterop.IPv4Address(rawValue) else { + return nil + } + self.init(bytes) + } + + @_alwaysEmitIntoClient + public var rawValue: String { + return try! String(bytes) + } +} + +extension IPv4Address: CustomStringConvertible, CustomDebugStringConvertible { + + public var description: String { + return rawValue + } + + public var debugDescription: String { + return description + } +} + +/// IPv6 Socket Address +@frozen +public struct IPv6Address: Equatable, Hashable, Codable { + + @usableFromInline + internal let bytes: CInterop.IPv6Address + + @_alwaysEmitIntoClient + public init(_ bytes: CInterop.IPv6Address) { + self.bytes = bytes + } + + @_alwaysEmitIntoClient + public func withUnsafeBytes(_ body: ((UnsafeRawBufferPointer) -> (Result))) -> Result { + Swift.withUnsafeBytes(of: bytes, body) + } +} + +public extension IPv6Address { + + /// Initialize with bytes + @_alwaysEmitIntoClient + init(_ byte0: UInt16, _ byte1: UInt16, _ byte2: UInt16, _ byte3: UInt16, _ byte4: UInt16, _ byte5: UInt16, _ byte6: UInt16, _ byte7: UInt16) { + self.init(unsafeBitCast((byte0.bigEndian, byte1.bigEndian, byte2.bigEndian, byte3.bigEndian, byte4.bigEndian, byte5.bigEndian, byte6.bigEndian, byte7.bigEndian), to: CInterop.IPv6Address.self)) + } +} + +public extension IPAddress { + + /// Initialize with a IP v6 address. + @_alwaysEmitIntoClient + init(_ byte0: UInt16, _ byte1: UInt16, _ byte2: UInt16, _ byte3: UInt16, _ byte4: UInt16, _ byte5: UInt16, _ byte6: UInt16, _ byte7: UInt16) { + self = .v6(IPv6Address(byte0, byte1, byte2, byte3, byte4, byte5, byte6, byte7)) + } +} + +public extension IPv6Address { + + @_alwaysEmitIntoClient + static var any: IPv6Address { IPv6Address(_INADDR6_ANY) } + + @_alwaysEmitIntoClient + static var loopback: IPv6Address { IPv6Address(_INADDR6_LOOPBACK) } +} + +extension IPv6Address: RawRepresentable { + + @_alwaysEmitIntoClient + public init?(rawValue: String) { + guard let bytes = CInterop.IPv6Address(rawValue) else { + return nil + } + self.init(bytes) + } + + @_alwaysEmitIntoClient + public var rawValue: String { + return try! String(bytes) + } +} + +extension IPv6Address: CustomStringConvertible, CustomDebugStringConvertible { + + public var description: String { + return rawValue + } + + public var debugDescription: String { + return description + } +} diff --git a/Sources/Socket/System/MessageFlags.swift b/Sources/Socket/System/MessageFlags.swift new file mode 100644 index 0000000..a89cf7e --- /dev/null +++ b/Sources/Socket/System/MessageFlags.swift @@ -0,0 +1,51 @@ + +/// Message Flags +@frozen +public struct MessageFlags: OptionSet, Hashable, Codable { + + /// The raw C file permissions. + @_alwaysEmitIntoClient + public let rawValue: CInt + + /// Create a strongly-typed file permission from a raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + @_alwaysEmitIntoClient + private init(_ raw: CInt) { self.init(rawValue: raw) } +} + +public extension MessageFlags { + + @_alwaysEmitIntoClient + static var outOfBand: MessageFlags { MessageFlags(_MSG_OOB) } + + @_alwaysEmitIntoClient + static var peek: MessageFlags { MessageFlags(_MSG_PEEK) } + + @_alwaysEmitIntoClient + static var noRoute: MessageFlags { MessageFlags(_MSG_DONTROUTE) } + + @_alwaysEmitIntoClient + static var endOfReadline: MessageFlags { MessageFlags(_MSG_EOR) } +} + +extension MessageFlags + : CustomStringConvertible, CustomDebugStringConvertible +{ + /// A textual representation of the file permissions. + @inline(never) + public var description: String { + let descriptions: [(Element, StaticString)] = [ + (.outOfBand, ".outOfBand"), + (.peek, ".peek"), + (.noRoute, ".noRoute"), + (.endOfReadline, ".endOfReadline") + ] + + return _buildDescription(descriptions) + } + + /// A textual representation of the file permissions, suitable for debugging. + public var debugDescription: String { self.description } +} diff --git a/Sources/Socket/System/NetworkOrder.swift b/Sources/Socket/System/NetworkOrder.swift new file mode 100644 index 0000000..205a900 --- /dev/null +++ b/Sources/Socket/System/NetworkOrder.swift @@ -0,0 +1,12 @@ +extension FixedWidthInteger { + + @usableFromInline + internal var networkOrder: Self { + bigEndian + } + + @usableFromInline + internal init(networkOrder value: Self) { + self.init(bigEndian: value) + } +} diff --git a/Sources/Socket/System/Poll.swift b/Sources/Socket/System/Poll.swift new file mode 100644 index 0000000..dae2c1d --- /dev/null +++ b/Sources/Socket/System/Poll.swift @@ -0,0 +1,209 @@ +import SystemPackage + +public extension SocketDescriptor { + + /// Poll File Descriptor + struct Poll { + + internal fileprivate(set) var bytes: CInterop.PollFileDescriptor + + internal init(_ bytes: CInterop.PollFileDescriptor) { + self.bytes = bytes + } + + // Initialize an events request. + public init(socket: SocketDescriptor, events: FileEvents) { + self.init(CInterop.PollFileDescriptor(socket: socket, events: events)) + } + + public var socket: SocketDescriptor { + return SocketDescriptor(rawValue: bytes.fd) + } + + public var events: FileEvents { + return FileEvents(rawValue: bytes.events) + } + + public var returnedEvents: FileEvents { + return FileEvents(rawValue: bytes.revents) + } + } +} + +internal extension CInterop.PollFileDescriptor { + + init(socket: SocketDescriptor, events: FileEvents) { + self.init(fd: socket.rawValue, events: events.rawValue, revents: 0) + } +} + +// MARK: - Poll Operations + +extension SocketDescriptor { + + /// Wait for some event on a file descriptor. + /// + /// - Parameters: + /// - events: A bit mask specifying the events the application is interested in for the file descriptor. + /// - timeout: Specifies the minimum number of milliseconds that this method will block. Specifying a negative value in timeout means an infinite timeout. Specifying a timeout of zero causes this method to return immediately. + /// - retryOnInterrupt: Whether to retry the receive operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: A bitmask filled by the kernel with the events that actually occurred. + /// + /// The corresponding C function is `poll`. + public func poll( + for events: FileEvents, + timeout: Int = 0, + retryOnInterrupt: Bool = true + ) throws -> FileEvents { + try _poll( + events: events, + timeout: CInt(timeout), + retryOnInterrupt: retryOnInterrupt + ).get() + } + + /// `poll()` + /// + /// Wait for some event on a file descriptor. + @usableFromInline + internal func _poll( + events: FileEvents, + timeout: CInt, + retryOnInterrupt: Bool + ) -> Result { + var pollFD = CInterop.PollFileDescriptor( + fd: self.rawValue, + events: events.rawValue, + revents: 0 + ) + return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_poll(&pollFD, 1, timeout) + }.map { FileEvents(rawValue: events.rawValue) } + } +} + +extension SocketDescriptor { + + /// wait for some event on file descriptors + @usableFromInline + internal static func _poll( + _ pollFDs: inout [Poll], + timeout: CInt, + retryOnInterrupt: Bool + ) -> Result<(), Errno> { + assert(pollFDs.isEmpty == false) + let count = CInterop.FileDescriptorCount(pollFDs.count) + return pollFDs.withUnsafeMutableBufferPointer { buffer in + buffer.withMemoryRebound(to: CInterop.PollFileDescriptor.self) { cBuffer in + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_poll( + cBuffer.baseAddress!, + count, + timeout + ) + } + } + } + } +} + +extension Array where Element == SocketDescriptor.Poll { + + /// Wait for some event on a set of file descriptors. + /// + /// - Parameters: + /// - fileDescriptors: An array of bit mask specifying the events the application is interested in for the file descriptors. + /// - timeout: Specifies the minimum number of milliseconds that this method will block. Specifying a negative value in timeout means an infinite timeout. Specifying a timeout of zero causes this method to return immediately. + /// - retryOnInterrupt: Whether to retry the receive operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns:A array of bitmasks filled by the kernel with the events that actually occurred + /// for the corresponding file descriptors. + /// + /// The corresponding C function is `poll`. + public mutating func poll( + timeout: Int = 0, + retryOnInterrupt: Bool = true + ) throws { + guard isEmpty == false else { return } + try SocketDescriptor._poll(&self, timeout: CInt(timeout), retryOnInterrupt: retryOnInterrupt).get() + } + + public mutating func reset() { + for index in 0 ..< count { + self[index].bytes.revents = 0 + } + } +} + +public extension SocketDescriptor { + + /// Wait for some event on a file descriptor. + /// + /// - Parameters: + /// - events: A bit mask specifying the events the application is interested in for the file descriptor. + /// - timeout: Specifies the minimum number of milliseconds that this method will wait. + /// - retryOnInterrupt: Whether to retry the receive operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: A bitmask filled by the kernel with the events that actually occurred. + /// + /// The corresponding C function is `poll`. + func poll( + for events: FileEvents, + timeout: UInt, // ms / 1sec + sleep: UInt64 = 1_000_000, // ns / 1ms + retryOnInterrupt: Bool = true + ) async throws -> FileEvents { + assert(events.isEmpty == false, "Must specify a set of events") + return try await retry( + sleep: sleep, + timeout: timeout, + condition: { $0.isEmpty == false }) { + _poll( + events: events, + timeout: 0, + retryOnInterrupt: retryOnInterrupt + ) + } + } +} + +extension Array where Element == SocketDescriptor.Poll { + + /// Wait for some event on a set of file descriptors. + /// + /// - Parameters: + /// - fileDescriptors: An array of bit mask specifying the events the application is interested in for the file descriptors. + /// - timeout: Specifies the minimum number of milliseconds that this method will block. + /// - retryOnInterrupt: Whether to retry the receive operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns:A array of bitmasks filled by the kernel with the events that actually occurred + /// for the corresponding file descriptors. + /// + /// The corresponding C function is `poll`. + public mutating func poll( + timeout: UInt, // ms / 1sec + sleep: UInt64 = 1_000_000, // ns / 1ms + retryOnInterrupt: Bool = true + ) async throws { + guard isEmpty else { return } + return try await retry( + sleep: sleep, + timeout: timeout + ) { + SocketDescriptor._poll( + &self, + timeout: 0, + retryOnInterrupt: retryOnInterrupt + ) + } + } +} diff --git a/Sources/Socket/System/SocketAddress.swift b/Sources/Socket/System/SocketAddress.swift new file mode 100644 index 0000000..f9fdd3f --- /dev/null +++ b/Sources/Socket/System/SocketAddress.swift @@ -0,0 +1,25 @@ +import SystemPackage + +/// Socket Address +public protocol SocketAddress { + + /// Socket Protocol + associatedtype ProtocolID: SocketProtocol + + /// Unsafe pointer closure + func withUnsafePointer( + _ body: (UnsafePointer, UInt32) throws -> Result + ) rethrows -> Result + + static func withUnsafePointer( + _ body: (UnsafeMutablePointer, UInt32) throws -> () + ) rethrows -> Self +} + +public extension SocketAddress { + + @_alwaysEmitIntoClient + static var family: SocketAddressFamily { + return ProtocolID.family + } +} diff --git a/Sources/Socket/System/SocketAddress/IPv4SocketAddress.swift b/Sources/Socket/System/SocketAddress/IPv4SocketAddress.swift new file mode 100644 index 0000000..53edd42 --- /dev/null +++ b/Sources/Socket/System/SocketAddress/IPv4SocketAddress.swift @@ -0,0 +1,54 @@ +// +// IPv4SocketAddress.swift +// +// +// Created by Alsey Coleman Miller on 4/26/22. +// + +import SystemPackage + +/// IPv4 Socket Address +public struct IPv4SocketAddress: SocketAddress, Equatable, Hashable { + + public typealias ProtocolID = IPv4Protocol + + public var address: IPv4Address + + public var port: UInt16 + + @_alwaysEmitIntoClient + public init(address: IPv4Address, + port: UInt16) { + + self.address = address + self.port = port + } + + public func withUnsafePointer( + _ body: (UnsafePointer, UInt32) throws -> Result + ) rethrows -> Result { + + var socketAddress = CInterop.IPv4SocketAddress() + socketAddress.sin_family = numericCast(Self.family.rawValue) + socketAddress.sin_port = port.networkOrder + socketAddress.sin_addr = address.bytes + return try socketAddress.withUnsafePointer(body) + } + + public static func withUnsafePointer( + _ body: (UnsafeMutablePointer, UInt32) throws -> () + ) rethrows -> Self { + var socketAddress = CInterop.IPv4SocketAddress() + try socketAddress.withUnsafeMutablePointer(body) + return Self.init( + address: IPv4Address(socketAddress.sin_addr), + port: socketAddress.sin_port.networkOrder + ) + } +} + +extension CInterop.IPv4SocketAddress: CSocketAddress { + + @_alwaysEmitIntoClient + static var family: SocketAddressFamily { .ipv4 } +} diff --git a/Sources/Socket/System/SocketAddress/IPv6SocketAddress.swift b/Sources/Socket/System/SocketAddress/IPv6SocketAddress.swift new file mode 100644 index 0000000..ecbad08 --- /dev/null +++ b/Sources/Socket/System/SocketAddress/IPv6SocketAddress.swift @@ -0,0 +1,54 @@ +// +// IPv6SocketAddress.swift +// +// +// Created by Alsey Coleman Miller on 4/26/22. +// + +import SystemPackage + +/// IPv6 Socket Address +public struct IPv6SocketAddress: SocketAddress, Equatable, Hashable { + + public typealias ProtocolID = IPv6Protocol + + public var address: IPv6Address + + public var port: UInt16 + + @_alwaysEmitIntoClient + public init(address: IPv6Address, + port: UInt16) { + + self.address = address + self.port = port + } + + public func withUnsafePointer( + _ body: (UnsafePointer, UInt32) throws -> Result + ) rethrows -> Result { + + var socketAddress = CInterop.IPv6SocketAddress() + socketAddress.sin6_family = numericCast(Self.family.rawValue) + socketAddress.sin6_port = port.networkOrder + socketAddress.sin6_addr = address.bytes + return try socketAddress.withUnsafePointer(body) + } + + public static func withUnsafePointer( + _ body: (UnsafeMutablePointer, UInt32) throws -> () + ) rethrows -> Self { + var socketAddress = CInterop.IPv6SocketAddress() + try socketAddress.withUnsafeMutablePointer(body) + return Self.init( + address: IPv6Address(socketAddress.sin6_addr), + port: socketAddress.sin6_port.networkOrder + ) + } +} + +extension CInterop.IPv6SocketAddress: CSocketAddress { + + @_alwaysEmitIntoClient + static var family: SocketAddressFamily { .ipv6 } +} diff --git a/Sources/Socket/System/SocketAddress/UnixSocketAddress.swift b/Sources/Socket/System/SocketAddress/UnixSocketAddress.swift new file mode 100644 index 0000000..7fd7dd0 --- /dev/null +++ b/Sources/Socket/System/SocketAddress/UnixSocketAddress.swift @@ -0,0 +1,53 @@ +// +// UnixSocketAddress.swift +// +// +// Created by Alsey Coleman Miller on 4/26/22. +// + +import SystemPackage + +/// Unix Socket Address +public struct UnixSocketAddress: SocketAddress, Equatable, Hashable { + + public typealias ProtocolID = UnixProtocol + + public var path: FilePath + + @_alwaysEmitIntoClient + public init(path: FilePath) { + self.path = path + } + + public func withUnsafePointer( + _ body: (UnsafePointer, UInt32) throws -> Result + ) rethrows -> Result { + return try path.withPlatformString { platformString in + var socketAddress = CInterop.UnixSocketAddress() + socketAddress.sun_family = numericCast(Self.family.rawValue) + withUnsafeMutableBytes(of: &socketAddress.sun_path) { pathBytes in + pathBytes + .bindMemory(to: CInterop.PlatformChar.self) + .baseAddress! + .assign(from: platformString, count: path.length) + } + return try socketAddress.withUnsafePointer(body) + } + } + + public static func withUnsafePointer( + _ body: (UnsafeMutablePointer, UInt32) throws -> () + ) rethrows -> Self { + var socketAddress = CInterop.UnixSocketAddress() + try socketAddress.withUnsafeMutablePointer(body) + return withUnsafeBytes(of: socketAddress.sun_path) { pathPointer in + Self.init(path: FilePath(platformString: pathPointer.baseAddress!.assumingMemoryBound(to: CInterop.PlatformChar.self))) + } + } +} + +extension CInterop.UnixSocketAddress: CSocketAddress { + + @_alwaysEmitIntoClient + static var family: SocketAddressFamily { .unix } +} diff --git a/Sources/Socket/System/SocketAddressFamily.swift b/Sources/Socket/System/SocketAddressFamily.swift new file mode 100644 index 0000000..7dc1f69 --- /dev/null +++ b/Sources/Socket/System/SocketAddressFamily.swift @@ -0,0 +1,216 @@ + +/// POSIX Socket Address Family +@frozen +public struct SocketAddressFamily: RawRepresentable, Hashable, Codable { + + /// The raw socket address family identifier. + @_alwaysEmitIntoClient + public let rawValue: CInt + + /// Creates a strongly-typed socket address family from a raw address family identifier. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + @_alwaysEmitIntoClient + private init(_ raw: CInt) { self.init(rawValue: raw) } +} + +public extension SocketAddressFamily { + + /// Local communication + @_alwaysEmitIntoClient + static var unix: SocketAddressFamily { SocketAddressFamily(_AF_UNIX) } + + /// IPv4 Internet protocol + @_alwaysEmitIntoClient + static var ipv4: SocketAddressFamily { SocketAddressFamily(_AF_INET) } + + /// IPv6 Internet protocol + @_alwaysEmitIntoClient + static var ipv6: SocketAddressFamily { SocketAddressFamily(_AF_INET6) } + + /// IPX - Novell protocol + @_alwaysEmitIntoClient + static var ipx: SocketAddressFamily { SocketAddressFamily(_AF_IPX) } + + /// AppleTalk protocol + @_alwaysEmitIntoClient + static var appleTalk: SocketAddressFamily { SocketAddressFamily(_AF_APPLETALK) } +} + +#if !os(Windows) +public extension SocketAddressFamily { + + /// DECet protocol sockets + @_alwaysEmitIntoClient + static var decnet: SocketAddressFamily { SocketAddressFamily(_AF_DECnet) } + + /// VSOCK (originally "VMWare VSockets") protocol for hypervisor-guest communication + @_alwaysEmitIntoClient + static var vsock: SocketAddressFamily { SocketAddressFamily(_AF_VSOCK) } + + /// Integrated Services Digital Network protocol + @_alwaysEmitIntoClient + static var isdn: SocketAddressFamily { SocketAddressFamily(_AF_ISDN) } +} +#endif + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +public extension SocketAddressFamily { + + /// NetBIOS protocol + @_alwaysEmitIntoClient + static var netbios: SocketAddressFamily { SocketAddressFamily(_AF_NETBIOS) } + + /// + @_alwaysEmitIntoClient + static var implink: SocketAddressFamily { SocketAddressFamily(_AF_IMPLINK) } + + /// + @_alwaysEmitIntoClient + static var pup: SocketAddressFamily { SocketAddressFamily(_AF_PUP) } + + /// + @_alwaysEmitIntoClient + static var chaos: SocketAddressFamily { SocketAddressFamily(_AF_CHAOS) } + + /// + @_alwaysEmitIntoClient + static var ns: SocketAddressFamily { SocketAddressFamily(_AF_NS) } + + /// + @_alwaysEmitIntoClient + static var iso: SocketAddressFamily { SocketAddressFamily(_AF_ISO) } + + /// Generic PPP transport layer, for setting up L2 tunnels (L2TP and PPPoE). + @_alwaysEmitIntoClient + static var ppp: SocketAddressFamily { SocketAddressFamily(_AF_PPP) } + + /// + @_alwaysEmitIntoClient + static var link: SocketAddressFamily { SocketAddressFamily(_AF_LINK) } +} +#endif + +#if os(Linux) +public extension SocketAddressFamily { + + /// Amateur radio AX.25 protocol + @_alwaysEmitIntoClient + static var ax25: SocketAddressFamily { SocketAddressFamily(_AF_AX25) } + + /// ITU-T X.25 / ISO-8208 protocol + @_alwaysEmitIntoClient + static var x25: SocketAddressFamily { SocketAddressFamily(_AF_X25) } + + /// Key management protocol + @_alwaysEmitIntoClient + static var key: SocketAddressFamily { SocketAddressFamily(_AF_KEY) } + + /// Kernel user interface device + @_alwaysEmitIntoClient + static var netlink: SocketAddressFamily { SocketAddressFamily(_AF_NETLINK) } + + /// Low-level packet interface + @_alwaysEmitIntoClient + static var packet: SocketAddressFamily { SocketAddressFamily(_AF_PACKET) } + + /// Access to ATM Switched Virtual Circuits + @_alwaysEmitIntoClient + static var atm: SocketAddressFamily { SocketAddressFamily(_AF_ATMSVC) } + + /// Reliable Datagram Sockets (RDS) protocol + @_alwaysEmitIntoClient + static var rds: SocketAddressFamily { SocketAddressFamily(_AF_RDS) } + + /// Generic PPP transport layer, for setting up L2 tunnels (L2TP and PPPoE). + @_alwaysEmitIntoClient + static var ppp: SocketAddressFamily { SocketAddressFamily(_AF_PPPOX) } + + /// Legacy protocol for wide area network (WAN) connectivity that was used by Sangoma WAN cards. + @_alwaysEmitIntoClient + static var wanpipe: SocketAddressFamily { SocketAddressFamily(_AF_WANPIPE) } + + /// Logical link control (IEEE 802.2 LLC) protocol, upper part of data link layer of ISO/OSI networking protocol stack. + @_alwaysEmitIntoClient + static var link: SocketAddressFamily { SocketAddressFamily(_AF_LLC) } + + /// InfiniBand native addressing. + @_alwaysEmitIntoClient + static var ib: SocketAddressFamily { SocketAddressFamily(_AF_IB) } + + /// Multiprotocol Label Switching + @_alwaysEmitIntoClient + static var mpls: SocketAddressFamily { SocketAddressFamily(_AF_MPLS) } + + /// Controller Area Network automotive bus protocol + @_alwaysEmitIntoClient + static var can: SocketAddressFamily { SocketAddressFamily(_AF_CAN) } + + /// TIPC, "cluster domain sockets" protocol + @_alwaysEmitIntoClient + static var tipc: SocketAddressFamily { SocketAddressFamily(_AF_TIPC) } + + /// Bluetooth protocol + @_alwaysEmitIntoClient + static var bluetooth: SocketAddressFamily { SocketAddressFamily(_AF_BLUETOOTH) } + + /// IUCV (inter-user communication vehicle) z/VM protocol for hypervisor-guest interaction + @_alwaysEmitIntoClient + static var iucv: SocketAddressFamily { SocketAddressFamily(_AF_IUCV) } + + /// Rx, Andrew File System remote procedure call protocol + @_alwaysEmitIntoClient + static var rxrpc: SocketAddressFamily { SocketAddressFamily(_AF_RXRPC) } + + /// Nokia cellular modem IPC/RPC interface + @_alwaysEmitIntoClient + static var phonet: SocketAddressFamily { SocketAddressFamily(_AF_PHONET) } + + /// IEEE 802.15.4 WPAN (wireless personal area network) raw packet protocol + @_alwaysEmitIntoClient + static var ieee802154: SocketAddressFamily { SocketAddressFamily(_AF_IEEE802154) } + + /// Ericsson's Communication CPU to Application CPU interface (CAIF) protocol + @_alwaysEmitIntoClient + static var caif: SocketAddressFamily { SocketAddressFamily(_AF_CAIF) } + + /// Interface to kernel crypto API + @_alwaysEmitIntoClient + static var crypto: SocketAddressFamily { SocketAddressFamily(_AF_ALG) } + + /// KCM (kernel connection multiplexer) interface + @_alwaysEmitIntoClient + static var kcm: SocketAddressFamily { SocketAddressFamily(_AF_KCM) } + + /// Qualcomm IPC router interface protocol + @_alwaysEmitIntoClient + static var qipcrtr: SocketAddressFamily { SocketAddressFamily(_AF_QIPCRTR) } + + /// SMC-R (shared memory communications over RDMA) protocol + /// and SMC-D (shared memory communications, direct memory access) protocol for intra-node z/VM quest interaction. + @_alwaysEmitIntoClient + static var smc: SocketAddressFamily { SocketAddressFamily(_AF_SMC) } + + /// XDP (express data path) interface. + @_alwaysEmitIntoClient + static var xdp: SocketAddressFamily { SocketAddressFamily(_AF_XDP) } +} +#endif + +#if os(Windows) +public extension SocketAddressFamily { + + /// NetBIOS protocol + @_alwaysEmitIntoClient + static var netbios: SocketAddressFamily { SocketAddressFamily(_AF_NETBIOS) } + + /// IrDA protocol + @_alwaysEmitIntoClient + static var irda: SocketAddressFamily { SocketAddressFamily(_AF_IRDA) } + + /// Bluetooth protocol + @_alwaysEmitIntoClient + static var bluetooth: SocketAddressFamily { SocketAddressFamily(_AF_BTH) } +} +#endif diff --git a/Sources/Socket/System/SocketDescriptor.swift b/Sources/Socket/System/SocketDescriptor.swift new file mode 100644 index 0000000..a6d6b64 --- /dev/null +++ b/Sources/Socket/System/SocketDescriptor.swift @@ -0,0 +1,31 @@ +// +// SocketDescriptor.swift +// +// +// Created by Alsey Coleman Miller on 4/26/22. +// + +import SystemPackage + +/// Native Socket handle. +/// +/// Same as ``FileDescriptor`` on POSIX and opaque type on Windows. +public struct SocketDescriptor: RawRepresentable, Equatable, Hashable { + + #if os(Windows) + #error("Implement Windows support") + /// Native Windows Socket handle + /// + /// https://docs.microsoft.com/en-us/windows/win32/api/winsock2/ + public typealias RawValue = CInterop.WinSock + #else + /// Native POSIX Socket handle + public typealias RawValue = FileDescriptor.RawValue + #endif + + public init(rawValue: RawValue) { + self.rawValue = rawValue + } + + public let rawValue: RawValue +} diff --git a/Sources/Socket/System/SocketFlags.swift b/Sources/Socket/System/SocketFlags.swift new file mode 100644 index 0000000..3f9fcf8 --- /dev/null +++ b/Sources/Socket/System/SocketFlags.swift @@ -0,0 +1,47 @@ +#if os(Linux) +/// Flags when opening sockets. +@frozen +// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +public struct SocketFlags: OptionSet, Hashable, Codable { + + /// The raw C file events. + @_alwaysEmitIntoClient + public let rawValue: CInt + + /// Create a strongly-typed file events from a raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + @_alwaysEmitIntoClient + private init(_ cValue: CInterop.SocketType) { self.init(rawValue: numericCast(cValue.rawValue)) } +} + +public extension SocketFlags { + + /// Set the `O_NONBLOCK` file status flag on the open file description referred to by the new file + /// descriptor. Using this flag saves extra calls to `fcntl()` to achieve the same result. + @_alwaysEmitIntoClient + static var nonBlocking: SocketFlags { SocketFlags(_SOCK_NONBLOCK) } + + /// Set the close-on-exec (`FD_CLOEXEC`) flag on the new file descriptor. + @_alwaysEmitIntoClient + static var closeOnExec: SocketFlags { SocketFlags(_SOCK_CLOEXEC) } +} + +// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +extension SocketFlags: CustomStringConvertible, CustomDebugStringConvertible +{ + /// A textual representation of the open options. + @inline(never) + public var description: String { + let descriptions: [(Element, StaticString)] = [ + (.nonBlocking, ".nonBlocking"), + (.closeOnExec, ".closeOnExec") + ] + return _buildDescription(descriptions) + } + + /// A textual representation of the open options, suitable for debugging. + public var debugDescription: String { self.description } +} +#endif diff --git a/Sources/Socket/System/SocketHelpers.swift b/Sources/Socket/System/SocketHelpers.swift new file mode 100644 index 0000000..f0e9ab2 --- /dev/null +++ b/Sources/Socket/System/SocketHelpers.swift @@ -0,0 +1,86 @@ +// +// SocketHelpers.swift +// +// +// Created by Alsey Coleman Miller on 4/26/22. +// + +extension SocketDescriptor { + + /// Runs a closure and then closes the file descriptor, even if an error occurs. + /// + /// - Parameter body: The closure to run. + /// If the closure throws an error, + /// this method closes the file descriptor before it rethrows that error. + /// + /// - Returns: The value returned by the closure. + /// + /// If `body` throws an error + /// or an error occurs while closing the file descriptor, + /// this method rethrows that error. + public func closeAfter(_ body: () throws -> R) throws -> R { + // No underscore helper, since the closure's throw isn't necessarily typed. + let result: R + do { + result = try body() + } catch { + _ = try? self.close() // Squash close error and throw closure's + throw error + } + try self.close() + return result + } + + /// Runs a closure and then closes the file descriptor if an error occurs. + /// + /// - Parameter body: The closure to run. + /// If the closure throws an error, + /// this method closes the file descriptor before it rethrows that error. + /// + /// - Returns: The value returned by the closure. + /// + /// If `body` throws an error + /// this method rethrows that error. + @_alwaysEmitIntoClient + public func closeIfThrows(_ body: () throws -> R) throws -> R { + do { + return try body() + } catch { + _ = self._close() // Squash close error and throw closure's + throw error + } + } + + #if swift(>=5.5) + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + public func closeIfThrows(_ body: () async throws -> R) async throws -> R { + do { + return try await body() + } catch { + _ = self._close() // Squash close error and throw closure's + throw error + } + } + #endif + + @usableFromInline + internal func _closeIfThrows(_ body: () -> Result) -> Result { + return body().mapError { + // close if error is thrown + let _ = _close() + return $0 + } + } +} + +internal extension Result where Success == SocketDescriptor, Failure == Errno { + + @usableFromInline + func _closeIfThrows(_ body: (SocketDescriptor) -> Result) -> Result { + return flatMap { fileDescriptor in + fileDescriptor._closeIfThrows { + body(fileDescriptor) + } + } + } +} diff --git a/Sources/Socket/System/SocketOperations.swift b/Sources/Socket/System/SocketOperations.swift new file mode 100644 index 0000000..33f79bb --- /dev/null +++ b/Sources/Socket/System/SocketOperations.swift @@ -0,0 +1,588 @@ +import SystemPackage + +extension SocketDescriptor { + + /// Creates an endpoint for communication and returns a descriptor. + /// + /// - Parameters: + /// - protocolID: The protocol which will be used for communication. + /// - retryOnInterrupt: Whether to retry the read operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: The file descriptor of the opened socket. + /// + @_alwaysEmitIntoClient + public init( + _ protocolID: T, + retryOnInterrupt: Bool = true + ) throws { + self = try Self._socket(T.family, type: protocolID.type.rawValue, protocol: protocolID.rawValue, retryOnInterrupt: retryOnInterrupt).get() + } + + #if os(Linux) + /// Creates an endpoint for communication and returns a descriptor. + /// + /// - Parameters: + /// - protocolID: The protocol which will be used for communication. + /// - flags: Flags to set when opening the socket. + /// - retryOnInterrupt: Whether to retry the read operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: The file descriptor of the opened socket. + /// + @_alwaysEmitIntoClient + public init( + _ protocolID: T, + flags: SocketFlags, + retryOnInterrupt: Bool = true + ) throws { + self = try Self._socket(T.family, type: protocolID.type.rawValue | flags.rawValue, protocol: protocolID.rawValue, retryOnInterrupt: retryOnInterrupt).get() + } + #endif + + @usableFromInline + internal static func _socket( + _ family: SocketAddressFamily, + type: CInt, + protocol protocolID: Int32, + retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_socket(family.rawValue, type, protocolID) + }.map({ SocketDescriptor(rawValue: $0) }) + } + + /// Creates an endpoint for communication and returns a descriptor. + /// + /// - Parameters: + /// - protocolID: The protocol which will be used for communication. + /// - flags: Flags to set when opening the socket. + /// - retryOnInterrupt: Whether to retry the read operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: The file descriptor of the opened socket. + /// + @_alwaysEmitIntoClient + public init( + _ protocolID: Address.ProtocolID, + bind address: Address, + retryOnInterrupt: Bool = true + ) throws { + self = try Self._socket( + address: address, + type: protocolID.type.rawValue, + protocol: protocolID.rawValue, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + #if os(Linux) + /// Creates an endpoint for communication and returns a descriptor. + /// + /// - Parameters: + /// - protocolID: The protocol which will be used for communication. + /// - flags: Flags to set when opening the socket. + /// - retryOnInterrupt: Whether to retry the read operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: The file descriptor of the opened socket. + /// + @_alwaysEmitIntoClient + public init( + _ protocolID: Address.ProtocolID, + bind address: Address, + flags: SocketFlags, + retryOnInterrupt: Bool = true + ) throws { + self = try Self._socket( + address: address, + type: protocolID.type.rawValue | flags.rawValue, + protocol: protocolID.rawValue, + retryOnInterrupt: retryOnInterrupt + ).get() + } + #endif + + @usableFromInline + internal static func _socket( + address: Address, + type: CInt, + protocol protocolID: Int32, + retryOnInterrupt: Bool + ) -> Result { + return _socket( + Address.family, + type: type, + protocol: protocolID, + retryOnInterrupt: retryOnInterrupt + )._closeIfThrows { fileDescriptor in + fileDescriptor + ._bind(address, retryOnInterrupt: retryOnInterrupt) + .map { fileDescriptor } + } + } + + /// Assigns the address specified to the socket referred to by the file descriptor. + /// + /// - Parameter address: Specifies the address to bind the socket. + /// - Parameter retryOnInterrupt: Whether to retry the open operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// + /// The corresponding C function is `bind`. + @_alwaysEmitIntoClient + public func bind( + _ address: Address, + retryOnInterrupt: Bool = true + ) throws { + try _bind(address, retryOnInterrupt: retryOnInterrupt).get() + } + + @usableFromInline + internal func _bind( + _ address: T, + retryOnInterrupt: Bool + ) -> Result<(), Errno> { + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + address.withUnsafePointer { (addressPointer, length) in + system_bind(rawValue, addressPointer, length) + } + } + } + + /// Set the option specified for the socket associated with the file descriptor. + /// + /// - Parameter option: Socket option value to set. + /// - Parameter retryOnInterrupt: Whether to retry the open operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// + /// The method corresponds to the C function `setsockopt`. + @_alwaysEmitIntoClient + public func setSocketOption( + _ option: T, + retryOnInterrupt: Bool = true + ) throws { + try _setSocketOption(option, retryOnInterrupt: retryOnInterrupt).get() + } + + @usableFromInline + internal func _setSocketOption( + _ option: T, + retryOnInterrupt: Bool + ) -> Result<(), Errno> { + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + option.withUnsafeBytes { bufferPointer in + system_setsockopt(self.rawValue, T.ID.optionLevel.rawValue, T.id.rawValue, bufferPointer.baseAddress!, UInt32(bufferPointer.count)) + } + } + } + + /// Retrieve the value associated with the option specified for the socket associated with the file descriptor. + /// + /// - Parameter option: Type of `SocketOption` to retrieve. + /// - Parameter retryOnInterrupt: Whether to retry the open operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// + /// The method corresponds to the C function `getsockopt`. + @_alwaysEmitIntoClient + public func getSocketOption( + _ option: T.Type, + retryOnInterrupt: Bool = true + ) throws -> T { + return try _getSocketOption(option, retryOnInterrupt: retryOnInterrupt) + } + + @usableFromInline + internal func _getSocketOption( + _ option: T.Type, + retryOnInterrupt: Bool + ) throws -> T { + return try T.withUnsafeBytes { bufferPointer in + var length = UInt32(bufferPointer.count) + guard system_getsockopt(self.rawValue, T.ID.optionLevel.rawValue, T.id.rawValue, bufferPointer.baseAddress!, &length) != -1 else { + throw Errno.current + } + } + } + + /// Send a message from a socket. + /// + /// - Parameters: + /// - buffer: The region of memory that contains the data being sent. + /// - flags: see `send(2)` + /// - retryOnInterrupt: Whether to retry the send operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: The number of bytes that were sent. + /// + /// The corresponding C function is `send`. + @_alwaysEmitIntoClient + public func send( + _ buffer: UnsafeRawBufferPointer, + flags: MessageFlags = [], + retryOnInterrupt: Bool = true + ) throws -> Int { + try _send(buffer, flags: flags, retryOnInterrupt: retryOnInterrupt).get() + } + + /// Send a message from a socket. + /// + /// - Parameters: + /// - data: The sequence of bytes being sent. + /// - address: Address of destination client. + /// - flags: see `send(2)` + /// - retryOnInterrupt: Whether to retry the send operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: The number of bytes that were sent. + /// + /// The corresponding C function is `send`. + public func send( + _ data: Data, + flags: MessageFlags = [], + retryOnInterrupt: Bool = true + ) throws -> Int where Data: Sequence, Data.Element == UInt8 { + try data._withRawBufferPointer { dataPointer in + _send(dataPointer, flags: flags, retryOnInterrupt: retryOnInterrupt) + }.get() + } + + @usableFromInline + internal func _send( + _ buffer: UnsafeRawBufferPointer, + flags: MessageFlags, + retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_send(self.rawValue, buffer.baseAddress!, buffer.count, flags.rawValue) + } + } + + /// Send a message from a socket. + /// + /// - Parameters: + /// - buffer: The region of memory that contains the data being sent. + /// - address: Address of destination client. + /// - flags: see `send(2)` + /// - retryOnInterrupt: Whether to retry the send operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: The number of bytes that were sent. + /// + /// The corresponding C function is `send`. + @_alwaysEmitIntoClient + public func send( + _ buffer: UnsafeRawBufferPointer, + to address: Address, + flags: MessageFlags = [], + retryOnInterrupt: Bool = true + ) throws -> Int { + try _send(buffer, to: address, flags: flags, retryOnInterrupt: retryOnInterrupt).get() + } + + /// Send a message from a socket. + /// + /// - Parameters: + /// - data: The sequence of bytes being sent. + /// - address: Address of destination client. + /// - flags: see `send(2)` + /// - retryOnInterrupt: Whether to retry the send operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: The number of bytes that were sent. + /// + /// The corresponding C function is `send`. + public func send( + _ data: Data, + to address: Address, + flags: MessageFlags = [], + retryOnInterrupt: Bool = true + ) throws -> Int where Address: SocketAddress, Data: Sequence, Data.Element == UInt8 { + try data._withRawBufferPointer { dataPointer in + _send(dataPointer, to: address, flags: flags, retryOnInterrupt: retryOnInterrupt) + }.get() + } + + /// `send()` + @usableFromInline + internal func _send( + _ data: UnsafeRawBufferPointer, + to address: T, + flags: MessageFlags, + retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + address.withUnsafePointer { (addressPointer, addressLength) in + system_sendto(self.rawValue, data.baseAddress, data.count, flags.rawValue, addressPointer, addressLength) + } + } + } + + /// Receive a message from a socket. + /// + /// - Parameters: + /// - buffer: The region of memory to receive into. + /// - flags: see `recv(2)` + /// - retryOnInterrupt: Whether to retry the receive operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: The number of bytes that were received. + /// + /// The corresponding C function is `recv`. + @_alwaysEmitIntoClient + public func receive( + into buffer: UnsafeMutableRawBufferPointer, + flags: MessageFlags = [], + retryOnInterrupt: Bool = true + ) throws -> Int { + try _receive( + into: buffer, flags: flags, retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal func _receive( + into buffer: UnsafeMutableRawBufferPointer, + flags: MessageFlags, + retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_recv(self.rawValue, buffer.baseAddress!, buffer.count, flags.rawValue) + } + } + + @usableFromInline + internal func _recieve( + _ dataBuffer: UnsafeMutableRawBufferPointer, + flags: MessageFlags, + retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_recv(self.rawValue, dataBuffer.baseAddress, dataBuffer.count, flags.rawValue) + } + } + + /// Listen for connections on a socket. + /// + /// Only applies to sockets of connection type `.stream`. + /// + /// - Parameters: + /// - backlog: the maximum length for the queue of pending connections + /// - retryOnInterrupt: Whether to retry the receive operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// + /// The corresponding C function is `listen`. + @_alwaysEmitIntoClient + public func listen( + backlog: Int, + retryOnInterrupt: Bool = true + ) throws { + try _listen(backlog: Int32(backlog), retryOnInterrupt: retryOnInterrupt).get() + } + + @usableFromInline + internal func _listen( + backlog: Int32, + retryOnInterrupt: Bool + ) -> Result<(), Errno> { + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_listen(self.rawValue, backlog) + } + } + + /// Accept a connection on a socket. + /// + /// - Parameters: + /// - address: The type of the `SocketAddress` expected for the new connection. + /// - retryOnInterrupt: Whether to retry the receive operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: A tuple containing the file descriptor and address of the new connection. + /// + /// The corresponding C function is `accept`. + @_alwaysEmitIntoClient + public func accept( + _ address: Address.Type, + retryOnInterrupt: Bool = true + ) throws -> (SocketDescriptor, Address) { + return try _accept(address, retryOnInterrupt: retryOnInterrupt).get() + } + + @usableFromInline + internal func _accept( + _ address: Address.Type, + retryOnInterrupt: Bool + ) -> Result<(SocketDescriptor, Address), Errno> { + var result: Result = .success(0) + let address = Address.withUnsafePointer { socketPointer, socketLength in + var length = socketLength + result = valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_accept(self.rawValue, socketPointer, &length) + } + } + return result.map { (SocketDescriptor(rawValue: $0), address) } + } + + /// Accept a connection on a socket. + /// + /// - Parameters: + /// - retryOnInterrupt: Whether to retry the receive operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: The file descriptor of the new connection. + /// + /// The corresponding C function is `accept`. + @_alwaysEmitIntoClient + public func accept( + retryOnInterrupt: Bool = true + ) throws -> SocketDescriptor { + return try _accept(retryOnInterrupt: retryOnInterrupt).get() + } + + @usableFromInline + internal func _accept( + retryOnInterrupt: Bool + ) -> Result { + var length: UInt32 = 0 + return valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_accept(self.rawValue, nil, &length) + }.map(SocketDescriptor.init(rawValue:)) + } + + /// Initiate a connection on a socket. + /// + /// - Parameters: + /// - address: The peer address. + /// - retryOnInterrupt: Whether to retry the receive operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: The file descriptor of the new connection. + /// + /// The corresponding C function is `connect`. + @_alwaysEmitIntoClient + public func connect( + to address: Address, + retryOnInterrupt: Bool = true + ) throws { + try _connect(to: address, retryOnInterrupt: retryOnInterrupt).get() + } + + /// The `connect()` function shall attempt to make a connection on a socket. + @usableFromInline + internal func _connect( + to address: Address, + retryOnInterrupt: Bool + ) -> Result<(), Errno> { + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + address.withUnsafePointer { (addressPointer, addressLength) in + system_connect(self.rawValue, addressPointer, addressLength) + } + } + } + + /// Deletes a file descriptor. + /// + /// Deletes the file descriptor from the per-process object reference table. + /// If this is the last reference to the underlying object, + /// the object will be deactivated. + /// + /// The corresponding C function is `close`. + @_alwaysEmitIntoClient + public func close() throws { try _close().get() } + + @usableFromInline + internal func _close() -> Result<(), Errno> { + nothingOrErrno(retryOnInterrupt: false) { system_close(self.rawValue) } + } + + + /// Reads bytes at the current file offset into a buffer. + /// + /// - Parameters: + /// - buffer: The region of memory to read into. + /// - retryOnInterrupt: Whether to retry the read operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: The number of bytes that were read. + /// + /// The property of `buffer` + /// determines the maximum number of bytes that are read into that buffer. + /// + /// After reading, + /// this method increments the file's offset by the number of bytes read. + /// To change the file's offset, + /// call the ``seek(offset:from:)`` method. + /// + /// The corresponding C function is `read`. + @_alwaysEmitIntoClient + public func read( + into buffer: UnsafeMutableRawBufferPointer, + retryOnInterrupt: Bool = true + ) throws -> Int { + try _read(into: buffer, retryOnInterrupt: retryOnInterrupt).get() + } + + @usableFromInline + internal func _read( + into buffer: UnsafeMutableRawBufferPointer, + retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_read(self.rawValue, buffer.baseAddress, buffer.count) + } + } + + /// Writes the contents of a buffer at the current file offset. + /// + /// - Parameters: + /// - buffer: The region of memory that contains the data being written. + /// - retryOnInterrupt: Whether to retry the write operation + /// if it throws ``Errno/interrupted``. + /// The default is `true`. + /// Pass `false` to try only once and throw an error upon interruption. + /// - Returns: The number of bytes that were written. + /// + /// After writing, + /// this method increments the file's offset by the number of bytes written. + /// To change the file's offset, + /// call the ``seek(offset:from:)`` method. + /// + /// The corresponding C function is `write`. + @_alwaysEmitIntoClient + public func write( + _ buffer: UnsafeRawBufferPointer, + retryOnInterrupt: Bool = true + ) throws -> Int { + try _write(buffer, retryOnInterrupt: retryOnInterrupt).get() + } + + @usableFromInline + internal func _write( + _ buffer: UnsafeRawBufferPointer, + retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_write(self.rawValue, buffer.baseAddress, buffer.count) + } + } +} diff --git a/Sources/Socket/System/SocketOption.swift b/Sources/Socket/System/SocketOption.swift new file mode 100644 index 0000000..13a59c5 --- /dev/null +++ b/Sources/Socket/System/SocketOption.swift @@ -0,0 +1,76 @@ + +/// POSIX Socket Option ID +public protocol SocketOption { + + associatedtype ID: SocketOptionID + + static var id: ID { get } + + func withUnsafeBytes(_ pointer: ((UnsafeRawBufferPointer) throws -> (Result))) rethrows -> Result + + static func withUnsafeBytes( + _ body: (UnsafeMutableRawBufferPointer) throws -> () + ) rethrows -> Self +} + +public protocol BooleanSocketOption: SocketOption { + + init(_ boolValue: Bool) + + var boolValue: Bool { get } +} + +extension BooleanSocketOption where Self: ExpressibleByBooleanLiteral { + + @_alwaysEmitIntoClient + public init(booleanLiteral boolValue: Bool) { + self.init(boolValue) + } +} + +public extension BooleanSocketOption { + + func withUnsafeBytes(_ pointer: ((UnsafeRawBufferPointer) throws -> (Result))) rethrows -> Result { + return try Swift.withUnsafeBytes(of: boolValue.cInt) { bufferPointer in + try pointer(bufferPointer) + } + } + + static func withUnsafeBytes(_ body: (UnsafeMutableRawBufferPointer) throws -> ()) rethrows -> Self { + var value: CInt = 0 + try Swift.withUnsafeMutableBytes(of: &value, body) + return Self.init(Bool(value)) + } +} + +/// Platform Socket Option +public extension GenericSocketOption { + + @frozen + struct Debug: BooleanSocketOption, Equatable, Hashable, ExpressibleByBooleanLiteral { + + @_alwaysEmitIntoClient + public static var id: GenericSocketOption { .debug } + + public var boolValue: Bool + + @_alwaysEmitIntoClient + public init(_ boolValue: Bool) { + self.boolValue = boolValue + } + } + + @frozen + struct KeepAlive: BooleanSocketOption, Equatable, Hashable, ExpressibleByBooleanLiteral { + + @_alwaysEmitIntoClient + public static var id: GenericSocketOption { .keepAlive } + + public var boolValue: Bool + + @_alwaysEmitIntoClient + public init(_ boolValue: Bool) { + self.boolValue = boolValue + } + } +} diff --git a/Sources/Socket/System/SocketOptionID.swift b/Sources/Socket/System/SocketOptionID.swift new file mode 100644 index 0000000..218affb --- /dev/null +++ b/Sources/Socket/System/SocketOptionID.swift @@ -0,0 +1,37 @@ + +/// POSIX Socket Option ID +public protocol SocketOptionID: RawRepresentable { + + static var optionLevel: SocketOptionLevel { get } + + init?(rawValue: Int32) + + var rawValue: Int32 { get } +} + +@frozen +public struct GenericSocketOption: RawRepresentable, Equatable, Hashable, SocketOptionID { + + @_alwaysEmitIntoClient + public static var optionLevel: SocketOptionLevel { .default } + + /// The raw socket address family identifier. + @_alwaysEmitIntoClient + public let rawValue: CInt + + /// Creates a strongly-typed socket address family from a raw address family identifier. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + @_alwaysEmitIntoClient + private init(_ raw: CInt) { self.init(rawValue: raw) } +} + +public extension GenericSocketOption { + + @_alwaysEmitIntoClient + static var debug: GenericSocketOption { GenericSocketOption(_SO_DEBUG) } + + @_alwaysEmitIntoClient + static var keepAlive: GenericSocketOption { GenericSocketOption(_SO_KEEPALIVE) } +} diff --git a/Sources/Socket/System/SocketOptionLevel.swift b/Sources/Socket/System/SocketOptionLevel.swift new file mode 100644 index 0000000..19fe166 --- /dev/null +++ b/Sources/Socket/System/SocketOptionLevel.swift @@ -0,0 +1,33 @@ + +// POSIX Socket Option Level +@frozen +public struct SocketOptionLevel: RawRepresentable, Hashable, Codable { + + /// The raw socket address family identifier. + @_alwaysEmitIntoClient + public let rawValue: CInt + + /// Creates a strongly-typed socket address family from a raw address family identifier. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + @_alwaysEmitIntoClient + private init(_ raw: CInt) { self.init(rawValue: raw) } +} + +public extension SocketOptionLevel { + + @_alwaysEmitIntoClient + static var `default`: SocketOptionLevel { SocketOptionLevel(_SOL_SOCKET) } +} + +#if os(Linux) +public extension SocketOptionLevel { + + @_alwaysEmitIntoClient + static var netlink: SocketOptionLevel { SocketOptionLevel(_SOL_NETLINK) } + + @_alwaysEmitIntoClient + static var bluetooth: SocketOptionLevel { SocketOptionLevel(_SOL_BLUETOOTH) } +} +#endif diff --git a/Sources/Socket/System/SocketProtocol.swift b/Sources/Socket/System/SocketProtocol.swift new file mode 100644 index 0000000..67ad255 --- /dev/null +++ b/Sources/Socket/System/SocketProtocol.swift @@ -0,0 +1,12 @@ + +/// POSIX Socket Protocol +public protocol SocketProtocol: RawRepresentable { + + static var family: SocketAddressFamily { get } + + var type: SocketType { get } + + init?(rawValue: Int32) + + var rawValue: Int32 { get } +} diff --git a/Sources/Socket/System/SocketProtocol/IPv4Protocol.swift b/Sources/Socket/System/SocketProtocol/IPv4Protocol.swift new file mode 100644 index 0000000..b611fd0 --- /dev/null +++ b/Sources/Socket/System/SocketProtocol/IPv4Protocol.swift @@ -0,0 +1,35 @@ +// +// IPv4SocketAddress.swift +// +// +// Created by Alsey Coleman Miller on 4/26/22. +// + +/// IPv4 Protocol Family +public enum IPv4Protocol: Int32, Codable, SocketProtocol { + + case raw + case tcp + case udp + + @_alwaysEmitIntoClient + public static var family: SocketAddressFamily { .ipv4 } + + @_alwaysEmitIntoClient + public var type: SocketType { + switch self { + case .raw: return .raw + case .tcp: return .stream + case .udp: return .datagram + } + } + + @_alwaysEmitIntoClient + public var rawValue: Int32 { + switch self { + case .raw: return _IPPROTO_RAW + case .tcp: return _IPPROTO_TCP + case .udp: return _IPPROTO_UDP + } + } +} diff --git a/Sources/Socket/System/SocketProtocol/IPv6Protocol.swift b/Sources/Socket/System/SocketProtocol/IPv6Protocol.swift new file mode 100644 index 0000000..218c108 --- /dev/null +++ b/Sources/Socket/System/SocketProtocol/IPv6Protocol.swift @@ -0,0 +1,35 @@ +// +// IPv6SocketAddress.swift +// +// +// Created by Alsey Coleman Miller on 4/26/22. +// + +/// IPv6 Protocol Family +public enum IPv6Protocol: Int32, Codable, SocketProtocol { + + case raw + case tcp + case udp + + @_alwaysEmitIntoClient + public static var family: SocketAddressFamily { .ipv6 } + + @_alwaysEmitIntoClient + public var type: SocketType { + switch self { + case .raw: return .raw + case .tcp: return .stream + case .udp: return .datagram + } + } + + @_alwaysEmitIntoClient + public var rawValue: Int32 { + switch self { + case .raw: return _IPPROTO_RAW + case .tcp: return _IPPROTO_TCP + case .udp: return _IPPROTO_UDP + } + } +} diff --git a/Sources/Socket/System/SocketProtocol/UnixProtocol.swift b/Sources/Socket/System/SocketProtocol/UnixProtocol.swift new file mode 100644 index 0000000..023c2ad --- /dev/null +++ b/Sources/Socket/System/SocketProtocol/UnixProtocol.swift @@ -0,0 +1,22 @@ +// +// UnixProtocol.swift +// +// +// Created by Alsey Coleman Miller on 4/26/22. +// + +/// Unix Protocol Family +public enum UnixProtocol: Int32, Codable, SocketProtocol { + + case raw = 0 + + @_alwaysEmitIntoClient + public static var family: SocketAddressFamily { .unix } + + @_alwaysEmitIntoClient + public var type: SocketType { + switch self { + case .raw: return .raw + } + } +} diff --git a/Sources/Socket/System/SocketType.swift b/Sources/Socket/System/SocketType.swift new file mode 100644 index 0000000..f15142d --- /dev/null +++ b/Sources/Socket/System/SocketType.swift @@ -0,0 +1,62 @@ +import SystemPackage + +/// POSIX Socket Type +@frozen +public struct SocketType: RawRepresentable, Hashable, Codable { + + /// The raw socket type identifier. + @_alwaysEmitIntoClient + public let rawValue: CInt + + /// Creates a strongly-typed socket type from a raw socket type identifier. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + @_alwaysEmitIntoClient + private init(_ cValue: CInterop.SocketType) { + #if os(Linux) + self.init(rawValue: numericCast(cValue.rawValue)) + #else + self.init(rawValue: cValue) + #endif + } +} + +public extension SocketType { + + /// Stream socket + /// + /// Provides sequenced, reliable, two-way, connection-based byte streams. + /// An out-of-band data transmission mechanism may be supported. + @_alwaysEmitIntoClient + static var stream: SocketType { SocketType(_SOCK_STREAM) } + + /// Supports datagrams (connectionless, unreliable messages of a fixed maximum length). + @_alwaysEmitIntoClient + static var datagram: SocketType { SocketType(_SOCK_DGRAM) } + + /// Provides raw network protocol access. + @_alwaysEmitIntoClient + static var raw: SocketType { SocketType(_SOCK_RAW) } + + /// Provides a reliable datagram layer that does not guarantee ordering. + @_alwaysEmitIntoClient + static var reliableDatagramMessage: SocketType { SocketType(_SOCK_RDM) } + + /// Provides a sequenced, reliable, two-way connection-based data transmission + /// path for datagrams of fixed maximum length; a consumer is required to read + /// an entire packet with each input system call. + @_alwaysEmitIntoClient + static var sequencedPacket: SocketType { SocketType(_SOCK_SEQPACKET) } +} + +#if os(Linux) +public extension SocketType { + + /// Datagram Congestion Control Protocol + /// + /// Linux specific way of getting packets at the dev level. + @_alwaysEmitIntoClient + static var datagramCongestionControlProtocol: SocketType { SocketType(_SOCK_DCCP) } +} +#endif diff --git a/Sources/Socket/System/Syscalls.swift b/Sources/Socket/System/Syscalls.swift new file mode 100644 index 0000000..91da685 --- /dev/null +++ b/Sources/Socket/System/Syscalls.swift @@ -0,0 +1,423 @@ +import SystemPackage + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import Glibc +#elseif os(Windows) +import ucrt +#else +#error("Unsupported Platform") +#endif + +@inline(__always) +internal var mockingEnabled: Bool { + // Fast constant-foldable check for release builds + #if ENABLE_MOCKING + return contextualMockingEnabled + #else + return false + #endif +} + + +#if ENABLE_MOCKING +// Strip the mock_system prefix and the arg list suffix +private func originalSyscallName(_ function: String) -> String { + // `function` must be of format `system_()` + precondition(function.starts(with: "system_")) + return String(function.dropFirst("system_".count).prefix { $0 != "(" }) +} + +private func mockImpl( + name: String, + path: UnsafePointer?, + _ args: [AnyHashable] +) -> CInt { + precondition(mockingEnabled) + let origName = originalSyscallName(name) + guard let driver = currentMockingDriver else { + fatalError("Mocking requested from non-mocking context") + } + var mockArgs: Array = [] + if let p = path { + mockArgs.append(String(_errorCorrectingPlatformString: p)) + } + mockArgs.append(contentsOf: args) + driver.trace.add(Trace.Entry(name: origName, mockArgs)) + + switch driver.forceErrno { + case .none: break + case .always(let e): + system_errno = e + return -1 + case .counted(let e, let count): + assert(count >= 1) + system_errno = e + driver.forceErrno = count > 1 ? .counted(errno: e, count: count-1) : .none + return -1 + } + + return 0 +} + +internal func _mock( + name: String = #function, path: UnsafePointer? = nil, _ args: AnyHashable... +) -> CInt { + return mockImpl(name: name, path: path, args) +} +internal func _mockInt( + name: String = #function, path: UnsafePointer? = nil, _ args: AnyHashable... +) -> Int { + Int(mockImpl(name: name, path: path, args)) +} + +#endif // ENABLE_MOCKING + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +internal var system_errno: CInt { + get { Darwin.errno } + set { Darwin.errno = newValue } +} +#elseif os(Windows) +internal var system_errno: CInt { + get { + var value: CInt = 0 + // TODO(compnerd) handle the error? + _ = ucrt._get_errno(&value) + return value + } + set { + _ = ucrt._set_errno(newValue) + } +} +#else +internal var system_errno: CInt { + get { Glibc.errno } + set { Glibc.errno = newValue } +} +#endif + +// close +internal func system_close(_ fd: Int32) -> Int32 { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(fd) } +#endif + return close(fd) +} + +// write +internal func system_write( + _ fd: Int32, _ buf: UnsafeRawPointer!, _ nbyte: Int +) -> Int { +#if ENABLE_MOCKING + if mockingEnabled { return _mockInt(fd, buf, nbyte) } +#endif + return write(fd, buf, nbyte) +} + +// read +internal func system_read( + _ fd: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int +) -> Int { +#if ENABLE_MOCKING + if mockingEnabled { return _mockInt(fd, buf, nbyte) } +#endif + return read(fd, buf, nbyte) +} + +internal func system_inet_pton( + _ family: Int32, + _ cString: UnsafePointer, + _ address: UnsafeMutableRawPointer) -> Int32 { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(family, cString, address) } + #endif + return inet_pton(family, cString, address) +} + +internal func system_inet_ntop(_ family: Int32, _ pointer : UnsafeRawPointer, _ string: UnsafeMutablePointer, _ length: UInt32) -> UnsafePointer? { + #if ENABLE_MOCKING + //if mockingEnabled { return _mock(family, pointer, string, length) } + #endif + return inet_ntop(family, pointer, string, length) +} + +internal func system_socket(_ fd: Int32, _ fd2: Int32, _ fd3: Int32) -> Int32 { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, fd2, fd3) } + #endif + return socket(fd, fd2, fd3) +} + +internal func system_setsockopt(_ fd: Int32, _ fd2: Int32, _ fd3: Int32, _ pointer: UnsafeRawPointer, _ dataLength: UInt32) -> Int32 { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, fd2, fd3, pointer, dataLength) } + #endif + return setsockopt(fd, fd2, fd3, pointer, dataLength) +} + +internal func system_getsockopt( + _ socket: CInt, + _ level: CInt, + _ option: CInt, + _ value: UnsafeMutableRawPointer?, + _ length: UnsafeMutablePointer? +) -> CInt { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(socket, level, option, value, length) } + #endif + return getsockopt(socket, level, option, value, length) +} + +internal func system_bind( + _ socket: CInt, + _ address: UnsafePointer, + _ length: UInt32 +) -> CInt { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(socket, address, length) } + #endif + return bind(socket, address, length) +} + +internal func system_connect( + _ socket: CInt, + _ addr: UnsafePointer?, + _ len: socklen_t +) -> CInt { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(socket, addr, len) } + #endif + return connect(socket, addr, len) +} + +internal func system_accept( + _ socket: CInt, + _ addr: UnsafeMutablePointer?, + _ len: UnsafeMutablePointer? +) -> CInt { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(socket, addr, len) } + #endif + return accept(socket, addr, len) +} + +internal func system_getaddrinfo( + _ hostname: UnsafePointer?, + _ servname: UnsafePointer?, + _ hints: UnsafePointer?, + _ res: UnsafeMutablePointer?>? +) -> CInt { + #if ENABLE_MOCKING + if mockingEnabled { + return _mock(hostname, + servname, + hints, res) + } + #endif + return getaddrinfo(hostname, servname, hints, res) +} + +internal func system_getnameinfo( + _ sa: UnsafePointer?, + _ salen: UInt32, + _ host: UnsafeMutablePointer?, + _ hostlen: UInt32, + _ serv: UnsafeMutablePointer?, + _ servlen: UInt32, + _ flags: CInt +) -> CInt { + #if ENABLE_MOCKING + if mockingEnabled { + return _mock(sa, salen, host, hostlen, serv, servlen, flags) + } + #endif + return getnameinfo(sa, salen, host, hostlen, serv, servlen, flags) +} + +internal func system_freeaddrinfo( + _ addrinfo: UnsafeMutablePointer? +) { + #if ENABLE_MOCKING + if mockingEnabled { + _ = _mock(addrinfo) + return + } + #endif + return freeaddrinfo(addrinfo) +} + +internal func system_gai_strerror(_ error: CInt) -> UnsafePointer { + #if ENABLE_MOCKING + // FIXME + #endif + return gai_strerror(error) +} + +internal func system_shutdown(_ socket: CInt, _ how: CInt) -> CInt { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(socket, how) } + #endif + return shutdown(socket, how) +} + +internal func system_listen(_ socket: CInt, _ backlog: CInt) -> CInt { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(socket, backlog) } + #endif + return listen(socket, backlog) +} + +internal func system_send( + _ socket: Int32, _ buffer: UnsafeRawPointer?, _ len: Int, _ flags: Int32 +) -> Int { + #if ENABLE_MOCKING + if mockingEnabled { return _mockInt(socket, buffer, len, flags) } + #endif + return send(socket, buffer, len, flags) +} + +internal func system_recv( + _ socket: Int32, + _ buffer: UnsafeMutableRawPointer?, + _ len: Int, + _ flags: Int32 +) -> Int { + #if ENABLE_MOCKING + if mockingEnabled { return _mockInt(socket, buffer, len, flags) } + #endif + return recv(socket, buffer, len, flags) +} + +internal func system_sendto( + _ socket: CInt, + _ buffer: UnsafeRawPointer?, + _ length: Int, + _ flags: CInt, + _ dest_addr: UnsafePointer?, + _ dest_len: UInt32 +) -> Int { + #if ENABLE_MOCKING + if mockingEnabled { + return _mockInt(socket, buffer, length, flags, dest_addr, dest_len) + } + #endif + return sendto(socket, buffer, length, flags, dest_addr, dest_len) +} + +internal func system_recvfrom( + _ socket: CInt, + _ buffer: UnsafeMutableRawPointer?, + _ length: Int, + _ flags: CInt, + _ address: UnsafeMutablePointer?, + _ addres_len: UnsafeMutablePointer? +) -> Int { + #if ENABLE_MOCKING + if mockingEnabled { + return _mockInt(socket, buffer, length, flags, address, addres_len) + } + #endif + return recvfrom(socket, buffer, length, flags, address, addres_len) +} + +internal func system_poll( + _ fileDescriptors: UnsafeMutablePointer, + _ fileDescriptorsCount: CInterop.FileDescriptorCount, + _ timeout: CInt +) -> CInt { + #if ENABLE_MOCKING + if mockingEnabled { return _mock(fileDescriptors, fileDescriptorsCount, timeout) } + #endif + return poll(fileDescriptors, fileDescriptorsCount, timeout) +} + +internal func system_sendmsg( + _ socket: CInt, + _ message: UnsafePointer?, + _ flags: CInt +) -> Int { + #if ENABLE_MOCKING + if mockingEnabled { return _mockInt(socket, message, flags) } + #endif + return sendmsg(socket, message, flags) +} + +internal func system_recvmsg( + _ socket: CInt, + _ message: UnsafeMutablePointer?, + _ flags: CInt +) -> Int { + #if ENABLE_MOCKING + if mockingEnabled { return _mockInt(socket, message, flags) } + #endif + return recvmsg(socket, message, flags) +} + +internal func system_fcntl( + _ fd: Int32, + _ cmd: Int32 +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, cmd) } +#endif + return fcntl(fd, cmd) +} + +internal func system_fcntl( + _ fd: Int32, + _ cmd: Int32, + _ value: Int32 +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, cmd, value) } +#endif + return fcntl(fd, cmd, value) +} + +internal func system_fcntl( + _ fd: Int32, + _ cmd: Int32, + _ pointer: UnsafeMutableRawPointer +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, cmd, pointer) } +#endif + return fcntl(fd, cmd, pointer) +} + +// ioctl +internal func system_ioctl( + _ fd: Int32, + _ request: CUnsignedLong +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, request) } +#endif + return ioctl(fd, request) +} + +// ioctl +internal func system_ioctl( + _ fd: Int32, + _ request: CUnsignedLong, + _ value: CInt +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, request, value) } +#endif + return ioctl(fd, request, value) +} + +// ioctl +internal func system_ioctl( + _ fd: Int32, + _ request: CUnsignedLong, + _ pointer: UnsafeMutableRawPointer +) -> CInt { +#if ENABLE_MOCKING + if mockingEnabled { return _mock(fd, request, pointer) } +#endif + return ioctl(fd, request, pointer) +} diff --git a/Sources/Socket/System/Util.swift b/Sources/Socket/System/Util.swift new file mode 100644 index 0000000..05fd660 --- /dev/null +++ b/Sources/Socket/System/Util.swift @@ -0,0 +1,232 @@ +// Results in errno if i == -1 +private func valueOrErrno( + _ i: I +) -> Result { + i == -1 ? .failure(Errno.current) : .success(i) +} + +private func nothingOrErrno( + _ i: I +) -> Result<(), Errno> { + valueOrErrno(i).map { _ in () } +} + +internal func valueOrErrno( + retryOnInterrupt: Bool, _ f: () -> I +) -> Result { + repeat { + switch valueOrErrno(f()) { + case .success(let r): return .success(r) + case .failure(let err): + guard retryOnInterrupt && err == .interrupted else { return .failure(err) } + break + } + } while true +} + +internal func nothingOrErrno( + retryOnInterrupt: Bool, _ f: () -> I +) -> Result<(), Errno> { + valueOrErrno(retryOnInterrupt: retryOnInterrupt, f).map { _ in () } +} + +// Run a precondition for debug client builds +internal func _debugPrecondition( + _ condition: @autoclosure () -> Bool, + _ message: StaticString = StaticString(), + file: StaticString = #file, line: UInt = #line +) { + // Only check in debug mode. + if _slowPath(_isDebugAssertConfiguration()) { + precondition( + condition(), String(describing: message), file: file, line: line) + } +} + +extension OpaquePointer { + internal var _isNULL: Bool { + OpaquePointer(bitPattern: Int(bitPattern: self)) == nil + } +} + +extension Sequence { + // Tries to recast contiguous pointer if available, otherwise allocates memory. + internal func _withRawBufferPointer( + _ body: (UnsafeRawBufferPointer) throws -> R + ) rethrows -> R { + guard let result = try self.withContiguousStorageIfAvailable({ + try body(UnsafeRawBufferPointer($0)) + }) else { + return try Array(self).withUnsafeBytes(body) + } + return result + } +} + +extension MutableCollection { + // Tries to recast contiguous pointer if available, otherwise allocates memory. + internal mutating func _withMutableRawBufferPointer( + _ body: (UnsafeMutableRawBufferPointer) throws -> R + ) rethrows -> R { + guard let result = try self.withContiguousMutableStorageIfAvailable({ + try body(UnsafeMutableRawBufferPointer($0)) + }) else { + fatalError() + } + return result + } +} + +extension OptionSet { + // Helper method for building up a comma-separated list of options + // + // Taking an array of descriptions reduces code size vs + // a series of calls due to avoiding register copies. Make sure + // to pass an array literal and not an array built up from a series of + // append calls, else that will massively bloat code size. This takes + // StaticStrings because otherwise we get a warning about getting evicted + // from the shared cache. + @inline(never) + internal func _buildDescription( + _ descriptions: [(Element, StaticString)] + ) -> String { + var copy = self + var result = "[" + + for (option, name) in descriptions { + if _slowPath(copy.contains(option)) { + result += name.description + copy.remove(option) + if !copy.isEmpty { result += ", " } + } + } + + if _slowPath(!copy.isEmpty) { + result += "\(Self.self)(rawValue: \(copy.rawValue))" + } + result += "]" + return result + } +} + +internal extension Sequence { + + func _buildDescription() -> String { + var string = "[" + for element in self { + if _slowPath(string.count == 1) { + string += "\(element)" + } else { + string += ", \(element)" + } + } + string += "]" + return string + } +} + +internal func _dropCommonPrefix( + _ lhs: C, _ rhs: C +) -> (C.SubSequence, C.SubSequence) +where C.Element: Equatable { + var (lhs, rhs) = (lhs[...], rhs[...]) + while lhs.first != nil && lhs.first == rhs.first { + lhs.removeFirst() + rhs.removeFirst() + } + return (lhs, rhs) +} + +extension MutableCollection where Element: Equatable { + mutating func _replaceAll(_ e: Element, with new: Element) { + for idx in self.indices { + if self[idx] == e { self[idx] = new } + } + } +} + +internal extension Bool { + + @usableFromInline + init(_ cInt: CInt) { + self = cInt != 0 + } + + @usableFromInline + var cInt: CInt { + self ? 1 : 0 + } +} + +/// Pauses the current task if the operation throws ``Errno/wouldBlock`` or other async I/O errors. +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@usableFromInline +func retry( + sleep nanoseconds: UInt64, + _ body: () -> Result +) async throws -> Result { + repeat { + try Task.checkCancellation() + switch body() { + case let .success(result): + return .success(result) + case let .failure(error): + guard error.isBlocking else { + return .failure(error) + } + try await Task.sleep(nanoseconds: nanoseconds) + } + } while true +} + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +@usableFromInline +func retry( + sleep: UInt64, // ns + timeout: UInt, // ms + condition: (T) -> (Bool) = { _ in return true }, + _ body: () -> Result +) async throws -> T { + assert(timeout > 0, "\(#function) Must specify a timeout") + // convert ms to ns + var timeRemaining = UInt64(timeout) * 1_000_000 + repeat { + // immediately get poll results + switch body() { + case let .success(events): + // sleep if no events + guard condition(events) == false else { + return events + } + case let .failure(error): + // sleep if blocking error is thrown + guard error.isBlocking else { + throw error + } + } + // check for cancellation + try Task.checkCancellation() + // check if we have time remaining + guard timeRemaining > sleep else { + throw Errno.timedOut + } + // check clock? + timeRemaining -= sleep + try await Task.sleep(nanoseconds: sleep) // checks for cancelation + } while true +} + +internal extension Errno { + + var isBlocking: Bool { + switch self { + case .wouldBlock, + .nowInProgress, + .alreadyInProcess, + .resourceTemporarilyUnavailable: + return true + default: + return false + } + } +} diff --git a/Tests/SocketTests/SocketTests.swift b/Tests/SocketTests/SocketTests.swift index 2902642..6bdedd2 100644 --- a/Tests/SocketTests/SocketTests.swift +++ b/Tests/SocketTests/SocketTests.swift @@ -8,12 +8,14 @@ final class SocketTests: XCTestCase { func _testUnixSocket() async throws { let address = UnixSocketAddress(path: "/tmp/socket1") let socketA = try await Socket( - fileDescriptor: .socket(UnixProtocol.raw, bind: address) + UnixProtocol.raw, + bind: address ) defer { Task { await socketA.close() } } let socketB = try await Socket( - fileDescriptor: .socket(UnixProtocol.raw, bind: address) + UnixProtocol.raw, + bind: address ) defer { Task { await socketB.close() } } @@ -24,12 +26,13 @@ final class SocketTests: XCTestCase { XCTAssertEqual(data, read) } - func testIPv4Socket() async throws { + func _testIPv4Socket() async throws { let address = IPv4SocketAddress(address: .any, port: 8888) let data = Data("Test \(UUID())".utf8) let server = try await Socket( - fileDescriptor: .socket(IPv4Protocol.tcp, bind: address) + IPv4Protocol.tcp, + bind: address ) defer { Task { await server.close() } } NSLog("Server: Created server socket \(server.fileDescriptor)") @@ -52,7 +55,7 @@ final class SocketTests: XCTestCase { } let client = try await Socket( - fileDescriptor: .socket(IPv4Protocol.tcp) + IPv4Protocol.tcp ) defer { Task { await client.close() } } NSLog("Client: Created client socket \(client.fileDescriptor)")