Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .claude/rules/ai-mcp-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
paths:
- "TablePro/Core/AI/**/*"
- "TablePro/Core/MCP/**/*"
- "Packages/TableProMacCore/Sources/TableProMCP/**/*"
---

# AI and MCP
Expand Down
15 changes: 12 additions & 3 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,15 @@ custom_rules:
excluded: ".*/(AppStorageEnvironment|GeneralSettings|SoftwareUpdater|WorkspaceRailViewController|SplitViewAutosaveSweep)\\.swift"
message: "Resolve preferences through AppStorageEnvironment.shared.defaults. Only a macOS-owned system preference reads the standard domain."
severity: error
# Package code cannot reach AppStorageEnvironment and the iOS app has no sandbox, so `.standard` is
# their only default. ManagedPolicy reads MDM-managed keys, which live in the standard domain.
storage_environment_defaults_argument:
name: "Storage Environment Defaults Argument"
regex: '\bUserDefaults[?!]?\s*=\s*\.standard\b'
excluded: ['.*/(Packages|Plugins|TableProMobile)/', '.*/ManagedPolicy\.swift']
excluded_match_kinds: [comment, doccomment]
message: "Default a UserDefaults parameter to AppStorageEnvironment.shared.defaults, or a sandboxed run writes the developer's own com.TablePro."
severity: error
public_error_text_in_log:
name: "Error Text In The System Log"
regex: '(\.(localizedDescription|errorDescription|failureReason)(\.prefix\(\d+\))?(\s*\?\?\s*"[^"]*")?|String\(describing:\s*\w*[eE]rr(or)?\)|\\\((?!(has|is|did|was|should)[A-Z])(\w*[eE]rr(or)?|e)(\.(message|description|debugDescription))?)\s*,\s*privacy:\s*\.public'
Expand Down Expand Up @@ -494,14 +503,14 @@ custom_rules:
# MCP and scripting access control
mcp_log_token_material:
name: "MCP Log Token Material"
included: ['.*/TablePro/Core/MCP/']
included: ['.*/(TablePro/Core/MCP|Packages/TableProMacCore/Sources/TableProMCP)/']
regex: '(?i)(?-s)^(?=[^\n]*privacy:)[^\n]*?(\\\([^,)\n]*(?:fingerprint|plaintext|tokenhash|salt|verifier|prefix|password|secret))'
capture_group: 1
message: "A log line never carries token material, not even a prefix or a digest."
severity: error
mcp_reported_status:
name: "MCP Reported Status"
included: ['.*/TablePro/Core/MCP/']
included: ['.*/(TablePro/Core/MCP|Packages/TableProMacCore/Sources/TableProMCP)/']
regex: '\.status\.isConnected\b'
excluded_match_kinds: [comment, doccomment]
message: "MCP reports connection health through reportedStatus."
Expand Down Expand Up @@ -654,7 +663,7 @@ custom_rules:
# Tests (listed in TableProTests/.swiftlint.yml and Plugins/.swiftlint.yml)
suite_without_trait:
name: "Suite Without A Trait"
included: ['.*/(TableProTests|Plugins/[A-Za-z]+Tests)/']
included: ['.*/(TableProTests|Plugins/[A-Za-z]+Tests|Packages/TableProMacCore/Tests)/']
regex: '(?:^#if\b[^\n]*\n[ \t]*|^)(?:@\w+[ \t]+)*@(?:Testing\.)?Suite(?:[ \t]*\(\s*(?:"(?:[^"\\\n]|\\.)*"|#+"[^\n]*?"#+)?\s*\))?(?![ \t]*\()(?=\s)'
excluded_match_kinds: [comment, doccomment]
message: "Every top-level @Suite expansion walks every other one, so compile time grows with their square. Keep @Suite only to carry a trait such as .serialized."
Expand Down
63 changes: 58 additions & 5 deletions Packages/TableProCore/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

import PackageDescription

