import Foundation import CQuicProQuo /// High-level quicproquo client for iOS/macOS. /// /// Wraps ``libquicproquo_ffi`` to provide a Swift-native API for /// connecting, authenticating, and messaging. /// /// ```swift /// let client = try QpqClient( /// server: "127.0.0.1:5001", /// caCertPath: "/path/to/ca.pem" /// ) /// try client.login(username: "alice", password: "secret") /// try client.send(to: "bob", message: "hello".data(using: .utf8)!) /// let messages = try client.receive(timeoutMs: 5000) /// client.disconnect() /// ``` public final class QpqClient: @unchecked Sendable { private var handle: OpaquePointer? private let lock = NSLock() /// Connect to a quicproquo server. /// /// - Parameters: /// - server: Server address as ``host:port``. /// - caCertPath: Path to the PEM-encoded CA certificate. /// - serverName: TLS SNI server name (defaults to host from *server*). /// - Throws: ``QpqError/connectionFailed(_:)`` if the connection fails. public init(server: String, caCertPath: String, serverName: String? = nil) throws { let sn = serverName ?? server.components(separatedBy: ":").first ?? server let h = qpq_connect(server, caCertPath, sn) guard let h else { throw QpqError.connectionFailed("qpq_connect returned NULL for \(server)") } self.handle = h } deinit { disconnect() } // MARK: - Authentication /// Authenticate with the server using OPAQUE (username + password). /// /// - Throws: ``QpqError/authFailed(_:)`` on bad credentials. public func login(username: String, password: String) throws { try withHandle { h in let code = qpq_login(h, username, password) try checkStatus(code, handle: h) } } // MARK: - Messaging /// Send a message to a recipient by username. /// /// The message is encrypted via MLS before delivery. /// /// - Parameters: /// - recipient: Recipient username. /// - message: Message payload (arbitrary bytes). public func send(to recipient: String, message: Data) throws { try withHandle { h in let code = message.withUnsafeBytes { buffer -> Int32 in guard let ptr = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return QPQ_ERROR } return qpq_send(h, recipient, ptr, buffer.count) } try checkStatus(code, handle: h) } } /// Receive pending messages, blocking up to ``timeoutMs`` milliseconds. /// /// - Returns: An array of message strings (UTF-8). public func receive(timeoutMs: UInt32 = 5000) throws -> [String] { try withHandle { h in var jsonPtr: UnsafeMutablePointer? let code = qpq_receive(h, timeoutMs, &jsonPtr) try checkStatus(code, handle: h) guard let jsonPtr else { return [] } defer { qpq_free_string(jsonPtr) } let jsonString = String(cString: jsonPtr) guard let data = jsonString.data(using: .utf8), let array = try? JSONSerialization.jsonObject(with: data) as? [String] else { return [] } return array } } // MARK: - Lifecycle /// Disconnect from the server and release resources. public func disconnect() { lock.lock() defer { lock.unlock() } if let h = handle { qpq_disconnect(h) handle = nil } } /// Whether the client is currently connected. public var isConnected: Bool { lock.lock() defer { lock.unlock() } return handle != nil } // MARK: - Internal private func withHandle(_ body: (OpaquePointer) throws -> T) throws -> T { lock.lock() defer { lock.unlock() } guard let h = handle else { throw QpqError.notConnected } return try body(h) } private func checkStatus(_ code: Int32, handle: OpaquePointer) throws { guard code != QPQ_OK else { return } let msg: String if let errPtr = qpq_last_error(handle) { msg = String(cString: errPtr) } else { msg = "unknown error" } switch code { case QPQ_AUTH_FAILED: throw QpqError.authFailed(msg) case QPQ_TIMEOUT: throw QpqError.timeout(msg) case QPQ_NOT_CONNECTED: throw QpqError.notConnected default: throw QpqError.ffiError(msg) } } }