Files
element-x-ios/ElementX/Sources/Other/Extensions/URL.swift
T
Mauro a520d94760 Swift 6 Migration (#5733)
* Enable Swift 6.2 with default main actor isolation for the app target

Turn on SWIFT_VERSION 6.2, SWIFT_APPROACHABLE_CONCURRENCY and SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor for the ElementX target, matching the NSE and Share Extension (which already use 6.2 without the main actor default).

# Conflicts:
#	ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

* Update the code generation templates for Swift 6

Emit nonisolated/@unchecked Sendable mocks and nonisolated string enums, and mark async mock methods @concurrent so they keep suspending like the real implementations (otherwise intermediate states are unobservable in tests). Includes the regenerated mocks and strings.

# Conflicts:
#	ElementX/Sources/Mocks/Generated/GeneratedMocks.swift

* Make the data layer and services nonisolated and Sendable

Annotate the value-type models, Rust SDK proxy wrappers, timeline item graph, settings, logging and NSE-shared types as nonisolated, and conform the service protocols to Sendable so their existentials can cross isolation boundaries. These types run off the main actor and no longer inherit the target's default main actor isolation.

* Adopt the new isolation across call sites, mocks and signatures

Update call sites, closure signatures and mock configuration helpers to match the nonisolated/Sendable data layer, including the main actor isolated mock inits and the @Sendable filter on recentlyVisitedRooms.

^ Conflicts:
^	ElementX/Sources/Services/SecureBackup/SecureBackupController.swift

* Remove redundant @MainActor annotations

With the app target defaulting to main actor isolation, the explicit @MainActor on view models, coordinators, views and their protocols is no longer required.

* Fix concurrency hotspots surfaced by the migration

Resolve the room summary main-thread deadlock by moving diff processing onto AsyncStreams, add the SDKListener.onMainActor FIFO bridge for SDK callbacks, hop KVO/timer/UIKit callbacks to the main actor via assumeIsolated, add isolated deinits, and apply the few justified nonisolated(unsafe) escapes for queue-confined Apple APIs.

* Fix the unit tests for Swift 6 concurrency

Hop the waitForConfirmation body onto the caller's actor (Swift Testing runs confirmation closures off-actor), and adjust the affected suites and async signatures.

* offload diffs on concurrent threads instead of main actor

* make room info protocols sendable

* use a task group for fetching data so that everything runs concurrently in the TextRoomTimelineView when getting the preview.

* use the generic onMainActor implementation, which already makes a stream and send the continuation on the main actor.

* small pr comments issues being addressed

* update enterprise

* Update StaticCode to 1.1.0 and specify the required Swift isolation.

* fix ElementCallServiceTests

* update enterprise

* re-generate mocks

* update enterprise

* enterprise new code conformed to swift 6

---------

Co-authored-by: Doug <douglase@element.io>
2026-06-18 17:55:31 +02:00

231 lines
8.5 KiB
Swift

//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Foundation
// MARK: - Custom URLs
nonisolated extension URL {
/// The URL of the primary app group container.
static var appGroupContainerDirectory: URL {
guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: InfoPlistReader.main.appGroupIdentifier) else {
MXLog.error("Application Group unavailable, falling back to the application folder")
// Browserstack doesn't properly handle AppGroup entitlements so this fails, presumably because of the resigning happening on their side
// Try using the normal app folder instead of the app group
// https://www.browserstack.com/docs/app-automate/appium/troubleshooting/entitlements-error
return URL.applicationSupportDirectory.deletingLastPathComponent().deletingLastPathComponent()
}
return url
}
static var appGroupLogsDirectory: URL {
appGroupContainerDirectory
.appending(component: "Library", directoryHint: .isDirectory)
.appending(component: "Logs", directoryHint: .isDirectory)
.appending(component: InfoPlistReader.main.baseBundleIdentifier, directoryHint: .isDirectory)
}
/// The base directory where all session data is stored.
static var sessionsBaseDirectory: URL {
let applicationSupportSessionsURL = applicationSupportBaseDirectory.appending(component: "Sessions", directoryHint: .isDirectory)
try? FileManager.default.createDirectoryIfNeeded(at: applicationSupportSessionsURL)
return applicationSupportSessionsURL
}
/// The base directory where all application support data is stored.
static var applicationSupportBaseDirectory: URL {
var url = appGroupContainerDirectory
.appending(component: "Library", directoryHint: .isDirectory)
.appending(component: "Application Support", directoryHint: .isDirectory)
.appending(component: InfoPlistReader.main.baseBundleIdentifier, directoryHint: .isDirectory)
try? FileManager.default.createDirectoryIfNeeded(at: url)
do {
var resourceValues = URLResourceValues()
resourceValues.isExcludedFromBackup = true
try url.setResourceValues(resourceValues)
} catch {
MXLog.error("Failed excluding Application Support from backups")
}
return url
}
/// The base directory where all application support data is stored.
static var sessionCachesBaseDirectory: URL {
let url = appGroupContainerDirectory
.appending(component: "Library", directoryHint: .isDirectory)
.appending(component: "Caches", directoryHint: .isDirectory)
.appending(component: InfoPlistReader.main.baseBundleIdentifier, directoryHint: .isDirectory)
.appending(component: "Sessions", directoryHint: .isDirectory)
try? FileManager.default.createDirectoryIfNeeded(at: url)
// Caches are excluded from backups automatically.
// https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html
return url
}
/// The app group temporary directory (useful for transferring files between different bundles).
///
/// **Note:** This `tmp` directory doesn't appear to behave as expected as it isn't being tidied up by the system.
/// Make sure to manually tidy up any files you place in here once you've transferred them from one bundle to another.
static var appGroupTemporaryDirectory: URL {
let url = appGroupContainerDirectory
.appending(component: "tmp", directoryHint: .isDirectory)
try? FileManager.default.createDirectoryIfNeeded(at: url)
// Temporary files are excluded from backups automatically.
// https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html
return url
}
var globalProxy: String? {
let span = MXLog.createSpan("Global proxy")
span.enter()
defer {
span.exit()
}
guard let proxySettings = CFNetworkCopySystemProxySettings()?.takeRetainedValue() else {
MXLog.error("Failed retrieving proxy settings")
return nil
}
let proxies = CFNetworkCopyProxiesForURL(self as CFURL, proxySettings).takeRetainedValue() as? [[CFString: Any]]
guard let firstProxy = proxies?.first,
let proxyType = firstProxy[kCFProxyTypeKey] as? String,
proxyType != kCFProxyTypeNone as String else {
MXLog.info("No global proxy configured.")
return nil
}
MXLog.info("Found \(String(describing: proxies?.count)) proxies, using the first one.")
MXLog.info("Proxy type is \(proxyType)")
guard let host = firstProxy[kCFProxyHostNameKey] as? String else {
MXLog.error("Found proxy with invalid host name")
return nil
}
let port = firstProxy[kCFProxyPortNumberKey] as? Int
MXLog.info("Found proxy host: \(host), port: \(String(describing: port))")
if let port {
return "\(host):\(port)"
} else {
return host
}
}
// MARK: Mocks
nonisolated static var mockMXCAudio: URL {
"mxc://matrix.org/1234567890AuDiO"
}
nonisolated static var mockMXCFile: URL {
"mxc://matrix.org/1234567890FiLe"
}
nonisolated static var mockMXCImage: URL {
"mxc://matrix.org/1234567890ImAgE"
}
nonisolated static var mockMXCVideo: URL {
"mxc://matrix.org/1234567890ViDeO"
}
nonisolated static var mockMXCAvatar: URL {
"mxc://matrix.org/1234567890AvAtAr"
}
nonisolated static var mockMXCUserAvatar: URL {
"mxc://matrix.org/1234567890AvAtArUsEr"
}
}
// MARK: - Helpers
nonisolated extension URL: @retroactive ExpressibleByStringLiteral {
public init(stringLiteral value: StaticString) {
guard let url = URL(string: "\(value)") else {
fatalError("The static string used to create this URL is invalid")
}
self = url
}
/// Sanitises the URL for use as the name of a directory.
func asDirectoryName() -> String {
absoluteString.asURLDirectoryName()
}
}
nonisolated extension String {
/// Assumes that the string is a URL and sanitises it for use as the name of a directory.
func asURLDirectoryName() -> String {
replacingOccurrences(of: "https://", with: "")
.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
.replacing(/[:\/\p{C}]/, with: "-")
}
}
// MARK: - Phishing Confirmation URL
nonisolated extension URL {
static let confirmationScheme = "confirm"
var requiresConfirmation: Bool {
scheme == Self.confirmationScheme
}
var confirmationParameters: ConfirmURLParameters? {
guard requiresConfirmation,
let queryItems = URLComponents(url: self, resolvingAgainstBaseURL: true)?.queryItems else {
return nil
}
return ConfirmURLParameters(queryItems: queryItems)
}
}
nonisolated struct ConfirmURLParameters {
static let internalURLKey = "internalURL"
static let displayStringKey = "displayString"
let internalURL: URL
let displayString: String
var urlQueryItems: [URLQueryItem] {
[URLQueryItem(name: Self.internalURLKey, value: internalURL.absoluteString),
URLQueryItem(name: Self.displayStringKey, value: displayString)]
}
}
nonisolated extension ConfirmURLParameters {
init?(queryItems: [URLQueryItem]) {
guard let internalURLString = queryItems.first(where: { $0.name == Self.internalURLKey })?.value,
let internalURL = URL(string: internalURLString),
let externalURLString = queryItems.first(where: { $0.name == Self.displayStringKey })?.value else {
return nil
}
displayString = externalURLString
self.internalURL = internalURL
}
}