From 773708a3221592d8744c1a23d0b23eeeb68613aa Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Wed, 20 May 2026 22:58:16 +0700 Subject: [PATCH] fix(plugin-mssql): keep FreeTDSConnection PluginKit-neutral so iOS builds --- .../TableProMSSQLCore/MSSQLCoreError.swift | 15 +++++++++++--- .../MSSQLDriverPlugin/FreeTDSConnection.swift | 17 ++++++++-------- Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift | 20 +++++++++++++++++-- .../TableProMobile/Drivers/MSSQLDriver.swift | 4 ++-- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/Packages/TableProCore/Sources/TableProMSSQLCore/MSSQLCoreError.swift b/Packages/TableProCore/Sources/TableProMSSQLCore/MSSQLCoreError.swift index 8343aa526..a8970608a 100644 --- a/Packages/TableProCore/Sources/TableProMSSQLCore/MSSQLCoreError.swift +++ b/Packages/TableProCore/Sources/TableProMSSQLCore/MSSQLCoreError.swift @@ -1,11 +1,20 @@ import Foundation +public enum MSSQLTLSFailureKind: Sendable { + case serverRejectedPlaintext + case serverRequiresPlaintext + case untrustedCertificate + case hostnameMismatch + case clientCertRequired + case cipherMismatch +} + public enum MSSQLCoreError: LocalizedError, Sendable { case connectionFailed(String) case notConnected case queryFailed(String) case cancelled - case tlsHandshakeFailed(String) + case tlsHandshakeFailed(kind: MSSQLTLSFailureKind, serverMessage: String) public var errorDescription: String? { switch self { @@ -17,8 +26,8 @@ public enum MSSQLCoreError: LocalizedError, Sendable { return String(format: String(localized: "Query failed: %@"), detail) case .cancelled: return String(localized: "Query was cancelled") - case .tlsHandshakeFailed(let detail): - return String(format: String(localized: "TLS handshake failed: %@"), detail) + case .tlsHandshakeFailed(_, let serverMessage): + return String(format: String(localized: "TLS handshake failed: %@"), serverMessage) } } } diff --git a/Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift b/Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift index d577cfcbf..d989f80fe 100644 --- a/Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift +++ b/Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift @@ -13,7 +13,6 @@ import CFreeTDS import Foundation import os import TableProMSSQLCore -import TableProPluginKit private let freetdsLogger = Logger(subsystem: "com.TablePro", category: "FreeTDSConnection") @@ -167,8 +166,8 @@ nonisolated final class FreeTDSConnection: @unchecked Sendable { guard let proc = dbopen(login, serverName) else { let detail = freetdsGetError(for: nil) let msg = detail.isEmpty ? "Check host, port, credentials, and TLS settings" : detail - if let sslError = FreeTDSConnection.classifySSLError(detail) { - throw sslError + if let kind = FreeTDSConnection.classifySSLError(detail) { + throw MSSQLCoreError.tlsHandshakeFailed(kind: kind, serverMessage: detail) } throw MSSQLCoreError.connectionFailed("Failed to connect to \(options.host):\(options.port): \(msg)") } @@ -532,22 +531,22 @@ nonisolated final class FreeTDSConnection: @unchecked Sendable { return raw } - static func classifySSLError(_ message: String) -> SSLHandshakeError? { + static func classifySSLError(_ message: String) -> MSSQLTLSFailureKind? { let lower = message.lowercased() if lower.contains("encryption is required") || lower.contains("server requires encryption") { - return .serverRejectedPlaintext(serverMessage: message) + return .serverRejectedPlaintext } if lower.contains("encryption not supported") || lower.contains("server does not support encryption") { - return .serverRequiresPlaintext(serverMessage: message) + return .serverRequiresPlaintext } if lower.contains("certificate verify failed") || lower.contains("certificate is not trusted") { - return .untrustedCertificate(serverMessage: message) + return .untrustedCertificate } if lower.contains("does not match host") { - return .hostnameMismatch(serverMessage: message) + return .hostnameMismatch } if lower.contains("ssl handshake") || lower.contains("tls handshake") || lower.contains("openssl error") { - return .cipherMismatch(serverMessage: message) + return .cipherMismatch } return nil } diff --git a/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift b/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift index 96583bd98..16b33de1b 100644 --- a/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift +++ b/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift @@ -44,8 +44,21 @@ private extension MSSQLPluginError { self = .queryFailed(m) case .cancelled: self = .queryFailed(String(localized: "Query was cancelled")) - case .tlsHandshakeFailed(let m): - self = .connectionFailed(String(format: String(localized: "TLS: %@"), m)) + case .tlsHandshakeFailed(_, let serverMessage): + self = .connectionFailed(String(format: String(localized: "TLS: %@"), serverMessage)) + } + } +} + +private extension MSSQLTLSFailureKind { + func sslHandshakeError(serverMessage: String) -> SSLHandshakeError { + switch self { + case .serverRejectedPlaintext: return .serverRejectedPlaintext(serverMessage: serverMessage) + case .serverRequiresPlaintext: return .serverRequiresPlaintext(serverMessage: serverMessage) + case .untrustedCertificate: return .untrustedCertificate(serverMessage: serverMessage) + case .hostnameMismatch: return .hostnameMismatch(serverMessage: serverMessage) + case .clientCertRequired: return .clientCertRequired(serverMessage: serverMessage) + case .cipherMismatch: return .cipherMismatch(serverMessage: serverMessage) } } } @@ -229,6 +242,9 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable { do { try await conn.connect() } catch let error as MSSQLCoreError { + if case let .tlsHandshakeFailed(kind, serverMessage) = error { + throw kind.sslHandshakeError(serverMessage: serverMessage) + } throw MSSQLPluginError(coreError: error) } self.freeTDSConn = conn diff --git a/TableProMobile/TableProMobile/Drivers/MSSQLDriver.swift b/TableProMobile/TableProMobile/Drivers/MSSQLDriver.swift index 36a1d0413..b0435ab8b 100644 --- a/TableProMobile/TableProMobile/Drivers/MSSQLDriver.swift +++ b/TableProMobile/TableProMobile/Drivers/MSSQLDriver.swift @@ -275,8 +275,8 @@ final class MSSQLDriver: DatabaseDriver, @unchecked Sendable { return ConnectionError.notConnected case .connectionFailed(let msg): return DatabaseError(message: msg) - case .tlsHandshakeFailed(let msg): - return DatabaseError(message: "TLS handshake failed: \(msg)") + case .tlsHandshakeFailed(_, let serverMessage): + return DatabaseError(message: "TLS handshake failed: \(serverMessage)") case .queryFailed(let msg): return DatabaseError(message: msg) case .cancelled: