247 lines · 9.1 KB · Swift
RawDownload
import Foundation#if os(macOS)import Security#endif public enum AppGroupSupport {    public static let defaultTeamID = "Y5PE65HELJ"    public static let teamIDInfoKey = "CodexBarTeamID"    public static let legacyReleaseGroupID = "group.com.steipete.codexbar"    public static let legacyDebugGroupID = "group.com.steipete.codexbar.debug"    public static let widgetSnapshotFilename = "widget-snapshot.json"    public static let migrationVersion = 1    public static let migrationVersionKey = "appGroupMigrationVersion"    private static let sharedDefaultsMigrationKeys = [        "debugDisableKeychainAccess",        "widgetSelectedProvider",    ]     public struct MigrationResult: Sendable {        public enum Status: String, Sendable {            case alreadyCompleted            case targetUnavailable            case noChangesNeeded            case migrated        }         public let status: Status        public let copiedSnapshot: Bool        public let copiedDefaults: Int         public init(status: Status, copiedSnapshot: Bool = false, copiedDefaults: Int = 0) {            self.status = status            self.copiedSnapshot = copiedSnapshot            self.copiedDefaults = copiedDefaults        }    }     public static func currentGroupID(for bundleID: String? = Bundle.main.bundleIdentifier) -> String {        self.currentGroupID(teamID: self.resolvedTeamID(), bundleID: bundleID)    }     static func currentGroupID(teamID: String, bundleID: String?) -> String {        let base = "\(teamID).com.steipete.codexbar"        return self.isDebugBundleID(bundleID) ? "\(base).debug" : base    }     public static func resolvedTeamID(bundle: Bundle = .main) -> String {        self.resolvedTeamID(            infoDictionaryOverride: bundle.infoDictionary,            bundleURLOverride: bundle.bundleURL)    }     static func resolvedTeamID(        infoDictionaryOverride: [String: Any]?,        bundleURLOverride: URL?) -> String    {        if let teamID = self.codeSignatureTeamID(bundleURL: bundleURLOverride) {            return teamID        }        if let teamID = infoDictionaryOverride?[self.teamIDInfoKey] as? String,           !teamID.isEmpty        {            return teamID        }        return self.defaultTeamID    }     public static func legacyGroupID(for bundleID: String? = Bundle.main.bundleIdentifier) -> String {        self.isDebugBundleID(bundleID) ? self.legacyDebugGroupID : self.legacyReleaseGroupID    }     public static func sharedDefaults(        bundleID: String? = Bundle.main.bundleIdentifier,        fileManager: FileManager = .default)        -> UserDefaults?    {        guard self.currentContainerURL(bundleID: bundleID, fileManager: fileManager) != nil else { return nil }        return UserDefaults(suiteName: self.currentGroupID(for: bundleID))    }     public static func currentContainerURL(        bundleID: String? = Bundle.main.bundleIdentifier,        fileManager: FileManager = .default)        -> URL?    {        #if os(macOS)        fileManager.containerURL(forSecurityApplicationGroupIdentifier: self.currentGroupID(for: bundleID))        #else        nil        #endif    }     public static func snapshotURL(        bundleID: String? = Bundle.main.bundleIdentifier,        fileManager: FileManager = .default,        homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser)        -> URL    {        if let container = self.currentContainerURL(bundleID: bundleID, fileManager: fileManager) {            return container.appendingPathComponent(self.widgetSnapshotFilename, isDirectory: false)        }         let directory = self.localFallbackDirectory(fileManager: fileManager, homeDirectory: homeDirectory)        return directory.appendingPathComponent(self.widgetSnapshotFilename, isDirectory: false)    }     public static func localFallbackDirectory(        fileManager: FileManager = .default,        homeDirectory _: URL = FileManager.default.homeDirectoryForCurrentUser)        -> URL    {        let base = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first            ?? fileManager.temporaryDirectory        let directory = base.appendingPathComponent("CodexBar", isDirectory: true)        try? fileManager.createDirectory(at: directory, withIntermediateDirectories: true)        return directory    }     public static func legacyContainerCandidateURL(        bundleID: String? = Bundle.main.bundleIdentifier,        homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser)        -> URL    {        homeDirectory            .appendingPathComponent("Library", isDirectory: true)            .appendingPathComponent("Group Containers", isDirectory: true)            .appendingPathComponent(self.legacyGroupID(for: bundleID), isDirectory: true)    }     public static func migrateLegacyDataIfNeeded(        bundleID: String? = Bundle.main.bundleIdentifier,        standardDefaults: UserDefaults = .standard,        fileManager: FileManager = .default,        homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser,        currentDefaultsOverride: UserDefaults? = nil,        legacyDefaultsOverride: UserDefaults? = nil,        currentSnapshotURLOverride: URL? = nil,        legacySnapshotURLOverride: URL? = nil)        -> MigrationResult    {        if standardDefaults.integer(forKey: self.migrationVersionKey) >= self.migrationVersion {            return MigrationResult(status: .alreadyCompleted)        }         guard let currentDefaults = currentDefaultsOverride ?? self.sharedDefaults(            bundleID: bundleID,            fileManager: fileManager)        else {            return MigrationResult(status: .targetUnavailable)        }         let legacyDefaults = legacyDefaultsOverride ?? UserDefaults(suiteName: self.legacyGroupID(for: bundleID))        let currentSnapshotURL = currentSnapshotURLOverride            ?? self.currentContainerURL(bundleID: bundleID, fileManager: fileManager)?            .appendingPathComponent(self.widgetSnapshotFilename, isDirectory: false)        let legacySnapshotURL = legacySnapshotURLOverride            ?? self.legacyContainerCandidateURL(bundleID: bundleID, homeDirectory: homeDirectory)            .appendingPathComponent(self.widgetSnapshotFilename, isDirectory: false)         let copiedSnapshot = {            guard let currentSnapshotURL else { return false }            guard !fileManager.fileExists(atPath: currentSnapshotURL.path),                  fileManager.fileExists(atPath: legacySnapshotURL.path)            else {                return false            }            do {                try fileManager.createDirectory(                    at: currentSnapshotURL.deletingLastPathComponent(),                    withIntermediateDirectories: true)                try fileManager.copyItem(at: legacySnapshotURL, to: currentSnapshotURL)                return true            } catch {                return false            }        }()         let copiedDefaults = self.copyLegacySharedDefaults(            from: legacyDefaults,            to: currentDefaults)         let result = if copiedSnapshot || copiedDefaults > 0 {            MigrationResult(                status: .migrated,                copiedSnapshot: copiedSnapshot,                copiedDefaults: copiedDefaults)        } else {            MigrationResult(status: .noChangesNeeded)        }         standardDefaults.set(self.migrationVersion, forKey: self.migrationVersionKey)        return result    }     private static func copyLegacySharedDefaults(        from legacyDefaults: UserDefaults?,        to currentDefaults: UserDefaults) -> Int    {        guard let legacyDefaults else { return 0 }         var copied = 0        for key in self.sharedDefaultsMigrationKeys {            guard currentDefaults.object(forKey: key) == nil,                  let legacyValue = legacyDefaults.object(forKey: key)            else {                continue            }            currentDefaults.set(legacyValue, forKey: key)            copied += 1        }        return copied    }     private static func isDebugBundleID(_ bundleID: String?) -> Bool {        guard let bundleID, !bundleID.isEmpty else { return false }        return bundleID.contains(".debug")    }     private static func codeSignatureTeamID(bundleURL: URL?) -> String? {        #if os(macOS)        guard let bundleURL else { return nil }         var staticCode: SecStaticCode?        guard SecStaticCodeCreateWithPath(bundleURL as CFURL, SecCSFlags(), &staticCode) == errSecSuccess,              let code = staticCode        else {            return nil        }         var infoCF: CFDictionary?        guard SecCodeCopySigningInformation(            code,            SecCSFlags(rawValue: kSecCSSigningInformation),            &infoCF) == errSecSuccess,            let info = infoCF as? [String: Any],            let teamID = info[kSecCodeInfoTeamIdentifier as String] as? String,            !teamID.isEmpty        else {            return nil        }        return teamID        #else        _ = bundleURL        return nil        #endif    }}