Skip to content

Commit 798caf7

Browse files
yibieclaude
andcommitted
feat: add URL scheme / protocol handler protection
Extend FileTypeGuard to protect URL scheme associations (mailto:, https:, etc.) in addition to file type associations. Introduces ProtectableTarget enum to unify both protection types with backward-compatible config migration. - New models: URLScheme, ProtectableTarget, CommonURLSchemes (12 presets) - ProtectionRule now wraps ProtectableTarget with legacy Codable fallback - LaunchServicesManager: add 3 URL scheme APIs (get/set/list handlers) - ProtectionEngine: parallel validation & recovery path for URL schemes - UI: segmented picker (File Types / URL Schemes) in FileTypePickerView - RuleRow shows distinct icons for file types vs URL schemes - Localized in en, ja, zh-Hans, zh-Hant Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4c26dbe commit 798caf7

13 files changed

Lines changed: 1560 additions & 107 deletions

FileTypeGuard/App/FileTypeGuardApp.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,15 +104,15 @@ final class AppCoordinator: ObservableObject {
104104

105105
// 获取显示名称
106106
Task { @MainActor in
107-
if let rule = ConfigurationManager.shared.getProtectionRules().first(where: { $0.fileType.uti == uti }) {
107+
if let rule = ConfigurationManager.shared.getProtectionRules().first(where: { $0.target.lookupKey == uti }) {
108108
let oldAppInfo = Application.from(bundleID: oldApp)
109109
let newAppInfo = Application.from(bundleID: newApp)
110110

111111
// 发送成功通知
112112
if ConfigurationManager.shared.getPreferences().showNotifications {
113113
self.notificationService.send(.associationRestored(
114114
fileType: uti,
115-
fileTypeName: rule.fileType.displayName,
115+
fileTypeName: rule.target.displayName,
116116
fromApp: oldAppInfo?.name ?? oldApp,
117117
toApp: newAppInfo?.name ?? newApp
118118
))
@@ -130,12 +130,12 @@ final class AppCoordinator: ObservableObject {
130130

131131
// 获取显示名称
132132
Task { @MainActor in
133-
if let rule = ConfigurationManager.shared.getProtectionRules().first(where: { $0.fileType.uti == uti }) {
133+
if let rule = ConfigurationManager.shared.getProtectionRules().first(where: { $0.target.lookupKey == uti }) {
134134
// 发送失败通知
135135
if ConfigurationManager.shared.getPreferences().showNotifications {
136136
self.notificationService.send(.recoveryFailed(
137137
fileType: uti,
138-
fileTypeName: rule.fileType.displayName,
138+
fileTypeName: rule.target.displayName,
139139
error: error.localizedDescription
140140
))
141141
}

FileTypeGuard/Core/Configuration/ConfigurationManager.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,12 @@ final class ConfigurationManager {
145145
func addProtectionRule(_ rule: ProtectionRule) throws {
146146
var config = loadConfiguration()
147147

148-
// 检查是否已存在相同的 UTI
149-
if config.protectedTypes.contains(where: { $0.fileType.uti == rule.fileType.uti }) {
150-
print("⚠️ 已存在相同 UTI 的保护规则: \(rule.fileType.uti)")
148+
// 检查是否已存在相同目标的保护规则
149+
let lookupKey = rule.target.lookupKey
150+
if config.protectedTypes.contains(where: { $0.target.lookupKey == lookupKey }) {
151+
print("⚠️ 已存在相同目标的保护规则: \(lookupKey)")
151152
// 替换现有规则
152-
config.protectedTypes.removeAll { $0.fileType.uti == rule.fileType.uti }
153+
config.protectedTypes.removeAll { $0.target.lookupKey == lookupKey }
153154
}
154155

155156
config.protectedTypes.append(rule)
@@ -195,6 +196,16 @@ final class ConfigurationManager {
195196
return loadConfiguration().preferences
196197
}
197198

199+
/// 获取所有文件类型保护规则
200+
func getFileTypeRules() -> [ProtectionRule] {
201+
return getProtectionRules().filter { $0.target.isFileType }
202+
}
203+
204+
/// 获取所有 URL Scheme 保护规则
205+
func getURLSchemeRules() -> [ProtectionRule] {
206+
return getProtectionRules().filter { $0.target.isURLScheme }
207+
}
208+
198209
/// 导出配置到指定路径
199210
/// - Parameter url: 导出路径
200211
func exportConfiguration(to url: URL) throws {

FileTypeGuard/Core/LaunchServices/LaunchServicesManager.swift

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,4 +226,67 @@ final class LaunchServicesManager {
226226

227227
return Array(bundleIDs)
228228
}
229+
230+
// MARK: - URL Scheme Methods
231+
232+
/// 获取指定 URL Scheme 的默认处理应用
233+
/// - Parameter scheme: URL Scheme 字符串,如 "https"、"mailto"
234+
/// - Returns: 默认应用的 Bundle ID
235+
/// - Throws: LSError 如果获取失败
236+
func getDefaultHandlerForURLScheme(_ scheme: String) throws -> String? {
237+
guard !scheme.isEmpty else {
238+
throw LSError.invalidUTI
239+
}
240+
241+
let schemeCF = scheme as CFString
242+
243+
guard let bundleID = LSCopyDefaultHandlerForURLScheme(schemeCF)?.takeRetainedValue() as String? else {
244+
return nil
245+
}
246+
247+
return bundleID
248+
}
249+
250+
/// 设置指定 URL Scheme 的默认处理应用
251+
/// - Parameters:
252+
/// - bundleID: 应用的 Bundle ID
253+
/// - scheme: URL Scheme 字符串
254+
/// - Throws: LSError 如果设置失败
255+
func setDefaultHandlerForURLScheme(_ bundleID: String, scheme: String) throws {
256+
guard !scheme.isEmpty, !bundleID.isEmpty else {
257+
throw LSError.invalidUTI
258+
}
259+
260+
let bundleIDCF = bundleID as CFString
261+
let schemeCF = scheme as CFString
262+
263+
let status = LSSetDefaultHandlerForURLScheme(schemeCF, bundleIDCF)
264+
265+
guard status == noErr else {
266+
throw LSError.setFailed(status)
267+
}
268+
269+
// 验证设置是否成功
270+
let currentApp = try getDefaultHandlerForURLScheme(scheme)
271+
guard currentApp == bundleID else {
272+
throw LSError.setFailed(-1)
273+
}
274+
}
275+
276+
/// 获取可以处理指定 URL Scheme 的所有应用
277+
/// - Parameter scheme: URL Scheme 字符串
278+
/// - Returns: 应用 Bundle ID 数组
279+
func getAvailableHandlersForURLScheme(_ scheme: String) -> [String] {
280+
guard !scheme.isEmpty else {
281+
return []
282+
}
283+
284+
let schemeCF = scheme as CFString
285+
286+
guard let apps = LSCopyAllHandlersForURLScheme(schemeCF)?.takeRetainedValue() as? [String] else {
287+
return []
288+
}
289+
290+
return apps
291+
}
229292
}

0 commit comments

Comments
 (0)