113 lines · 4.1 KB · Swift
RawDownload
#if os(macOS)import Foundationimport SweetCookieKit struct BrowserLocalStorageAPI: Sendable {    struct Entry: Sendable {        let key: String        let value: String    }     struct Profile: Sendable {        let id: String        let label: String        let entries: [Entry]    }     typealias Loader = @Sendable (        _ origin: String,        _ browsers: [Browser],        _ detection: BrowserDetection,        _ logger: @escaping @Sendable (String) -> Void) -> [Profile]     private let loader: Loader     init(loader: @escaping Loader) {        self.loader = loader    }     func profiles(        for origin: String,        browsers: [Browser],        using detection: BrowserDetection,        logger: @escaping @Sendable (String) -> Void) -> [Profile]    {        self.loader(origin, browsers, detection, logger)    }     static let live = BrowserLocalStorageAPI { origin, browsers, detection, logger in        let installedBrowsers = browsers.browsersWithProfileData(using: detection)        let roots = ChromiumProfileLocator.roots(            for: installedBrowsers,            homeDirectories: BrowserCookieClient.defaultHomeDirectories())        return roots.flatMap { root in            Self.loadProfiles(                origin: origin,                root: root.url,                browserID: root.browser.rawValue,                labelPrefix: root.labelPrefix,                logger: logger)        }    }     private static func loadProfiles(        origin: String,        root: URL,        browserID: String,        labelPrefix: String,        logger: @escaping @Sendable (String) -> Void) -> [Profile]    {        guard let directories = try? FileManager.default.contentsOfDirectory(            at: root,            includingPropertiesForKeys: [.isDirectoryKey],            options: [.skipsHiddenFiles])        else { return [] }         let profileNames = Self.chromeProfileNames(root: root)        return directories.compactMap { directory in            guard !Task.isCancelled,                  let isDirectory = try? directory.resourceValues(forKeys: [.isDirectoryKey]).isDirectory,                  isDirectory            else { return nil }            let name = directory.lastPathComponent            guard name == "Default" || name.hasPrefix("Profile ") || name.hasPrefix("user-") else {                return nil            }            let levelDB = directory.appendingPathComponent("Local Storage").appendingPathComponent("leveldb")            guard FileManager.default.fileExists(atPath: levelDB.path) else { return nil }             let id = "\(browserID):\(name)"            logger("Checking \(id)")            let entries = SweetCookieKit.ChromiumLocalStorageReader.readEntries(                for: origin,                in: levelDB,                logger: logger)                .map { Entry(key: $0.key, value: $0.value) }            let displayName = profileNames[name]?.trimmingCharacters(in: .whitespacesAndNewlines)            let label = if let displayName, !displayName.isEmpty, displayName != name {                "\(labelPrefix) — \(displayName)"            } else {                "\(labelPrefix) \(name)"            }            return Profile(id: id, label: label, entries: entries)        }        .sorted { $0.label.localizedStandardCompare($1.label) == .orderedAscending }    }     private static func chromeProfileNames(root: URL) -> [String: String] {        let localStateURL = root.appendingPathComponent("Local State")        guard let data = try? Data(contentsOf: localStateURL),              let rootObject = try? JSONSerialization.jsonObject(with: data) as? [String: Any],              let profile = rootObject["profile"] as? [String: Any],              let infoCache = profile["info_cache"] as? [String: Any]        else { return [:] }         return infoCache.reduce(into: [:]) { result, entry in            guard let info = entry.value as? [String: Any] else { return }            let name = (info["gaia_given_name"] as? String) ?? (info["name"] as? String)            guard let name else { return }            result[entry.key] = name        }    }}#endif