chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,665 @@
|
||||
import Foundation
|
||||
#if os(macOS)
|
||||
import Darwin
|
||||
import Security
|
||||
#endif
|
||||
|
||||
public enum KeychainCacheStore {
|
||||
public struct Key: Hashable, Sendable {
|
||||
public let category: String
|
||||
public let identifier: String
|
||||
|
||||
public init(category: String, identifier: String) {
|
||||
self.category = category
|
||||
self.identifier = identifier
|
||||
}
|
||||
|
||||
var account: String {
|
||||
"\(self.category).\(self.identifier)"
|
||||
}
|
||||
}
|
||||
|
||||
public enum LoadResult<Entry> {
|
||||
case found(Entry)
|
||||
case missing
|
||||
case temporarilyUnavailable
|
||||
case invalid
|
||||
}
|
||||
|
||||
public enum ClearResult: Equatable, Sendable {
|
||||
case removed
|
||||
case missing
|
||||
case failed
|
||||
}
|
||||
|
||||
public enum KeysResult: Equatable, Sendable {
|
||||
case found([Key])
|
||||
case temporarilyUnavailable
|
||||
case failed
|
||||
}
|
||||
|
||||
private static let log = CodexBarLog.logger(LogCategories.keychainCache)
|
||||
private static let cacheService = "com.steipete.codexbar.cache"
|
||||
private static let cacheLabel = "CodexBar Cache"
|
||||
private nonisolated(unsafe) static var globalServiceOverride: String?
|
||||
@TaskLocal private static var serviceOverride: String?
|
||||
#if DEBUG
|
||||
@TaskLocal private static var operationRecorder: OperationRecorder?
|
||||
|
||||
enum Operation: Equatable, Sendable {
|
||||
case load
|
||||
case store
|
||||
case clear
|
||||
}
|
||||
|
||||
final class OperationRecorder: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var recordedOperations: [Operation] = []
|
||||
|
||||
var operations: [Operation] {
|
||||
self.lock.withLock { self.recordedOperations }
|
||||
}
|
||||
|
||||
func record(_ operation: Operation) {
|
||||
self.lock.withLock {
|
||||
self.recordedOperations.append(operation)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if DEBUG && os(macOS)
|
||||
@TaskLocal private static var loadFailureStatusOverride: OSStatus?
|
||||
@TaskLocal private static var storeFailureStatusOverride: OSStatus?
|
||||
@TaskLocal private static var clearFailureStatusOverride: OSStatus?
|
||||
@TaskLocal private static var keysFailureStatusOverride: OSStatus?
|
||||
#endif
|
||||
private static let testStoreLock = NSLock()
|
||||
private struct TestStoreKey: Hashable {
|
||||
let service: String
|
||||
let account: String
|
||||
}
|
||||
|
||||
private nonisolated(unsafe) static var testStore: [TestStoreKey: Data]?
|
||||
private nonisolated(unsafe) static var implicitTestStore: [TestStoreKey: Data] = [:]
|
||||
private nonisolated(unsafe) static var testStoreRefCount = 0
|
||||
|
||||
public static func load<Entry: Codable>(
|
||||
key: Key,
|
||||
as type: Entry.Type = Entry.self) -> LoadResult<Entry>
|
||||
{
|
||||
#if DEBUG
|
||||
self.operationRecorder?.record(.load)
|
||||
#endif
|
||||
#if DEBUG && os(macOS)
|
||||
if let status = self.loadFailureStatusOverride {
|
||||
return self.loadResultForKeychainReadFailure(status: status, key: key)
|
||||
}
|
||||
#endif
|
||||
if let testResult = loadFromTestStore(key: key, as: type) {
|
||||
return testResult
|
||||
}
|
||||
guard self.canUseRealKeychain else { return .missing }
|
||||
#if os(macOS)
|
||||
var query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: self.serviceName,
|
||||
kSecAttrAccount as String: key.account,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecReturnData as String: true,
|
||||
]
|
||||
KeychainNoUIQuery.apply(to: &query)
|
||||
|
||||
var result: AnyObject?
|
||||
let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
|
||||
switch status {
|
||||
case errSecSuccess:
|
||||
guard let data = result as? Data, !data.isEmpty else {
|
||||
self.log.error("Keychain cache item was empty (\(key.account))")
|
||||
return .invalid
|
||||
}
|
||||
let decoder = Self.makeDecoder()
|
||||
guard let decoded = try? decoder.decode(Entry.self, from: data) else {
|
||||
self.log.error("Failed to decode keychain cache (\(key.account))")
|
||||
return .invalid
|
||||
}
|
||||
return .found(decoded)
|
||||
default:
|
||||
return self.loadResultForKeychainReadFailure(status: status, key: key)
|
||||
}
|
||||
#else
|
||||
return .missing
|
||||
#endif
|
||||
}
|
||||
|
||||
public static func store(key: Key, entry: some Codable) {
|
||||
_ = self.storeResult(key: key, entry: entry)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public static func storeResult(key: Key, entry: some Codable) -> Bool {
|
||||
#if DEBUG
|
||||
self.operationRecorder?.record(.store)
|
||||
#endif
|
||||
#if DEBUG && os(macOS)
|
||||
if let status = self.storeFailureStatusOverride {
|
||||
self.log.error("Keychain cache store failed (\(key.account)): \(status)")
|
||||
return false
|
||||
}
|
||||
#endif
|
||||
if let stored = self.storeInTestStore(key: key, entry: entry) {
|
||||
return stored
|
||||
}
|
||||
guard self.canUseRealKeychain else { return false }
|
||||
#if os(macOS)
|
||||
let encoder = Self.makeEncoder()
|
||||
guard let data = try? encoder.encode(entry) else {
|
||||
self.log.error("Failed to encode keychain cache (\(key.account))")
|
||||
return false
|
||||
}
|
||||
|
||||
var query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: self.serviceName,
|
||||
kSecAttrAccount as String: key.account,
|
||||
]
|
||||
KeychainNoUIQuery.apply(to: &query)
|
||||
|
||||
let updateStatus = KeychainSecurity.update(
|
||||
query as CFDictionary,
|
||||
[kSecValueData as String: data] as CFDictionary)
|
||||
if updateStatus == errSecSuccess {
|
||||
return true
|
||||
}
|
||||
if updateStatus != errSecItemNotFound {
|
||||
self.log.error("Keychain cache update failed (\(key.account)): \(updateStatus)")
|
||||
return false
|
||||
}
|
||||
|
||||
var addQuery = query
|
||||
addQuery[kSecValueData as String] = data
|
||||
addQuery[kSecAttrLabel as String] = self.cacheLabel
|
||||
addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
||||
if let access = self.cacheAccessControl() {
|
||||
addQuery[kSecAttrAccess as String] = access
|
||||
}
|
||||
|
||||
let addStatus = KeychainSecurity.add(addQuery as CFDictionary, nil)
|
||||
if addStatus != errSecSuccess {
|
||||
self.log.error("Keychain cache add failed (\(key.account)): \(addStatus)")
|
||||
}
|
||||
return addStatus == errSecSuccess
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public static func clear(key: Key) -> Bool {
|
||||
self.clearResult(key: key) == .removed
|
||||
}
|
||||
|
||||
public static func clearResult(key: Key) -> ClearResult {
|
||||
#if DEBUG
|
||||
self.operationRecorder?.record(.clear)
|
||||
#endif
|
||||
#if DEBUG && os(macOS)
|
||||
if let status = self.clearFailureStatusOverride {
|
||||
return self.clearResultForKeychainDeleteStatus(status, key: key)
|
||||
}
|
||||
#endif
|
||||
if let removed = self.clearTestStore(key: key) {
|
||||
return removed ? .removed : .missing
|
||||
}
|
||||
guard self.canUseRealKeychain else { return .failed }
|
||||
#if os(macOS)
|
||||
var query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: self.serviceName,
|
||||
kSecAttrAccount as String: key.account,
|
||||
]
|
||||
KeychainNoUIQuery.apply(to: &query)
|
||||
return self.clearResultForKeychainDeleteStatus(KeychainSecurity.delete(query as CFDictionary), key: key)
|
||||
#else
|
||||
return .failed
|
||||
#endif
|
||||
}
|
||||
|
||||
public static func keys(category: String) -> [Key] {
|
||||
switch self.keysResult(category: category) {
|
||||
case let .found(keys):
|
||||
keys
|
||||
case .temporarilyUnavailable, .failed:
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
public static func keysResult(category: String) -> KeysResult {
|
||||
#if DEBUG && os(macOS)
|
||||
if let status = self.keysFailureStatusOverride {
|
||||
return self.keysResultForKeychainStatus(status, category: category, result: nil)
|
||||
}
|
||||
#endif
|
||||
if let keys = self.keysFromTestStore(category: category) {
|
||||
return .found(keys)
|
||||
}
|
||||
guard self.canUseRealKeychain else { return .failed }
|
||||
#if os(macOS)
|
||||
var query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: self.serviceName,
|
||||
kSecMatchLimit as String: kSecMatchLimitAll,
|
||||
kSecReturnAttributes as String: true,
|
||||
]
|
||||
KeychainNoUIQuery.apply(to: &query)
|
||||
|
||||
var result: AnyObject?
|
||||
let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
|
||||
return self.keysResultForKeychainStatus(status, category: category, result: result)
|
||||
#else
|
||||
return .failed
|
||||
#endif
|
||||
}
|
||||
|
||||
static func setServiceOverrideForTesting(_ service: String?) {
|
||||
self.globalServiceOverride = service
|
||||
}
|
||||
|
||||
public static func withServiceOverrideForTesting<T>(
|
||||
_ service: String?,
|
||||
operation: () throws -> T) rethrows -> T
|
||||
{
|
||||
try self.$serviceOverride.withValue(service) {
|
||||
try operation()
|
||||
}
|
||||
}
|
||||
|
||||
public static func withServiceOverrideForTesting<T>(
|
||||
_ service: String?,
|
||||
operation: () async throws -> T) async rethrows -> T
|
||||
{
|
||||
try await self.$serviceOverride.withValue(service) {
|
||||
try await operation()
|
||||
}
|
||||
}
|
||||
|
||||
public static func withCurrentServiceOverrideForTesting<T>(
|
||||
operation: () async throws -> T) async rethrows -> T
|
||||
{
|
||||
let service = self.serviceOverride
|
||||
return try await self.$serviceOverride.withValue(service) {
|
||||
try await operation()
|
||||
}
|
||||
}
|
||||
|
||||
public static var currentServiceOverrideForTesting: String? {
|
||||
self.serviceOverride
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
static func withOperationRecorderForTesting<T>(
|
||||
_ recorder: OperationRecorder?,
|
||||
operation: () throws -> T) rethrows -> T
|
||||
{
|
||||
try self.$operationRecorder.withValue(recorder) {
|
||||
try operation()
|
||||
}
|
||||
}
|
||||
|
||||
static func withOperationRecorderForTesting<T>(
|
||||
_ recorder: OperationRecorder?,
|
||||
operation: () async throws -> T) async rethrows -> T
|
||||
{
|
||||
try await self.$operationRecorder.withValue(recorder) {
|
||||
try await operation()
|
||||
}
|
||||
}
|
||||
|
||||
static var currentOperationRecorderForTesting: OperationRecorder? {
|
||||
self.operationRecorder
|
||||
}
|
||||
#endif
|
||||
|
||||
static var canUseRealKeychainForTesting: Bool {
|
||||
self.canUseRealKeychain
|
||||
}
|
||||
|
||||
static var canEnumerateOrDeleteRealKeychainForTesting: Bool {
|
||||
self.canUseRealKeychain
|
||||
}
|
||||
|
||||
#if DEBUG && os(macOS)
|
||||
public static func withLoadFailureStatusOverrideForTesting<T>(
|
||||
_ status: OSStatus?,
|
||||
operation: () throws -> T) rethrows -> T
|
||||
{
|
||||
try self.$loadFailureStatusOverride.withValue(status) {
|
||||
try operation()
|
||||
}
|
||||
}
|
||||
|
||||
public static func withStoreFailureStatusOverrideForTesting<T>(
|
||||
_ status: OSStatus?,
|
||||
operation: () throws -> T) rethrows -> T
|
||||
{
|
||||
try self.$storeFailureStatusOverride.withValue(status) {
|
||||
try operation()
|
||||
}
|
||||
}
|
||||
|
||||
public static func withClearFailureStatusOverrideForTesting<T>(
|
||||
_ status: OSStatus?,
|
||||
operation: () throws -> T) rethrows -> T
|
||||
{
|
||||
try self.$clearFailureStatusOverride.withValue(status) {
|
||||
try operation()
|
||||
}
|
||||
}
|
||||
|
||||
public static func withClearFailureStatusOverrideForTesting<T>(
|
||||
_ status: OSStatus?,
|
||||
operation: () async throws -> T) async rethrows -> T
|
||||
{
|
||||
try await self.$clearFailureStatusOverride.withValue(status) {
|
||||
try await operation()
|
||||
}
|
||||
}
|
||||
|
||||
public static func withKeysFailureStatusOverrideForTesting<T>(
|
||||
_ status: OSStatus?,
|
||||
operation: () throws -> T) rethrows -> T
|
||||
{
|
||||
try self.$keysFailureStatusOverride.withValue(status) {
|
||||
try operation()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static func setTestStoreForTesting(_ enabled: Bool) {
|
||||
self.testStoreLock.lock()
|
||||
defer { self.testStoreLock.unlock() }
|
||||
if enabled {
|
||||
self.testStoreRefCount += 1
|
||||
if self.testStoreRefCount == 1 {
|
||||
self.testStore = [:]
|
||||
}
|
||||
} else {
|
||||
self.testStoreRefCount = max(0, self.testStoreRefCount - 1)
|
||||
if self.testStoreRefCount == 0 {
|
||||
self.testStore = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static var serviceName: String {
|
||||
serviceOverride ?? self.globalServiceOverride ?? self.cacheService
|
||||
}
|
||||
|
||||
private static var canUseRealKeychain: Bool {
|
||||
!KeychainAccessGate.isDisabled
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private static var shouldUseImplicitTestStore: Bool {
|
||||
KeychainTestSafety.isRunningUnderTests(
|
||||
processName: ProcessInfo.processInfo.processName,
|
||||
environment: ProcessInfo.processInfo.environment) && !self.canUseRealKeychain
|
||||
}
|
||||
#else
|
||||
private static var shouldUseImplicitTestStore: Bool {
|
||||
false
|
||||
}
|
||||
#endif
|
||||
|
||||
private static func makeEncoder() -> JSONEncoder {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.dateEncodingStrategy = .iso8601
|
||||
return encoder
|
||||
}
|
||||
|
||||
private static func makeDecoder() -> JSONDecoder {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
return decoder
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
static func loadResultForKeychainReadFailure<Entry>(
|
||||
status: OSStatus,
|
||||
key: Key) -> LoadResult<Entry>
|
||||
{
|
||||
switch status {
|
||||
case errSecItemNotFound:
|
||||
return .missing
|
||||
case errSecInteractionNotAllowed:
|
||||
// Keychain is temporarily locked, e.g. immediately after wake from sleep.
|
||||
self.log.info("Keychain cache temporarily locked (\(key.account)), will retry on next access")
|
||||
return .temporarilyUnavailable
|
||||
default:
|
||||
self.log.error("Keychain cache read failed (\(key.account)): \(status)")
|
||||
return .invalid
|
||||
}
|
||||
}
|
||||
|
||||
static func clearResultForKeychainDeleteStatus(_ status: OSStatus, key: Key) -> ClearResult {
|
||||
switch status {
|
||||
case errSecSuccess:
|
||||
return .removed
|
||||
case errSecItemNotFound:
|
||||
return .missing
|
||||
case errSecInteractionNotAllowed:
|
||||
self.log.info("Keychain cache delete temporarily unavailable (\(key.account))")
|
||||
return .failed
|
||||
default:
|
||||
self.log.error("Keychain cache delete failed (\(key.account)): \(status)")
|
||||
return .failed
|
||||
}
|
||||
}
|
||||
|
||||
private static func keysResultForKeychainStatus(
|
||||
_ status: OSStatus,
|
||||
category: String,
|
||||
result: AnyObject?) -> KeysResult
|
||||
{
|
||||
switch status {
|
||||
case errSecSuccess:
|
||||
guard let rows = result as? [[String: Any]] else { return .failed }
|
||||
let keys: [Key] = rows.compactMap { row in
|
||||
guard let account = row[kSecAttrAccount as String] as? String else { return nil }
|
||||
return self.key(fromAccount: account, category: category)
|
||||
}
|
||||
return .found(keys)
|
||||
case errSecItemNotFound:
|
||||
return .found([])
|
||||
case errSecInteractionNotAllowed:
|
||||
self.log.info("Keychain cache keys temporarily unavailable (\(category))")
|
||||
return .temporarilyUnavailable
|
||||
default:
|
||||
self.log.error("Keychain cache key listing failed (\(category)): \(status)")
|
||||
return .failed
|
||||
}
|
||||
}
|
||||
|
||||
static func trustedApplicationPathsForCacheAccess(
|
||||
bundleURL: URL = Bundle.main.bundleURL,
|
||||
executableURL: URL? = Bundle.main.executableURL,
|
||||
fileExists: (String) -> Bool = { FileManager.default.fileExists(atPath: $0) }) -> [String]
|
||||
{
|
||||
var paths: [String] = []
|
||||
func append(_ path: String) {
|
||||
guard !path.isEmpty, fileExists(path), !paths.contains(path) else { return }
|
||||
paths.append(path)
|
||||
}
|
||||
|
||||
let appBundle = self.appBundleURL(containing: bundleURL)
|
||||
?? executableURL.flatMap(self.appBundleURL(containing:))
|
||||
if let appBundle {
|
||||
append(appBundle.path)
|
||||
append(appBundle.appendingPathComponent("Contents/Helpers/CodexBarCLI").path)
|
||||
}
|
||||
if let executableURL {
|
||||
append(executableURL.path)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
private static func appBundleURL(containing url: URL) -> URL? {
|
||||
var current = url.standardizedFileURL
|
||||
while current.path != "/" {
|
||||
if current.pathExtension == "app" {
|
||||
return current
|
||||
}
|
||||
current.deleteLastPathComponent()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func cacheAccessControl() -> SecAccess? {
|
||||
let trustedPaths = self.trustedApplicationPathsForCacheAccess()
|
||||
guard !trustedPaths.isEmpty else { return nil }
|
||||
|
||||
var trustedApplications: [SecTrustedApplication] = []
|
||||
for path in trustedPaths {
|
||||
let (status, application) = self.createTrustedApplication(path: path)
|
||||
if status == errSecSuccess, let application {
|
||||
trustedApplications.append(application)
|
||||
} else {
|
||||
self.log.error("Keychain cache trusted app creation failed (\(path)): \(status)")
|
||||
}
|
||||
}
|
||||
guard !trustedApplications.isEmpty else { return nil }
|
||||
|
||||
let (status, access) = self.createAccessControl(trustedApplications: trustedApplications)
|
||||
if status != errSecSuccess {
|
||||
self.log.error("Keychain cache access control creation failed: \(status)")
|
||||
return nil
|
||||
}
|
||||
return access
|
||||
}
|
||||
|
||||
private typealias SecTrustedApplicationCreateFromPathFunction = @convention(c) (
|
||||
UnsafePointer<CChar>?,
|
||||
UnsafeMutablePointer<SecTrustedApplication?>?) -> OSStatus
|
||||
private typealias SecAccessCreateFunction = @convention(c) (
|
||||
CFString,
|
||||
CFArray,
|
||||
UnsafeMutablePointer<SecAccess?>?) -> OSStatus
|
||||
|
||||
private static func createTrustedApplication(path: String) -> (OSStatus, SecTrustedApplication?) {
|
||||
guard let symbol = self.securitySymbol(named: "SecTrustedApplicationCreateFromPath") else {
|
||||
return (errSecInternalComponent, nil)
|
||||
}
|
||||
let function = unsafeBitCast(symbol, to: SecTrustedApplicationCreateFromPathFunction.self)
|
||||
var application: SecTrustedApplication?
|
||||
let status = path.withCString { cPath in
|
||||
function(cPath, &application)
|
||||
}
|
||||
return (status, application)
|
||||
}
|
||||
|
||||
private static func createAccessControl(trustedApplications: [SecTrustedApplication]) -> (OSStatus, SecAccess?) {
|
||||
guard let symbol = self.securitySymbol(named: "SecAccessCreate") else {
|
||||
return (errSecInternalComponent, nil)
|
||||
}
|
||||
let function = unsafeBitCast(symbol, to: SecAccessCreateFunction.self)
|
||||
var access: SecAccess?
|
||||
let status = function(self.cacheLabel as CFString, trustedApplications as CFArray, &access)
|
||||
return (status, access)
|
||||
}
|
||||
|
||||
private nonisolated(unsafe) static let securityFrameworkHandle: UnsafeMutableRawPointer? = {
|
||||
let securityPath = "/System/Library/Frameworks/Security.framework/Security"
|
||||
return dlopen(securityPath, RTLD_NOW)
|
||||
}()
|
||||
|
||||
private static func securitySymbol(named name: String) -> UnsafeMutableRawPointer? {
|
||||
// Resolve deprecated SecKeychain ACL helpers at runtime so release builds stay warning-free
|
||||
// while still granting the app bundle and bundled CLI prompt-free access to cache entries.
|
||||
guard let securityFrameworkHandle else { return nil }
|
||||
return dlsym(securityFrameworkHandle, name)
|
||||
}
|
||||
#endif
|
||||
|
||||
private static func loadFromTestStore<Entry: Codable>(
|
||||
key: Key,
|
||||
as type: Entry.Type) -> LoadResult<Entry>?
|
||||
{
|
||||
self.testStoreLock.lock()
|
||||
defer { self.testStoreLock.unlock() }
|
||||
guard let store = self.testStore ?? (self.shouldUseImplicitTestStore ? self.implicitTestStore : nil)
|
||||
else { return nil }
|
||||
let testKey = TestStoreKey(service: self.serviceName, account: key.account)
|
||||
guard let data = store[testKey] else { return .missing }
|
||||
let decoder = Self.makeDecoder()
|
||||
guard let decoded = try? decoder.decode(Entry.self, from: data) else {
|
||||
return .invalid
|
||||
}
|
||||
return .found(decoded)
|
||||
}
|
||||
|
||||
private static func storeInTestStore(key: Key, entry: some Codable) -> Bool? {
|
||||
self.testStoreLock.lock()
|
||||
defer { self.testStoreLock.unlock() }
|
||||
let encoder = Self.makeEncoder()
|
||||
guard let data = try? encoder.encode(entry) else { return false }
|
||||
let testKey = TestStoreKey(service: self.serviceName, account: key.account)
|
||||
if var store = self.testStore {
|
||||
store[testKey] = data
|
||||
self.testStore = store
|
||||
return true
|
||||
}
|
||||
if self.shouldUseImplicitTestStore {
|
||||
self.implicitTestStore[testKey] = data
|
||||
return true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func clearTestStore(key: Key) -> Bool? {
|
||||
self.testStoreLock.lock()
|
||||
defer { self.testStoreLock.unlock() }
|
||||
let testKey = TestStoreKey(service: self.serviceName, account: key.account)
|
||||
if var store = self.testStore {
|
||||
let removed = store.removeValue(forKey: testKey) != nil
|
||||
self.testStore = store
|
||||
return removed
|
||||
}
|
||||
if self.shouldUseImplicitTestStore {
|
||||
return self.implicitTestStore.removeValue(forKey: testKey) != nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func keysFromTestStore(category: String) -> [Key]? {
|
||||
self.testStoreLock.lock()
|
||||
defer { self.testStoreLock.unlock() }
|
||||
guard let store = self.testStore ?? (self.shouldUseImplicitTestStore ? self.implicitTestStore : nil)
|
||||
else { return nil }
|
||||
return store.keys
|
||||
.filter { $0.service == self.serviceName }
|
||||
.compactMap { self.key(fromAccount: $0.account, category: category) }
|
||||
.sorted { $0.identifier < $1.identifier }
|
||||
}
|
||||
|
||||
private static func key(fromAccount account: String, category: String) -> Key? {
|
||||
let prefix = "\(category)."
|
||||
guard account.hasPrefix(prefix) else { return nil }
|
||||
let identifier = String(account.dropFirst(prefix.count))
|
||||
guard !identifier.isEmpty else { return nil }
|
||||
return Key(category: category, identifier: identifier)
|
||||
}
|
||||
}
|
||||
|
||||
extension KeychainCacheStore.Key {
|
||||
public static func cookie(provider: UsageProvider, scopeIdentifier: String? = nil) -> Self {
|
||||
let identifier: String = if let scopeIdentifier, !scopeIdentifier.isEmpty {
|
||||
"\(provider.rawValue).\(scopeIdentifier)"
|
||||
} else {
|
||||
provider.rawValue
|
||||
}
|
||||
return Self(category: "cookie", identifier: identifier)
|
||||
}
|
||||
|
||||
public static func oauth(provider: UsageProvider) -> Self {
|
||||
Self(category: "oauth", identifier: provider.rawValue)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user