// The plugins and the iOS app compile this code under SWIFT_APPROACHABLE_CONCURRENCY and MemberImportVisibility.
let pluginCoreSettings: [SwiftSetting] = [
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.enableUpcomingFeature("InferIsolatedConformances"),
.enableUpcomingFeature("MemberImportVisibility")
]

let package = Package(
name: "TableProCore",
platforms: [
Expand Down Expand Up @@ -39,7 +46,11 @@ let package = Package(
.library(name: "CSQLite", targets: ["CSQLite"]),
.library(name: "TableProSQLiteCore", targets: ["TableProSQLiteCore"]),
.library(name: "TableProTabularIO", targets: ["TableProTabularIO"]),
.library(name: "TableProTabular", targets: ["TableProTabular"])
.library(name: "TableProTabular", targets: ["TableProTabular"]),
.library(name: "TableProMySQLCore", targets: ["TableProMySQLCore"]),
.library(name: "TableProPostgreSQLCore", targets: ["TableProPostgreSQLCore"]),
.library(name: "TableProRedisCore", targets: ["TableProRedisCore"]),
.library(name: "TableProTestSupport", targets: ["TableProTestSupport"])
],
targets: [
.target(
Expand Down Expand Up @@ -137,6 +148,11 @@ let package = Package(
dependencies: [],
path: "Tests/TableProTLSTestFixtures"
),
.target(
name: "TableProTestSupport",
dependencies: [],
path: "Tests/TableProTestSupport"
),
.target(
name: "TableProGoogleCloud",
dependencies: [],
Expand Down Expand Up @@ -197,6 +213,25 @@ let package = Package(
dependencies: ["CSQLite"],
path: "Sources/TableProSQLiteCore"
),
// Linked by a plugin bundle, so never TableProPluginKit: the plugin would carry its own copy of PluginKit's types.
.target(
name: "TableProMySQLCore",
dependencies: ["TableProGeometry"],
path: "Sources/TableProMySQLCore",
swiftSettings: pluginCoreSettings
),
.target(
name: "TableProPostgreSQLCore",
dependencies: [],
path: "Sources/TableProPostgreSQLCore",
swiftSettings: pluginCoreSettings
),
.target(
name: "TableProRedisCore",
dependencies: [],
path: "Sources/TableProRedisCore",
swiftSettings: pluginCoreSettings
),
.testTarget(
name: "TableProTabularIOTests",
dependencies: ["TableProTabularIO", "TableProPluginKit"],
Expand All @@ -214,7 +249,7 @@ let package = Package(
),
.testTarget(
name: "TableProCoreTypesTests",
dependencies: ["TableProCoreTypes"],
dependencies: ["TableProCoreTypes", "TableProTestSupport"],
path: "Tests/TableProCoreTypesTests"
),
.testTarget(
Expand Down Expand Up @@ -254,7 +289,7 @@ let package = Package(
),
.testTarget(
name: "TableProDatabaseTests",
dependencies: ["TableProDatabase", "TableProModels", "TableProPluginKit"],
dependencies: ["TableProDatabase", "TableProModels", "TableProPluginKit", "TableProTestSupport"],
path: "Tests/TableProDatabaseTests"
),
.testTarget(
Expand All @@ -274,7 +309,7 @@ let package = Package(
),
.testTarget(
name: "TableProMSSQLCoreTests",
dependencies: ["TableProMSSQLCore"],
dependencies: ["TableProMSSQLCore", "TableProTestSupport"],
path: "Tests/TableProMSSQLCoreTests"
),
.testTarget(
Expand All @@ -284,7 +319,7 @@ let package = Package(
),
.testTarget(
name: "TableProGoogleCloudTests",
dependencies: ["TableProGoogleCloud"],
dependencies: ["TableProGoogleCloud", "TableProTestSupport"],
path: "Tests/TableProGoogleCloudTests"
),
.testTarget(
Expand All @@ -296,6 +331,24 @@ let package = Package(
name: "TableProPluginKitTests",
dependencies: ["TableProPluginKit"],
path: "Tests/TableProPluginKitTests"
),
.testTarget(
name: "TableProMySQLCoreTests",
dependencies: ["TableProMySQLCore"],
path: "Tests/TableProMySQLCoreTests",
swiftSettings: pluginCoreSettings
),
.testTarget(
name: "TableProPostgreSQLCoreTests",
dependencies: ["TableProPostgreSQLCore"],
path: "Tests/TableProPostgreSQLCoreTests",
swiftSettings: pluginCoreSettings
),
.testTarget(
name: "TableProRedisCoreTests",
dependencies: ["TableProRedisCore"],
path: "Tests/TableProRedisCoreTests",
swiftSettings: pluginCoreSettings
)
]
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public final class ConnectionManager: @unchecked Sendable {
private let secureStore: SecureStore
private let sshProvider: SSHProvider?
private let teardownWaitLimit: Duration
private let clock: any Clock<Duration>

private let lock = NSLock()
private var sessions: [UUID: ConnectionSession] = [:]
Expand All @@ -20,12 +21,14 @@ public final class ConnectionManager: @unchecked Sendable {
driverFactory: DriverFactory,
secureStore: SecureStore,
sshProvider: SSHProvider? = nil,
teardownWaitLimit: Duration = ConnectionManager.defaultTeardownWaitLimit
teardownWaitLimit: Duration = ConnectionManager.defaultTeardownWaitLimit,
clock: any Clock<Duration> = ContinuousClock()
) {
self.driverFactory = driverFactory
self.secureStore = secureStore
self.sshProvider = sshProvider
self.teardownWaitLimit = teardownWaitLimit
self.clock = clock
}

/// Opens a session, waiting out a teardown still running for the same connection first.
Expand Down Expand Up @@ -152,18 +155,22 @@ public final class ConnectionManager: @unchecked Sendable {
case .untilFinished:
return await teardown.waitUntilFinished() ? .cleared : .cancelled
case .after(let limit):
return await Self.wait(on: teardown, within: limit)
return await Self.wait(on: teardown, within: limit, clock: clock)
}
}

private static func wait(on teardown: ConnectionTeardown, within limit: Duration) async -> TeardownWait {
private static func wait(
on teardown: ConnectionTeardown,
within limit: Duration,
clock: any Clock<Duration>
) async -> TeardownWait {
await withTaskGroup(of: TeardownWait.self) { group in
group.addTask {
await teardown.waitUntilFinished() ? .cleared : .cancelled
}
group.addTask {
do {
try await Task.sleep(for: limit)
try await clock.sleep(for: limit)
} catch {
return .cancelled
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
//
// GeometryWKBParser.swift
// MySQLDriverPlugin
//
// Renders MySQL's internal geometry binary as EWKT.
//

import Foundation
import TableProGeometry

Expand All @@ -23,8 +16,8 @@ import TableProGeometry
/// MariaDB 12.3.3, these bytes are longitude-first even for SRID 4326, whose `ST_AsText` prints
/// latitude first. Reordering them to match `ST_AsText` would move every MySQL point to the wrong
/// hemisphere.
nonisolated enum GeometryWKBParser {
static func parse(_ data: Data) -> String {
nonisolated public enum GeometryWKBParser {
public static func parse(_ data: Data) -> String {
guard data.count >= 9 else { return hexString(data) }
switch WKBGeometryReader.read(mysqlInternal: Array(data)) {
case .success(let value):
Expand All @@ -34,7 +27,7 @@ nonisolated enum GeometryWKBParser {
}
}

static func parse(_ buffer: UnsafeRawBufferPointer) -> String {
public static func parse(_ buffer: UnsafeRawBufferPointer) -> String {
parse(Data(buffer))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
//
// MariaDBFieldClassifier.swift
// MySQLDriverPlugin
//

import Foundation

nonisolated internal enum MariaDBFieldClassifier {
nonisolated public enum MariaDBFieldClassifier {
private static let bitType: UInt32 = 16
private static let binaryCharset: UInt32 = 63
private static let blobOrStringTypes: Set<UInt32> = [249, 250, 251, 252, 253, 254]

static func isBinary(typeRaw: UInt32, charset: UInt32) -> Bool {
public static func isBinary(typeRaw: UInt32, charset: UInt32) -> Bool {
guard charset == binaryCharset else {
return false
}
return blobOrStringTypes.contains(typeRaw)
}

static func isBit(typeRaw: UInt32) -> Bool {
public static func isBit(typeRaw: UInt32) -> Bool {
typeRaw == bitType
}

static func bitFieldToString(_ buffer: UnsafeRawBufferPointer) -> String {
public static func bitFieldToString(_ buffer: UnsafeRawBufferPointer) -> String {
guard !buffer.isEmpty else { return "0" }
var value: UInt64 = 0
for byte in buffer {
Expand All @@ -30,7 +25,7 @@ nonisolated internal enum MariaDBFieldClassifier {
return String(value)
}

static func bitFieldToString(_ data: Data) -> String {
public static func bitFieldToString(_ data: Data) -> String {
data.withUnsafeBytes { bitFieldToString($0) }
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
//
// MySQLBlockingPasswordRefresh.swift
// MySQLDriverPlugin
//

import Foundation
import os

/// Waits on a dispatch queue for the host to mint a password. Never call it from a Swift
/// concurrency thread: the refresh runs on one, and the host's refresh never needs the main actor.
internal enum MySQLBlockingPasswordRefresh {
static func wait(
public enum MySQLBlockingPasswordRefresh {
public static func wait(
for refresh: @escaping @Sendable () async throws -> String,
timeout: DispatchTimeInterval
) -> String? {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
//
// MySQLCharacterSet.swift
// MySQLDriverPlugin
//

import CoreFoundation
import Foundation

nonisolated internal struct MySQLCharacterSet: Hashable, Sendable {
static let utf8mb4 = MySQLCharacterSet(serverName: "utf8mb4")
static let latin1 = MySQLCharacterSet(serverName: "latin1")
nonisolated public struct MySQLCharacterSet: Hashable, Sendable {
public static let utf8mb4 = MySQLCharacterSet(serverName: "utf8mb4")
public static let latin1 = MySQLCharacterSet(serverName: "latin1")

let name: String
public let name: String
private let decoding: Decoding

init(serverName: String) {
public init(serverName: String) {
let normalized = serverName.trimmingCharacters(in: .whitespaces).lowercased()
name = normalized == "utf8" ? "utf8mb3" : normalized
decoding = Self.decoding(forName: name)
}

func decode(_ bytes: UnsafeRawBufferPointer) -> String {
public func decode(_ bytes: UnsafeRawBufferPointer) -> String {
switch decoding {
case .utf8:
return Self.decodeUTF8ReplacingInvalid(bytes)
Expand All @@ -30,14 +25,14 @@ nonisolated internal struct MySQLCharacterSet: Hashable, Sendable {
}
}

static func decodeUTF8OrMySQLLatin1(_ bytes: UnsafeRawBufferPointer) -> String {
public static func decodeUTF8OrMySQLLatin1(_ bytes: UnsafeRawBufferPointer) -> String {
if let text = String(bytes: bytes, encoding: .utf8) {
return text
}
return MySQLLatin1.decode(bytes)
}

static func decodeUTF8ReplacingInvalid(_ bytes: UnsafeRawBufferPointer) -> String {
public static func decodeUTF8ReplacingInvalid(_ bytes: UnsafeRawBufferPointer) -> String {
String(decoding: bytes, as: UTF8.self) // swiftlint:disable:this optional_data_string_conversion
}

Expand Down
Loading
Loading