@capgo/cordova-updater 8.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +82 -0
  3. package/dist/esm/definitions.d.ts +2684 -0
  4. package/dist/esm/definitions.js +109 -0
  5. package/dist/esm/definitions.js.map +1 -0
  6. package/dist/esm/exec.d.ts +4 -0
  7. package/dist/esm/exec.js +25 -0
  8. package/dist/esm/exec.js.map +1 -0
  9. package/dist/esm/history.d.ts +1 -0
  10. package/dist/esm/history.js +283 -0
  11. package/dist/esm/history.js.map +1 -0
  12. package/dist/esm/index.d.ts +57 -0
  13. package/dist/esm/index.js +68 -0
  14. package/dist/esm/index.js.map +1 -0
  15. package/dist/esm/plugin.d.ts +57 -0
  16. package/dist/esm/plugin.js +15 -0
  17. package/dist/esm/plugin.js.map +1 -0
  18. package/dist/plugin.cjs.js +217 -0
  19. package/dist/plugin.cjs.js.map +1 -0
  20. package/dist/plugin.js +222 -0
  21. package/dist/plugin.js.map +1 -0
  22. package/package.json +96 -0
  23. package/plugin.xml +207 -0
  24. package/src/android/CordovaUpdaterPlugin.java +5662 -0
  25. package/src/android/UpdaterPathHandler.java +99 -0
  26. package/src/android/app/capgo/cordova/updater/AndroidAppExitReporter.java +92 -0
  27. package/src/android/app/capgo/cordova/updater/AppLifecycleObserver.java +115 -0
  28. package/src/android/app/capgo/cordova/updater/BundleInfo.java +246 -0
  29. package/src/android/app/capgo/cordova/updater/BundleStatus.java +45 -0
  30. package/src/android/app/capgo/cordova/updater/Callback.java +13 -0
  31. package/src/android/app/capgo/cordova/updater/CapgoUpdater.java +3404 -0
  32. package/src/android/app/capgo/cordova/updater/CordovaUpdaterConfig.java +56 -0
  33. package/src/android/app/capgo/cordova/updater/CryptoCipher.java +462 -0
  34. package/src/android/app/capgo/cordova/updater/DataManager.java +44 -0
  35. package/src/android/app/capgo/cordova/updater/DelayCondition.java +56 -0
  36. package/src/android/app/capgo/cordova/updater/DelayUntilNext.java +14 -0
  37. package/src/android/app/capgo/cordova/updater/DelayUpdateUtils.java +307 -0
  38. package/src/android/app/capgo/cordova/updater/DeviceIdHelper.java +230 -0
  39. package/src/android/app/capgo/cordova/updater/DownloadService.java +1219 -0
  40. package/src/android/app/capgo/cordova/updater/DownloadWorkerManager.java +260 -0
  41. package/src/android/app/capgo/cordova/updater/InternalUtils.java +54 -0
  42. package/src/android/app/capgo/cordova/updater/Logger.java +345 -0
  43. package/src/android/app/capgo/cordova/updater/ShakeDetector.java +72 -0
  44. package/src/android/app/capgo/cordova/updater/ShakeMenu.java +39 -0
  45. package/src/android/app/capgo/cordova/updater/ThreeFingerPinchDetector.java +323 -0
  46. package/src/android/app/capgo/cordova/updater/compat/JSArray.java +9 -0
  47. package/src/android/app/capgo/cordova/updater/compat/JSObject.java +83 -0
  48. package/src/android/app/capgo/cordova/updater/compat/PluginCall.java +165 -0
  49. package/src/android/capgo-cordova-updater.gradle +19 -0
  50. package/src/android/capgo-updater.xml +3 -0
  51. package/src/android/test/app/capgo/cordova/updater/CapacitorUpdaterUnitTest.java.skip +3039 -0
  52. package/src/android/test/app/capgo/cordova/updater/DelayUpdateUtilsTest.java +177 -0
  53. package/src/android/test/app/capgo/cordova/updater/NativeContractTest.java +105 -0
  54. package/src/ios/AES.swift +216 -0
  55. package/src/ios/AppHealthTracker.swift +82 -0
  56. package/src/ios/BundleInfo.swift +185 -0
  57. package/src/ios/BundleStatus.swift +127 -0
  58. package/src/ios/CapgoRawRsa.swift +36 -0
  59. package/src/ios/CapgoUpdater.swift +3544 -0
  60. package/src/ios/CordovaCompat.swift +11 -0
  61. package/src/ios/CordovaPluginCall.swift +190 -0
  62. package/src/ios/CordovaUpdaterCommands.swift +270 -0
  63. package/src/ios/CordovaUpdaterPlugin.swift +4706 -0
  64. package/src/ios/CryptoCipher.swift +326 -0
  65. package/src/ios/DelayCondition.swift +74 -0
  66. package/src/ios/DelayUntilNext.swift +30 -0
  67. package/src/ios/DelayUpdateUtils.swift +257 -0
  68. package/src/ios/DeviceIdHelper.swift +120 -0
  69. package/src/ios/Info.plist +28 -0
  70. package/src/ios/InternalUtils.swift +471 -0
  71. package/src/ios/Logger.swift +283 -0
  72. package/src/ios/RSA.swift +96 -0
  73. package/src/ios/ShakeMenu.swift +52 -0
  74. package/src/ios/UpdaterSchemeHandler.swift +81 -0
  75. package/src/ios/UserDefaultsExtension.swift +46 -0
  76. package/src/ios/WebViewStatsReporter.swift +303 -0
