Move processor implementations to concrete objects (#983)

Formerly via blocks, now with final classes.

App:

- ProfileProcessor
- AppTunnelProcessor
- Implemented by DefaultAppProcessor in app
- Implemented by MockAppProcessor in UILibrary (for previews)

Tunnel:

- PacketTunnelProcessor
- Implemented by DefaultTunnelProcessor
This commit is contained in:
Davide 2024-12-08 16:24:23 +01:00 committed by GitHub
parent a4ebea1f95
commit aac04c4008
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 193 additions and 174 deletions

View File

@ -28,12 +28,10 @@ import CoreData
import Foundation import Foundation
extension AppData { extension AppData {
public static var cdPreferencesModel: NSManagedObjectModel {
@MainActor
public static let cdPreferencesModel: NSManagedObjectModel = {
guard let model: NSManagedObjectModel = .mergedModel(from: [.module]) else { guard let model: NSManagedObjectModel = .mergedModel(from: [.module]) else {
fatalError("Unable to build Core Data model (Preferences v3)") fatalError("Unable to build Core Data model (Preferences v3)")
} }
return model return model
}() }
} }

View File

@ -33,7 +33,7 @@ public final class ExtendedTunnel: ObservableObject {
private let environment: TunnelEnvironment private let environment: TunnelEnvironment
private let processor: TunnelProcessor? private let processor: AppTunnelProcessor?
private let interval: TimeInterval private let interval: TimeInterval
@ -56,7 +56,7 @@ public final class ExtendedTunnel: ObservableObject {
public init( public init(
tunnel: Tunnel, tunnel: Tunnel,
environment: TunnelEnvironment, environment: TunnelEnvironment,
processor: TunnelProcessor? = nil, processor: AppTunnelProcessor? = nil,
interval: TimeInterval interval: TimeInterval
) { ) {
self.tunnel = tunnel self.tunnel = tunnel

View File

@ -1,93 +0,0 @@
//
// InAppProcessor.swift
// Passepartout
//
// Created by Davide De Rosa on 10/6/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import Foundation
import PassepartoutKit
public final class InAppProcessor: ObservableObject, Sendable {
private let iapManager: IAPManager
public nonisolated let _title: (Profile) -> String
private nonisolated let _isIncluded: (IAPManager, Profile) -> Bool
private nonisolated let _preview: (Profile) -> ProfilePreview
private nonisolated let _requiredFeatures: (IAPManager, Profile) -> Set<AppFeature>?
private nonisolated let _willRebuild: (IAPManager, Profile.Builder) throws -> Profile.Builder
private nonisolated let _willInstall: (IAPManager, Profile) throws -> Profile
public init(
iapManager: IAPManager,
title: @escaping (Profile) -> String,
isIncluded: @escaping (IAPManager, Profile) -> Bool,
preview: @escaping (Profile) -> ProfilePreview,
requiredFeatures: @escaping (IAPManager, Profile) -> Set<AppFeature>?,
willRebuild: @escaping (IAPManager, Profile.Builder) throws -> Profile.Builder,
willInstall: @escaping (IAPManager, Profile) throws -> Profile
) {
self.iapManager = iapManager
_title = title
_isIncluded = isIncluded
_preview = preview
_requiredFeatures = requiredFeatures
_willRebuild = willRebuild
_willInstall = willInstall
}
}
// MARK: - ProfileProcessor
extension InAppProcessor: ProfileProcessor {
public func title(for profile: Profile) -> String {
_title(profile)
}
public func isIncluded(_ profile: Profile) -> Bool {
_isIncluded(iapManager, profile)
}
public func preview(from profile: Profile) -> ProfilePreview {
_preview(profile)
}
public func requiredFeatures(_ profile: Profile) -> Set<AppFeature>? {
_requiredFeatures(iapManager, profile)
}
public func willRebuild(_ builder: Profile.Builder) throws -> Profile.Builder {
try _willRebuild(iapManager, builder)
}
}
// MARK: - TunnelProcessor
extension InAppProcessor: TunnelProcessor {
public func willInstall(_ profile: Profile) throws -> Profile {
try _willInstall(iapManager, profile)
}
}

View File

@ -1,5 +1,5 @@
// //
// ProfileProcessor.swift // Processors.swift
// Passepartout // Passepartout
// //
// Created by Davide De Rosa on 11/20/24. // Created by Davide De Rosa on 11/20/24.
@ -26,6 +26,7 @@
import Foundation import Foundation
import PassepartoutKit import PassepartoutKit
@MainActor
public protocol ProfileProcessor { public protocol ProfileProcessor {
func isIncluded(_ profile: Profile) -> Bool func isIncluded(_ profile: Profile) -> Bool
@ -35,3 +36,14 @@ public protocol ProfileProcessor {
func willRebuild(_ builder: Profile.Builder) throws -> Profile.Builder func willRebuild(_ builder: Profile.Builder) throws -> Profile.Builder
} }
@MainActor
public protocol AppTunnelProcessor {
func title(for profile: Profile) -> String
func willInstall(_ profile: Profile) throws -> Profile
}
public protocol PacketTunnelProcessor {
nonisolated func willStart(_ profile: Profile) throws -> Profile
}

View File

@ -39,27 +39,7 @@ extension AppContext {
[] []
} }
) )
let processor = InAppProcessor( let processor = MockAppProcessor(iapManager: iapManager)
iapManager: iapManager,
title: {
"Passepartout.Mock: \($0.name)"
},
isIncluded: { _, _ in
true
},
preview: {
$0.localizedPreview
},
requiredFeatures: { _, _ in
nil
},
willRebuild: { _, builder in
builder
},
willInstall: { _, profile in
profile
}
)
let profileManager = { let profileManager = {
let profiles: [Profile] = (0..<20) let profiles: [Profile] = (0..<20)
.reduce(into: []) { list, _ in .reduce(into: []) { list, _ in

View File

@ -0,0 +1,64 @@
//
// MockAppProcessor.swift
// Passepartout
//
// Created by Davide De Rosa on 12/8/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import CommonLibrary
import Foundation
import PassepartoutKit
final class MockAppProcessor {
private let iapManager: IAPManager
init(iapManager: IAPManager) {
self.iapManager = iapManager
}
}
extension MockAppProcessor: ProfileProcessor {
func isIncluded(_ profile: Profile) -> Bool {
true
}
func preview(from profile: Profile) -> ProfilePreview {
profile.localizedPreview
}
func requiredFeatures(_ profile: Profile) -> Set<AppFeature>? {
nil
}
func willRebuild(_ builder: Profile.Builder) throws -> Profile.Builder {
builder
}
}
extension MockAppProcessor: AppTunnelProcessor {
func title(for profile: Profile) -> String {
"Passepartout.Mock: \(profile.name)"
}
func willInstall(_ profile: Profile) throws -> Profile {
profile
}
}

View File

@ -27,7 +27,7 @@ import CommonLibrary
import Foundation import Foundation
import PassepartoutKit import PassepartoutKit
final class MockTunnelProcessor: TunnelProcessor { final class MockTunnelProcessor: AppTunnelProcessor {
var titleCount = 0 var titleCount = 0
var willInstallCount = 0 var willInstallCount = 0

View File

@ -33,6 +33,8 @@
0E916B7C2CF811EB0072921A /* XCUIElement+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E916B7B2CF811EB0072921A /* XCUIElement+Extensions.swift */; }; 0E916B7C2CF811EB0072921A /* XCUIElement+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E916B7B2CF811EB0072921A /* XCUIElement+Extensions.swift */; };
0E94EE582B93554B00588243 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7E3D672B9345FD002BBDB4 /* PacketTunnelProvider.swift */; }; 0E94EE582B93554B00588243 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7E3D672B9345FD002BBDB4 /* PacketTunnelProvider.swift */; };
0EAD6A1B2CF7F79A00CC1F02 /* ScreenshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EAD6A1A2CF7F79A00CC1F02 /* ScreenshotTests.swift */; }; 0EAD6A1B2CF7F79A00CC1F02 /* ScreenshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EAD6A1A2CF7F79A00CC1F02 /* ScreenshotTests.swift */; };
0EAEC8A92D05DB8D001AA50C /* DefaultAppProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EAEC8A62D05DB8D001AA50C /* DefaultAppProcessor.swift */; };
0EAEC8AA2D05DB8D001AA50C /* DefaultTunnelProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EAEC8A72D05DB8D001AA50C /* DefaultTunnelProcessor.swift */; };
0EB08B982CA46F4900A02591 /* AppPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0EB08B962CA46F4900A02591 /* AppPlist.strings */; }; 0EB08B982CA46F4900A02591 /* AppPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0EB08B962CA46F4900A02591 /* AppPlist.strings */; };
0EBE80DC2BF55C0E00E36A20 /* TunnelLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 0EBE80DB2BF55C0E00E36A20 /* TunnelLibrary */; }; 0EBE80DC2BF55C0E00E36A20 /* TunnelLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 0EBE80DB2BF55C0E00E36A20 /* TunnelLibrary */; };
0EC066D12C7DC47600D88A94 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0EC066D02C7DC47600D88A94 /* LaunchScreen.storyboard */; platformFilter = ios; }; 0EC066D12C7DC47600D88A94 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0EC066D02C7DC47600D88A94 /* LaunchScreen.storyboard */; platformFilter = ios; };
@ -161,6 +163,8 @@
0E916B7B2CF811EB0072921A /* XCUIElement+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIElement+Extensions.swift"; sourceTree = "<group>"; }; 0E916B7B2CF811EB0072921A /* XCUIElement+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIElement+Extensions.swift"; sourceTree = "<group>"; };
0E94EE5C2B93570600588243 /* Tunnel.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Tunnel.plist; sourceTree = "<group>"; }; 0E94EE5C2B93570600588243 /* Tunnel.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Tunnel.plist; sourceTree = "<group>"; };
0EAD6A1A2CF7F79A00CC1F02 /* ScreenshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotTests.swift; sourceTree = "<group>"; }; 0EAD6A1A2CF7F79A00CC1F02 /* ScreenshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotTests.swift; sourceTree = "<group>"; };
0EAEC8A62D05DB8D001AA50C /* DefaultAppProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAppProcessor.swift; sourceTree = "<group>"; };
0EAEC8A72D05DB8D001AA50C /* DefaultTunnelProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultTunnelProcessor.swift; sourceTree = "<group>"; };
0EB08B972CA46F4900A02591 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AppPlist.strings; sourceTree = "<group>"; }; 0EB08B972CA46F4900A02591 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AppPlist.strings; sourceTree = "<group>"; };
0EBE80DD2BF55C9100E36A20 /* Library */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Library; sourceTree = "<group>"; }; 0EBE80DD2BF55C9100E36A20 /* Library */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Library; sourceTree = "<group>"; };
0EC066D02C7DC47600D88A94 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; }; 0EC066D02C7DC47600D88A94 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
@ -341,6 +345,8 @@
children = ( children = (
0E6EEEE62CF8CB090076E2B0 /* Testing */, 0E6EEEE62CF8CB090076E2B0 /* Testing */,
0EC797402B9378E000C093B7 /* AppContext+Shared.swift */, 0EC797402B9378E000C093B7 /* AppContext+Shared.swift */,
0EAEC8A62D05DB8D001AA50C /* DefaultAppProcessor.swift */,
0EAEC8A72D05DB8D001AA50C /* DefaultTunnelProcessor.swift */,
0E8195592CFDA75200CC8FFD /* Dependencies.swift */, 0E8195592CFDA75200CC8FFD /* Dependencies.swift */,
0EC797412B9378E000C093B7 /* Shared.swift */, 0EC797412B9378E000C093B7 /* Shared.swift */,
0E483E822CE6501100584B32 /* Shared+App.swift */, 0E483E822CE6501100584B32 /* Shared+App.swift */,
@ -681,6 +687,7 @@
0E7C3CCD2C9AF44600B72E69 /* AppDelegate.swift in Sources */, 0E7C3CCD2C9AF44600B72E69 /* AppDelegate.swift in Sources */,
0ED61CFA2CD04192008FE259 /* App+iOS.swift in Sources */, 0ED61CFA2CD04192008FE259 /* App+iOS.swift in Sources */,
0E7E3D6B2B9345FD002BBDB4 /* PassepartoutApp.swift in Sources */, 0E7E3D6B2B9345FD002BBDB4 /* PassepartoutApp.swift in Sources */,
0EAEC8A92D05DB8D001AA50C /* DefaultAppProcessor.swift in Sources */,
0EC797422B9378E000C093B7 /* AppContext+Shared.swift in Sources */, 0EC797422B9378E000C093B7 /* AppContext+Shared.swift in Sources */,
0EE8D7E12CD112C200F6600C /* App+tvOS.swift in Sources */, 0EE8D7E12CD112C200F6600C /* App+tvOS.swift in Sources */,
0E81955A2CFDA75200CC8FFD /* Dependencies.swift in Sources */, 0E81955A2CFDA75200CC8FFD /* Dependencies.swift in Sources */,
@ -729,6 +736,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
0E81955B2CFDA7BF00CC8FFD /* Dependencies.swift in Sources */, 0E81955B2CFDA7BF00CC8FFD /* Dependencies.swift in Sources */,
0EAEC8AA2D05DB8D001AA50C /* DefaultTunnelProcessor.swift in Sources */,
0E94EE582B93554B00588243 /* PacketTunnelProvider.swift in Sources */, 0E94EE582B93554B00588243 /* PacketTunnelProvider.swift in Sources */,
0E483E812CE64D6B00584B32 /* Shared+Tunnel.swift in Sources */, 0E483E812CE64D6B00584B32 /* Shared+Tunnel.swift in Sources */,
0EC797442B93790600C093B7 /* Shared.swift in Sources */, 0EC797442B93790600C093B7 /* Shared.swift in Sources */,

View File

@ -39,7 +39,7 @@ import UITesting
extension AppContext { extension AppContext {
static let shared: AppContext = { static let shared: AppContext = {
let iapManager: IAPManager = .sharedForApp let iapManager: IAPManager = .sharedForApp
let processor = InAppProcessor.sharedImplementation(with: iapManager) { let processor = DefaultAppProcessor(iapManager: iapManager) {
$0.localizedPreview $0.localizedPreview
} }

View File

@ -0,0 +1,83 @@
//
// DefaultAppProcessor.swift
// Passepartout
//
// Created by Davide De Rosa on 10/6/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import CommonLibrary
import Foundation
import PassepartoutKit
final class DefaultAppProcessor: Sendable {
private let iapManager: IAPManager
private let preview: @Sendable (Profile) -> ProfilePreview
init(iapManager: IAPManager, preview: @escaping @Sendable (Profile) -> ProfilePreview) {
self.iapManager = iapManager
self.preview = preview
}
}
extension DefaultAppProcessor: ProfileProcessor {
func isIncluded(_ profile: Profile) -> Bool {
Dependencies.ProfileManager.isIncluded(iapManager, profile)
}
func preview(from profile: Profile) -> ProfilePreview {
preview(profile)
}
func requiredFeatures(_ profile: Profile) -> Set<AppFeature>? {
do {
try iapManager.verify(profile)
return nil
} catch AppError.ineligibleProfile(let requiredFeatures) {
return requiredFeatures
} catch {
return nil
}
}
func willRebuild(_ builder: Profile.Builder) throws -> Profile.Builder {
builder
}
}
extension DefaultAppProcessor: AppTunnelProcessor {
func title(for profile: Profile) -> String {
Dependencies.ProfileManager.sharedTitle(profile)
}
func willInstall(_ profile: Profile) throws -> Profile {
try iapManager.verify(profile)
// validate provider modules
do {
_ = try profile.withProviderModules()
return profile
} catch {
pp_log(.app, .error, "Unable to inject provider modules: \(error)")
throw error
}
}
}

View File

@ -1,8 +1,8 @@
// //
// TunnelProcessor.swift // DefaultTunnelProcessor.swift
// Passepartout // Passepartout
// //
// Created by Davide De Rosa on 11/20/24. // Created by Davide De Rosa on 12/8/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved. // Copyright (c) 2024 Davide De Rosa. All rights reserved.
// //
// https://github.com/passepartoutvpn // https://github.com/passepartoutvpn
@ -23,11 +23,20 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>. // along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
// //
import CommonLibrary
import Foundation import Foundation
import PassepartoutKit import PassepartoutKit
public protocol TunnelProcessor { final class DefaultTunnelProcessor: Sendable {
func title(for profile: Profile) -> String private let preferencesManager: PreferencesManager
func willInstall(_ profile: Profile) throws -> Profile init(preferencesManager: PreferencesManager) {
self.preferencesManager = preferencesManager
}
}
extension DefaultTunnelProcessor: PacketTunnelProcessor {
func willStart(_ profile: Profile) throws -> Profile {
profile
}
} }

View File

@ -89,51 +89,7 @@ extension TunnelEnvironment where Self == AppGroupEnvironment {
} }
} }
extension InAppProcessor {
@MainActor
static func sharedImplementation(with iapManager: IAPManager, preview: @escaping (Profile) -> ProfilePreview) -> InAppProcessor {
InAppProcessor(
iapManager: iapManager,
title: {
Dependencies.ProfileManager.sharedTitle($0)
},
isIncluded: {
Dependencies.ProfileManager.isIncluded($0, $1)
},
preview: preview,
requiredFeatures: { iap, profile in
do {
try iap.verify(profile)
return nil
} catch AppError.ineligibleProfile(let requiredFeatures) {
return requiredFeatures
} catch {
return nil
}
},
willRebuild: { _, builder in
builder
},
willInstall: { iap, profile in
try iap.verify(profile)
// validate provider modules
do {
_ = try profile.withProviderModules()
return profile
} catch {
pp_log(.app, .error, "Unable to inject provider modules: \(error)")
throw error
}
}
)
}
}
extension PreferencesManager { extension PreferencesManager {
@MainActor
static func sharedImplementation(withCloudKit: Bool) -> PreferencesManager { static func sharedImplementation(withCloudKit: Bool) -> PreferencesManager {
let preferencesStore = CoreDataPersistentStore( let preferencesStore = CoreDataPersistentStore(
logger: .default, logger: .default,

View File

@ -40,7 +40,7 @@ extension AppContext {
[] []
} }
) )
let processor = InAppProcessor.sharedImplementation(with: iapManager) { let processor = DefaultAppProcessor(iapManager: iapManager) {
$0.localizedPreview $0.localizedPreview
} }

View File

@ -36,12 +36,14 @@ final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
parameters: Constants.shared.log, parameters: Constants.shared.log,
logsPrivateData: UserDefaults.appGroup.bool(forKey: AppPreference.logsPrivateData.key) logsPrivateData: UserDefaults.appGroup.bool(forKey: AppPreference.logsPrivateData.key)
) )
let processor = DefaultTunnelProcessor(preferencesManager: .sharedForTunnel)
do { do {
fwd = try await NEPTPForwarder( fwd = try await NEPTPForwarder(
provider: self, provider: self,
decoder: Registry.sharedProtocolCoder, decoder: Registry.sharedProtocolCoder,
registry: .shared, registry: .shared,
environment: .shared environment: .shared,
profileBlock: processor.willStart
) )
guard let fwd else { guard let fwd else {
fatalError("NEPTPForwarder nil without throwing error?") fatalError("NEPTPForwarder nil without throwing error?")