-
Notifications
You must be signed in to change notification settings - Fork 125
Expand file tree
/
Copy pathAuthenticatorBridgeDataStore.swift
More file actions
190 lines (164 loc) · 7.69 KB
/
AuthenticatorBridgeDataStore.swift
File metadata and controls
190 lines (164 loc) · 7.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import BitwardenKit
@preconcurrency import CoreData
// MARK: - AuthenticatorStoreType
/// A type of data store.
///
public enum AuthenticatorBridgeStoreType {
/// The data store is stored only in memory and isn't persisted to the device. This is used for
/// unit testing.
case memory
/// The data store is persisted to the device.
case persisted
}
// MARK: - AuthenticatorDataStore
/// The CoreData model name used within `AuthenticatorBridgeDataStore`.
private let authenticatorBridgeModelName = "Bitwarden-Authenticator"
/// A data store that manages persisting data across app launches in Core Data.
/// This is currently marked `@unchecked Sendable` because of how we ensure thread safety of the `backgroundContext`
/// property. Once we have a minimum version of iOS 16 or higher, we can migrate to the `Synchronization` framework
/// and make this more properly `Sendable`.
///
public final nonisolated class AuthenticatorBridgeDataStore: @unchecked Sendable {
// MARK: Type Properties
/// The managed object model representing the entities in the database schema. CoreData throws
/// warnings if this is instantiated multiple times (e.g. in tests), which is fixed by making
/// it static.
private static let managedObjectModel: NSManagedObjectModel = {
#if SWIFT_PACKAGE
let bundle = Bundle.module
#else
let bundle = Bundle(for: AuthenticatorBridgeDataStore.self)
#endif
let modelURL = bundle.url(forResource: authenticatorBridgeModelName, withExtension: "momd")!
let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL)!
return managedObjectModel
}()
static func persistedStoreURL(
fileManager: FileManager = .default,
groupIdentifier: String,
bundleIdentifier: String? = Bundle.main.bundleIdentifier,
bundlePath: String = Bundle.main.bundlePath,
containerURLProvider: (FileManager, String) -> URL? = { fileManager, groupIdentifier in
fileManager.containerURL(forSecurityApplicationGroupIdentifier: groupIdentifier)
}
) -> URL {
#if targetEnvironment(simulator)
if bundlePath.contains(".appex") {
let applicationSupportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
?? fileManager.temporaryDirectory
let directoryURL = applicationSupportURL
.appendingPathComponent(bundleIdentifier ?? "AuthenticatorBridgeExtension", isDirectory: true)
try? fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
return directoryURL.appendingPathComponent("\(authenticatorBridgeModelName).sqlite")
}
#endif
if let containerURL = containerURLProvider(fileManager, groupIdentifier) {
return containerURL.appendingPathComponent("\(authenticatorBridgeModelName).sqlite")
}
return fileManager.temporaryDirectory.appendingPathComponent("\(authenticatorBridgeModelName).sqlite")
}
// MARK: Properties
/// A thread-safe lock for `backgroundContext`. Once we have a minimum of iOS 16, we can use an
/// `OSAllocatedUnfairLock` instead.
private let _backgroundContextLock = DispatchQueue(label: "backgroundContext.lock")
/// A private backing for `backgroundContext`. The `backgroundContext` variable provides thread-safe access, and
/// is what should be used. Once we have a minimum of iOS 16, this can be converted to an `OSAllocatedUnfairLock`,
/// and remove the need for the additional `_backgroundContextLock`.
private var _backgroundContext: NSManagedObjectContext?
/// A managed object context which executes on a background queue.
/// This is the thread-safe version of the backing variable `_backgroundContext`,
/// and initializes that property lazily.
public var backgroundContext: NSManagedObjectContext {
_backgroundContextLock.sync {
if let context = _backgroundContext {
return context
}
let newContext = persistentContainer.newBackgroundContext()
newContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
_backgroundContext = newContext
return newContext
}
}
/// The service used by the application to report non-fatal errors.
let errorReporter: ErrorReporter
/// The Core Data persistent container.
public let persistentContainer: NSPersistentContainer
// MARK: Initialization
/// Initialize a `AuthenticatorBridgeDataStore`.
///
/// - Parameters:
/// - errorReporter: The service used by the application to report non-fatal errors.
/// - groupIdentifier: The app group identifier for the shared resource.
/// - storeType: The type of store to create.
///
public init(
errorReporter: ErrorReporter,
groupIdentifier: String,
storeType: AuthenticatorBridgeStoreType = .persisted,
) {
self.errorReporter = errorReporter
persistentContainer = NSPersistentContainer(
name: authenticatorBridgeModelName,
managedObjectModel: Self.managedObjectModel,
)
let storeDescription: NSPersistentStoreDescription
switch storeType {
case .memory:
storeDescription = NSPersistentStoreDescription(url: URL(fileURLWithPath: "/dev/null"))
case .persisted:
let storeURL = Self.persistedStoreURL(groupIdentifier: groupIdentifier)
storeDescription = NSPersistentStoreDescription(url: storeURL)
}
persistentContainer.persistentStoreDescriptions = [storeDescription]
persistentContainer.loadPersistentStores { _, error in
if let error {
errorReporter.log(error: error)
}
}
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
}
// MARK: Methods
/// Executes a batch delete request and merges the changes into the background and view contexts.
///
/// - Parameter request: The batch delete request to perform.
///
public func executeBatchDelete(_ request: NSBatchDeleteRequest) async throws {
try await backgroundContext.perform {
try self.backgroundContext.executeAndMergeChanges(
batchDeleteRequest: request,
additionalContexts: [self.persistentContainer.viewContext],
)
}
}
/// Executes a batch insert request and merges the changes into the background and view contexts.
///
/// - Parameter request: The batch insert request to perform.
///
public func executeBatchInsert(_ request: NSBatchInsertRequest) async throws {
try await backgroundContext.perform {
try self.backgroundContext.executeAndMergeChanges(
batchInsertRequest: request,
additionalContexts: [self.persistentContainer.viewContext],
)
}
}
/// Executes a batch delete and batch insert request and merges the changes into the background
/// and view contexts.
///
/// - Parameters:
/// - deleteRequest: The batch delete request to perform.
/// - insertRequest: The batch insert request to perform.
///
public func executeBatchReplace(
deleteRequest: NSBatchDeleteRequest,
insertRequest: NSBatchInsertRequest,
) async throws {
try await backgroundContext.perform {
try self.backgroundContext.executeAndMergeChanges(
batchDeleteRequest: deleteRequest,
batchInsertRequest: insertRequest,
additionalContexts: [self.persistentContainer.viewContext],
)
}
}
}