@@ -0,0 +1,120 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ */
6
+
7
+ import Foundation
8
+ import Security
9
+
10
+ /**
11
+ * Helper class to manage device ID persistence across app installations.
12
+ * Uses iOS Keychain to persist the device ID.
13
+ */
14
+ class DeviceIdHelper {
15
+ private static let keychainService = "app.capgo.updater"
16
+ private static let keychainAccount = "deviceId"
17
+ private static let legacyDefaultsKey = "appUUID"
18
+
19
+ /**
20
+ * Gets or creates a device ID that persists across reinstalls.
21
+ *
22
+ * This method:
23
+ * 1. First checks for an existing ID in Keychain (persists across reinstalls)
24
+ * 2. Falls back to UserDefaults (for migration from older versions)
25
+ * 3. Generates a new UUID if neither exists
26
+ * 4. Stores the ID in Keychain for future use
27
+ *
28
+ * @return Device ID as a lowercase UUID string
29
+ */
30
+ static func getOrCreateDeviceId() -> String {
31
+ // Try to get device ID from Keychain first
32
+ if let keychainDeviceId = getDeviceIdFromKeychain() {
33
+ return keychainDeviceId.lowercased()
34
+ }
35
+
36
+ // Migration: Check UserDefaults for existing device ID
37
+ var deviceId = UserDefaults.standard.string(forKey: legacyDefaultsKey)
38
+
39
+ if deviceId == nil || deviceId!.isEmpty {
40
+ // Generate new device ID if none exists
41
+ deviceId = UUID().uuidString
42
+ }
43
+
44
+ // Ensure lowercase for consistency
45
+ deviceId = deviceId!.lowercased()
46
+
47
+ // Save to Keychain for persistence across reinstalls
48
+ saveDeviceIdToKeychain(deviceId: deviceId!)
49
+
50
+ return deviceId!
51
+ }
52
+
53
+ /**
54
+ * Retrieves the device ID from iOS Keychain.
55
+ *
56
+ * @return Device ID string or nil if not found
57
+ */
58
+ private static func getDeviceIdFromKeychain() -> String? {
59
+ let query: [String: Any] = [
60
+ kSecClass as String: kSecClassGenericPassword,
61
+ kSecAttrService as String: keychainService,
62
+ kSecAttrAccount as String: keychainAccount,
63
+ kSecReturnData as String: true,
64
+ kSecMatchLimit as String: kSecMatchLimitOne
65
+ ]
66
+
67
+ var result: AnyObject?
68
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
69
+
70
+ guard status == errSecSuccess,
71
+ let data = result as? Data,
72
+ let deviceId = String(data: data, encoding: .utf8) else {
73
+ return nil
74
+ }
75
+
76
+ return deviceId
77
+ }
78
+
79
+ /**
80
+ * Saves the device ID to iOS Keychain with appropriate accessibility settings.
81
+ *
82
+ * Uses kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly:
83
+ * - Data persists across reinstalls
84
+ * - Data is NOT synced to iCloud
85
+ * - Data is accessible after first device unlock
86
+ * - Data stays on this device only (privacy-friendly)
87
+ *
88
+ * @param deviceId The device ID to save
89
+ */
90
+ private static func saveDeviceIdToKeychain(deviceId: String) {
91
+ guard let data = deviceId.data(using: .utf8) else {
92
+ return
93
+ }
94
+
95
+ // Delete any existing entry first
96
+ let deleteQuery: [String: Any] = [
97
+ kSecClass as String: kSecClassGenericPassword,
98
+ kSecAttrService as String: keychainService,
99
+ kSecAttrAccount as String: keychainAccount
100
+ ]
101
+ SecItemDelete(deleteQuery as CFDictionary)
102
+
103
+ // Add new entry with appropriate accessibility
104
+ let addQuery: [String: Any] = [
105
+ kSecClass as String: kSecClassGenericPassword,
106
+ kSecAttrService as String: keychainService,
107
+ kSecAttrAccount as String: keychainAccount,
108
+ kSecValueData as String: data,
109
+ // This ensures data persists across reinstalls but stays on device (not synced)
110
+ kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
111
+ ]
112
+
113
+ let status = SecItemAdd(addQuery as CFDictionary, nil)
114
+
115
+ if status != errSecSuccess {
116
+ // Log error but don't crash - we'll fall back to UserDefaults on next launch
117
+ print("Failed to save device ID to Keychain: \(status)")
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,28 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleDevelopmentRegion</key>
6
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
7
+ <key>CFBundleExecutable</key>
8
+ <string>$(EXECUTABLE_NAME)</string>
9
+ <key>CFBundleIdentifier</key>
10
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11
+ <key>CFBundleInfoDictionaryVersion</key>
12
+ <string>6.0</string>
13
+ <key>CFBundleName</key>
14
+ <string>$(PRODUCT_NAME)</string>
15
+ <key>CFBundlePackageType</key>
16
+ <string>FMWK</string>
17
+ <key>CFBundleShortVersionString</key>
18
+ <string>1.0</string>
19
+ <key>CFBundleVersion</key>
20
+ <string>$(CURRENT_PROJECT_VERSION)</string>
21
+ <key>NSPrincipalClass</key>
22
+ <string></string>
23
+ <key>UIFileSharingEnabled</key>
24
+ <true/>
25
+ <key>LSSupportsOpeningDocumentsInPlace</key>
26
+ <true/>
27
+ </dict>
28
+ </plist>
@@ -0,0 +1,471 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ */
6
+
7
+ import Foundation
8
+
9
+ extension Collection {
10
+ subscript(safe index: Index) -> Element? {
11
+ return indices.contains(index) ? self[index] : nil
12
+ }
13
+ }
14
+ extension URL {
15
+ var isDirectory: Bool {
16
+ (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
17
+ }
18
+ var exist: Bool {
19
+ return FileManager().fileExists(atPath: self.path)
20
+ }
21
+ }
22
+ struct SetChannelDec: Decodable {
23
+ let status: String?
24
+ let error: String?
25
+ let message: String?
26
+ let unset: Bool?
27
+ }
28
+ public class SetChannel: NSObject {
29
+ var status: String = ""
30
+ var error: String = ""
31
+ var message: String = ""
32
+ }
33
+ extension SetChannel {
34
+ func toDict() -> [String: Any] {
35
+ var dict: [String: Any] = [String: Any]()
36
+ let otherSelf: Mirror = Mirror(reflecting: self)
37
+ for child: Mirror.Child in otherSelf.children {
38
+ if let key: String = child.label {
39
+ dict[key] = child.value
40
+ }
41
+ }
42
+ return dict
43
+ }
44
+ }
45
+ struct GetChannelDec: Decodable {
46
+ let channel: String?
47
+ let status: String?
48
+ let error: String?
49
+ let message: String?
50
+ let allowSet: Bool?
51
+ }
52
+ public class GetChannel: NSObject {
53
+ var channel: String = ""
54
+ var status: String = ""
55
+ var error: String = ""
56
+ var message: String = ""
57
+ var allowSet: Bool = true
58
+ }
59
+ extension GetChannel {
60
+ func toDict() -> [String: Any] {
61
+ var dict: [String: Any] = [String: Any]()
62
+ let otherSelf: Mirror = Mirror(reflecting: self)
63
+ for child: Mirror.Child in otherSelf.children {
64
+ if let key: String = child.label {
65
+ dict[key] = child.value
66
+ }
67
+ }
68
+ return dict
69
+ }
70
+ }
71
+ // swiftlint:disable identifier_name
72
+ struct ChannelInfo: Codable {
73
+ let id: Int
74
+ let name: String?
75
+ let `public`: Bool?
76
+ let allow_self_set: Bool?
77
+ }
78
+ // swiftlint:enable identifier_name
79
+ struct ListChannelsDec: Decodable {
80
+ let channels: [ChannelInfo]?
81
+ let error: String?
82
+
83
+ init(from decoder: Decoder) throws {
84
+ let container = try decoder.singleValueContainer()
85
+
86
+ if let channelsArray = try? container.decode([ChannelInfo].self) {
87
+ // Backend returns direct array
88
+ self.channels = channelsArray
89
+ self.error = nil
90
+ } else {
91
+ // Handle error response
92
+ let errorContainer = try decoder.container(keyedBy: CodingKeys.self)
93
+ self.channels = nil
94
+ self.error = try? errorContainer.decode(String.self, forKey: .error)
95
+ }
96
+ }
97
+
98
+ private enum CodingKeys: String, CodingKey {
99
+ case error
100
+ }
101
+ }
102
+ public class ListChannels: NSObject {
103
+ var channels: [[String: Any]] = []
104
+ var error: String = ""
105
+ }
106
+ extension ListChannels {
107
+ func toDict() -> [String: Any] {
108
+ var dict: [String: Any] = [String: Any]()
109
+ let otherSelf: Mirror = Mirror(reflecting: self)
110
+ for child: Mirror.Child in otherSelf.children {
111
+ if let key: String = child.label {
112
+ dict[key] = child.value
113
+ }
114
+ }
115
+ return dict
116
+ }
117
+ }
118
+ // swiftlint:disable identifier_name
119
+ struct InfoObject: Codable {
120
+ let platform: String?
121
+ let device_id: String?
122
+ let app_id: String?
123
+ let custom_id: String?
124
+ let version_build: String?
125
+ let version_code: String?
126
+ let version_os: String?
127
+ var version_name: String?
128
+ var old_version_name: String?
129
+ let plugin_version: String?
130
+ let is_emulator: Bool?
131
+ let is_prod: Bool?
132
+ let installSource: String?
133
+ var action: String?
134
+ var channel: String?
135
+ var defaultChannel: String?
136
+ var key_id: String?
137
+
138
+ enum CodingKeys: String, CodingKey {
139
+ case platform
140
+ case device_id
141
+ case app_id
142
+ case custom_id
143
+ case version_build
144
+ case version_code
145
+ case version_os
146
+ case version_name
147
+ case old_version_name
148
+ case plugin_version
149
+ case is_emulator
150
+ case is_prod
151
+ case installSource = "install_source"
152
+ case action
153
+ case channel
154
+ case defaultChannel
155
+ case key_id
156
+ }
157
+ }
158
+
159
+ extension InfoObject {
160
+ func toParameters() -> [String: Any] {
161
+ var parameters: [String: Any] = [:]
162
+ func set(_ key: String, _ value: Any?) {
163
+ guard let value = value else {
164
+ return
165
+ }
166
+ parameters[key] = value
167
+ }
168
+ set("platform", platform)
169
+ set("device_id", device_id)
170
+ set("app_id", app_id)
171
+ set("custom_id", custom_id)
172
+ set("version_build", version_build)
173
+ set("version_code", version_code)
174
+ set("version_os", version_os)
175
+ set("version_name", version_name)
176
+ set("old_version_name", old_version_name)
177
+ set("plugin_version", plugin_version)
178
+ set("is_emulator", is_emulator)
179
+ set("is_prod", is_prod)
180
+ set("install_source", installSource)
181
+ set("action", action)
182
+ set("channel", channel)
183
+ set("defaultChannel", defaultChannel)
184
+ set("key_id", key_id)
185
+ return parameters
186
+ }
187
+ }
188
+ // swiftlint:enable identifier_name
189
+
190
+ // swiftlint:disable identifier_name
191
+ struct StatsEvent: Codable {
192
+ let platform: String?
193
+ let device_id: String?
194
+ let app_id: String?
195
+ let custom_id: String?
196
+ let version_build: String?
197
+ let version_code: String?
198
+ let version_os: String?
199
+ let version_name: String?
200
+ let old_version_name: String?
201
+ let plugin_version: String?
202
+ let is_emulator: Bool?
203
+ let is_prod: Bool?
204
+ let installSource: String?
205
+ let action: String?
206
+ let channel: String?
207
+ let defaultChannel: String?
208
+ let key_id: String?
209
+ let metadata: [String: String]?
210
+ let timestamp: Int64
211
+
212
+ enum CodingKeys: String, CodingKey {
213
+ case platform
214
+ case device_id
215
+ case app_id
216
+ case custom_id
217
+ case version_build
218
+ case version_code
219
+ case version_os
220
+ case version_name
221
+ case old_version_name
222
+ case plugin_version
223
+ case is_emulator
224
+ case is_prod
225
+ case installSource = "install_source"
226
+ case action
227
+ case channel
228
+ case defaultChannel
229
+ case key_id
230
+ case metadata
231
+ case timestamp
232
+ }
233
+ }
234
+ // swiftlint:enable identifier_name
235
+
236
+ // swiftlint:disable identifier_name
237
+ public struct ManifestEntry: Codable {
238
+ let file_name: String?
239
+ let file_hash: String?
240
+ let download_url: String?
241
+ }
242
+ // swiftlint:enable identifier_name
243
+
244
+ extension ManifestEntry {
245
+ func toDict() -> [String: Any] {
246
+ var dict: [String: Any] = [String: Any]()
247
+ let mirror = Mirror(reflecting: self)
248
+ for child in mirror.children {
249
+ if let key = child.label {
250
+ dict[key] = child.value
251
+ }
252
+ }
253
+ return dict
254
+ }
255
+ }
256
+
257
+ // swiftlint:disable identifier_name
258
+ struct AppVersionDec: Decodable {
259
+ let version: String?
260
+ let checksum: String?
261
+ let url: String?
262
+ let message: String?
263
+ let error: String?
264
+ let kind: String?
265
+ let session_key: String?
266
+ let major: Bool?
267
+ let breaking: Bool?
268
+ let data: [String: String]?
269
+ let manifest: [ManifestEntry]?
270
+ let link: String?
271
+ let comment: String?
272
+ // The HTTP status code is captured separately in CapgoUpdater; this struct only mirrors JSON.
273
+ }
274
+ // swiftlint:enable identifier_name
275
+
276
+ public class AppVersion: NSObject {
277
+ var version: String = ""
278
+ var checksum: String = ""
279
+ var url: String = ""
280
+ var message: String?
281
+ var error: String?
282
+ var kind: String?
283
+ var sessionKey: String?
284
+ var major: Bool?
285
+ var breaking: Bool?
286
+ var data: [String: String]?
287
+ var manifest: [ManifestEntry]?
288
+ var missing: [String: Any]?
289
+ var downloadSize: [String: Any]?
290
+ var link: String?
291
+ var comment: String?
292
+ var statusCode: Int = 0
293
+ }
294
+
295
+ extension AppVersion {
296
+ func toDict() -> [String: Any] {
297
+ var dict: [String: Any] = [String: Any]()
298
+ let otherSelf: Mirror = Mirror(reflecting: self)
299
+ for child: Mirror.Child in otherSelf.children {
300
+ if let key: String = child.label {
301
+ if key == "manifest", let manifestEntries = child.value as? [ManifestEntry] {
302
+ dict[key] = manifestEntries.map { $0.toDict() }
303
+ } else {
304
+ dict[key] = child.value
305
+ }
306
+ }
307
+ }
308
+ return dict
309
+ }
310
+ }
311
+
312
+ extension OperatingSystemVersion {
313
+ func getFullVersion(separator: String = ".") -> String {
314
+ return "\(majorVersion)\(separator)\(minorVersion)\(separator)\(patchVersion)"
315
+ }
316
+ }
317
+ extension Bundle {
318
+ var versionName: String? {
319
+ return infoDictionary?["CFBundleShortVersionString"] as? String
320
+ }
321
+ var versionCode: String? {
322
+ return infoDictionary?["CFBundleVersion"] as? String
323
+ }
324
+ }
325
+
326
+ extension ISO8601DateFormatter {
327
+ convenience init(_ formatOptions: Options) {
328
+ self.init()
329
+ self.formatOptions = formatOptions
330
+ }
331
+ }
332
+ extension Formatter {
333
+ static let iso8601withFractionalSeconds: ISO8601DateFormatter = ISO8601DateFormatter(
334
+ [.withInternetDateTime, .withFractionalSeconds])
335
+ }
336
+ extension Date {
337
+ var iso8601withFractionalSeconds: String { return Formatter.iso8601withFractionalSeconds.string(from: self) }
338
+ }
339
+ extension String {
340
+
341
+ var fileURL: URL {
342
+ return URL(fileURLWithPath: self)
343
+ }
344
+
345
+ var lastPathComponent: String {
346
+ get {
347
+ return fileURL.lastPathComponent
348
+ }
349
+ }
350
+ var iso8601withFractionalSeconds: Date? {
351
+ return Formatter.iso8601withFractionalSeconds.date(from: self)
352
+ }
353
+ func trim(using characterSet: CharacterSet = .whitespacesAndNewlines) -> String {
354
+ return trimmingCharacters(in: characterSet)
355
+ }
356
+ }
357
+
358
+ enum CustomError: Error {
359
+ // Throw when an unzip fail
360
+ case cannotUnzip
361
+ case cannotWrite
362
+ case cannotDecode
363
+ case cannotUnflat
364
+ case cannotCreateDirectory
365
+ case cannotDeleteDirectory
366
+ case cannotDecryptSessionKey
367
+ case invalidBase64
368
+ case insufficientDiskSpace
369
+
370
+ // Throw in all other cases
371
+ case unexpected(code: Int)
372
+ }
373
+
374
+ extension CustomError: LocalizedError {
375
+ public var errorDescription: String? {
376
+ switch self {
377
+ case .cannotUnzip:
378
+ return NSLocalizedString(
379
+ "The file cannot be unzip",
380
+ comment: "Invalid zip"
381
+ )
382
+ case .cannotCreateDirectory:
383
+ return NSLocalizedString(
384
+ "The folder cannot be created",
385
+ comment: "Invalid folder"
386
+ )
387
+ case .cannotDeleteDirectory:
388
+ return NSLocalizedString(
389
+ "The folder cannot be deleted",
390
+ comment: "Invalid folder"
391
+ )
392
+ case .cannotUnflat:
393
+ return NSLocalizedString(
394
+ "The file cannot be unflat",
395
+ comment: "Invalid folder"
396
+ )
397
+ case .unexpected:
398
+ return NSLocalizedString(
399
+ "An unexpected error occurred.",
400
+ comment: "Unexpected Error"
401
+ )
402
+ case .cannotDecode:
403
+ return NSLocalizedString(
404
+ "Decoding the zip failed with this key",
405
+ comment: "Invalid public key"
406
+ )
407
+ case .cannotWrite:
408
+ return NSLocalizedString(
409
+ "Cannot write to the destination",
410
+ comment: "Invalid destination"
411
+ )
412
+ case .cannotDecryptSessionKey:
413
+ return NSLocalizedString(
414
+ "Decrypting the session key failed",
415
+ comment: "Invalid session key"
416
+ )
417
+ case .invalidBase64:
418
+ return NSLocalizedString(
419
+ "Decrypting the base64 failed",
420
+ comment: "Invalid checksum key"
421
+ )
422
+ case .insufficientDiskSpace:
423
+ return NSLocalizedString(
424
+ "Insufficient disk space for download",
425
+ comment: "Not enough storage"
426
+ )
427
+ }
428
+ }
429
+ }
430
+
431
+ /// Thread-safe atomic counter for concurrent operations
432
+ final class AtomicCounter {
433
+ private var value: Int = 0
434
+ private let lock = NSLock()
435
+
436
+ func increment() -> Int {
437
+ lock.lock()
438
+ defer { lock.unlock() }
439
+ value += 1
440
+ return value
441
+ }
442
+
443
+ var current: Int {
444
+ lock.lock()
445
+ defer { lock.unlock() }
446
+ return value
447
+ }
448
+ }
449
+
450
+ /// Thread-safe atomic boolean for concurrent operations
451
+ final class AtomicBool {
452
+ private var _value: Bool
453
+ private let lock = NSLock()
454
+
455
+ init(initialValue: Bool = false) {
456
+ _value = initialValue
457
+ }
458
+
459
+ var value: Bool {
460
+ get {
461
+ lock.lock()
462
+ defer { lock.unlock() }
463
+ return _value
464
+ }
465
+ set {
466
+ lock.lock()
467
+ defer { lock.unlock() }
468
+ _value = newValue
469
+ }
470
+ }
471
+ }