@capgo/capacitor-updater 6.11.2 → 6.12.0

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.
@@ -11,8 +11,6 @@ import ZipArchive
11
11
  import SSZipArchive
12
12
  #endif
13
13
  import Alamofire
14
- import zlib
15
- import CryptoKit
16
14
  import Compression
17
15
 
18
16
  #if canImport(ZipArchive)
@@ -21,257 +19,6 @@ typealias ZipArchiveHelper = ZipArchive
21
19
  typealias ZipArchiveHelper = SSZipArchive
22
20
  #endif
23
21
 
24
- extension Collection {
25
- subscript(safe index: Index) -> Element? {
26
- return indices.contains(index) ? self[index] : nil
27
- }
28
- }
29
- extension URL {
30
- var isDirectory: Bool {
31
- (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
32
- }
33
- var exist: Bool {
34
- return FileManager().fileExists(atPath: self.path)
35
- }
36
- }
37
- struct SetChannelDec: Decodable {
38
- let status: String?
39
- let error: String?
40
- let message: String?
41
- }
42
- public class SetChannel: NSObject {
43
- var status: String = ""
44
- var error: String = ""
45
- var message: String = ""
46
- }
47
- extension SetChannel {
48
- func toDict() -> [String: Any] {
49
- var dict: [String: Any] = [String: Any]()
50
- let otherSelf: Mirror = Mirror(reflecting: self)
51
- for child: Mirror.Child in otherSelf.children {
52
- if let key: String = child.label {
53
- dict[key] = child.value
54
- }
55
- }
56
- return dict
57
- }
58
- }
59
- struct GetChannelDec: Decodable {
60
- let channel: String?
61
- let status: String?
62
- let error: String?
63
- let message: String?
64
- let allowSet: Bool?
65
- }
66
- public class GetChannel: NSObject {
67
- var channel: String = ""
68
- var status: String = ""
69
- var error: String = ""
70
- var message: String = ""
71
- var allowSet: Bool = true
72
- }
73
- extension GetChannel {
74
- func toDict() -> [String: Any] {
75
- var dict: [String: Any] = [String: Any]()
76
- let otherSelf: Mirror = Mirror(reflecting: self)
77
- for child: Mirror.Child in otherSelf.children {
78
- if let key: String = child.label {
79
- dict[key] = child.value
80
- }
81
- }
82
- return dict
83
- }
84
- }
85
- struct InfoObject: Codable {
86
- let platform: String?
87
- let device_id: String?
88
- let app_id: String?
89
- let custom_id: String?
90
- let version_build: String?
91
- let version_code: String?
92
- let version_os: String?
93
- var version_name: String?
94
- var old_version_name: String?
95
- let plugin_version: String?
96
- let is_emulator: Bool?
97
- let is_prod: Bool?
98
- var action: String?
99
- var channel: String?
100
- var defaultChannel: String?
101
- }
102
-
103
- public struct ManifestEntry: Codable {
104
- let file_name: String?
105
- let file_hash: String?
106
- let download_url: String?
107
- }
108
-
109
- extension ManifestEntry {
110
- func toDict() -> [String: Any] {
111
- var dict: [String: Any] = [String: Any]()
112
- let mirror = Mirror(reflecting: self)
113
- for child in mirror.children {
114
- if let key = child.label {
115
- dict[key] = child.value
116
- }
117
- }
118
- return dict
119
- }
120
- }
121
-
122
- struct AppVersionDec: Decodable {
123
- let version: String?
124
- let checksum: String?
125
- let url: String?
126
- let message: String?
127
- let error: String?
128
- let session_key: String?
129
- let major: Bool?
130
- let data: [String: String]?
131
- let manifest: [ManifestEntry]?
132
- }
133
-
134
- public class AppVersion: NSObject {
135
- var version: String = ""
136
- var checksum: String = ""
137
- var url: String = ""
138
- var message: String?
139
- var error: String?
140
- var sessionKey: String?
141
- var major: Bool?
142
- var data: [String: String]?
143
- var manifest: [ManifestEntry]?
144
- }
145
-
146
- extension AppVersion {
147
- func toDict() -> [String: Any] {
148
- var dict: [String: Any] = [String: Any]()
149
- let otherSelf: Mirror = Mirror(reflecting: self)
150
- for child: Mirror.Child in otherSelf.children {
151
- if let key: String = child.label {
152
- if key == "manifest", let manifestEntries = child.value as? [ManifestEntry] {
153
- dict[key] = manifestEntries.map { $0.toDict() }
154
- } else {
155
- dict[key] = child.value
156
- }
157
- }
158
- }
159
- return dict
160
- }
161
- }
162
-
163
- extension OperatingSystemVersion {
164
- func getFullVersion(separator: String = ".") -> String {
165
- return "\(majorVersion)\(separator)\(minorVersion)\(separator)\(patchVersion)"
166
- }
167
- }
168
- extension Bundle {
169
- var versionName: String? {
170
- return infoDictionary?["CFBundleShortVersionString"] as? String
171
- }
172
- var versionCode: String? {
173
- return infoDictionary?["CFBundleVersion"] as? String
174
- }
175
- }
176
-
177
- extension ISO8601DateFormatter {
178
- convenience init(_ formatOptions: Options) {
179
- self.init()
180
- self.formatOptions = formatOptions
181
- }
182
- }
183
- extension Formatter {
184
- static let iso8601withFractionalSeconds: ISO8601DateFormatter = ISO8601DateFormatter([.withInternetDateTime, .withFractionalSeconds])
185
- }
186
- extension Date {
187
- var iso8601withFractionalSeconds: String { return Formatter.iso8601withFractionalSeconds.string(from: self) }
188
- }
189
- extension String {
190
-
191
- var fileURL: URL {
192
- return URL(fileURLWithPath: self)
193
- }
194
-
195
- var lastPathComponent: String {
196
- get {
197
- return fileURL.lastPathComponent
198
- }
199
- }
200
- var iso8601withFractionalSeconds: Date? {
201
- return Formatter.iso8601withFractionalSeconds.date(from: self)
202
- }
203
- func trim(using characterSet: CharacterSet = .whitespacesAndNewlines) -> String {
204
- return trimmingCharacters(in: characterSet)
205
- }
206
- }
207
-
208
- enum CustomError: Error {
209
- // Throw when an unzip fail
210
- case cannotUnzip
211
- case cannotWrite
212
- case cannotDecode
213
- case cannotUnflat
214
- case cannotCreateDirectory
215
- case cannotDeleteDirectory
216
- case cannotDecryptSessionKey
217
- case invalidBase64
218
-
219
- // Throw in all other cases
220
- case unexpected(code: Int)
221
- }
222
-
223
- extension CustomError: LocalizedError {
224
- public var errorDescription: String? {
225
- switch self {
226
- case .cannotUnzip:
227
- return NSLocalizedString(
228
- "The file cannot be unzip",
229
- comment: "Invalid zip"
230
- )
231
- case .cannotCreateDirectory:
232
- return NSLocalizedString(
233
- "The folder cannot be created",
234
- comment: "Invalid folder"
235
- )
236
- case .cannotDeleteDirectory:
237
- return NSLocalizedString(
238
- "The folder cannot be deleted",
239
- comment: "Invalid folder"
240
- )
241
- case .cannotUnflat:
242
- return NSLocalizedString(
243
- "The file cannot be unflat",
244
- comment: "Invalid folder"
245
- )
246
- case .unexpected:
247
- return NSLocalizedString(
248
- "An unexpected error occurred.",
249
- comment: "Unexpected Error"
250
- )
251
- case .cannotDecode:
252
- return NSLocalizedString(
253
- "Decoding the zip failed with this key",
254
- comment: "Invalid public key"
255
- )
256
- case .cannotWrite:
257
- return NSLocalizedString(
258
- "Cannot write to the destination",
259
- comment: "Invalid destination"
260
- )
261
- case .cannotDecryptSessionKey:
262
- return NSLocalizedString(
263
- "Decrypting the session key failed",
264
- comment: "Invalid session key"
265
- )
266
- case .invalidBase64:
267
- return NSLocalizedString(
268
- "Decrypting the base64 failed",
269
- comment: "Invalid checksum key"
270
- )
271
- }
272
- }
273
- }
274
-
275
22
  @objc public class CapacitorUpdater: NSObject {
276
23
 
277
24
  private let versionCode: String = Bundle.main.versionCode ?? ""
@@ -287,7 +34,7 @@ extension CustomError: LocalizedError {
287
34
  // Add this line to declare cacheFolder
288
35
  private let cacheFolder: URL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("capgo_downloads")
289
36
 
290
- public let TAG: String = "✨ Capacitor-updater:"
37
+ static public let TAG: String = "✨ Capacitor-updater:"
291
38
  public let CAP_SERVER_PATH: String = "serverBasePath"
292
39
  public var versionBuild: String = ""
293
40
  public var customId: String = ""
@@ -368,7 +115,7 @@ extension CustomError: LocalizedError {
368
115
  do {
369
116
  try FileManager.default.createDirectory(atPath: source.path, withIntermediateDirectories: true, attributes: nil)
370
117
  } catch {
371
- print("\(self.TAG) Cannot createDirectory \(source.path)")
118
+ print("\(CapacitorUpdater.TAG) Cannot createDirectory \(source.path)")
372
119
  throw CustomError.cannotCreateDirectory
373
120
  }
374
121
  }
@@ -378,7 +125,7 @@ extension CustomError: LocalizedError {
378
125
  do {
379
126
  try FileManager.default.removeItem(atPath: source.path)
380
127
  } catch {
381
- print("\(self.TAG) File not removed. \(source.path)")
128
+ print("\(CapacitorUpdater.TAG) File not removed. \(source.path)")
382
129
  throw CustomError.cannotDeleteDirectory
383
130
  }
384
131
  }
@@ -395,105 +142,14 @@ extension CustomError: LocalizedError {
395
142
  return false
396
143
  }
397
144
  } catch {
398
- print("\(self.TAG) File not moved. source: \(source.path) dest: \(dest.path)")
145
+ print("\(CapacitorUpdater.TAG) File not moved. source: \(source.path) dest: \(dest.path)")
399
146
  throw CustomError.cannotUnflat
400
147
  }
401
148
  }
402
149
 
403
- private func decryptFileV2(filePath: URL, sessionKey: String, version: String) throws {
404
- if self.publicKey.isEmpty || sessionKey.isEmpty || sessionKey.components(separatedBy: ":").count != 2 {
405
- print("\(self.TAG) Cannot find public key or sessionKey")
406
- return
407
- }
408
- do {
409
- guard let rsaPublicKey: RSAPublicKey = .load(rsaPublicKey: self.publicKey) else {
410
- print("cannot decode publicKey", self.publicKey)
411
- throw CustomError.cannotDecode
412
- }
413
-
414
- let sessionKeyArray: [String] = sessionKey.components(separatedBy: ":")
415
- guard let ivData: Data = Data(base64Encoded: sessionKeyArray[0]) else {
416
- print("cannot decode sessionKey", sessionKey)
417
- throw CustomError.cannotDecode
418
- }
419
-
420
- guard let sessionKeyDataEncrypted = Data(base64Encoded: sessionKeyArray[1]) else {
421
- throw NSError(domain: "Invalid session key data", code: 1, userInfo: nil)
422
- }
423
-
424
- guard let sessionKeyDataDecrypted = rsaPublicKey.decrypt(data: sessionKeyDataEncrypted) else {
425
- throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
426
- }
427
-
428
- let aesPrivateKey = AES128Key(iv: ivData, aes128Key: sessionKeyDataDecrypted)
429
-
430
- guard let encryptedData = try? Data(contentsOf: filePath) else {
431
- throw NSError(domain: "Failed to read encrypted data", code: 3, userInfo: nil)
432
- }
433
-
434
- guard let decryptedData = aesPrivateKey.decrypt(data: encryptedData) else {
435
- throw NSError(domain: "Failed to decrypt data", code: 4, userInfo: nil)
436
- }
437
-
438
- try decryptedData.write(to: filePath)
439
-
440
- } catch {
441
- print("\(self.TAG) Cannot decode: \(filePath.path)", error)
442
- self.sendStats(action: "decrypt_fail", versionName: version)
443
- throw CustomError.cannotDecode
444
- }
445
- }
446
-
447
- private func decryptFile(filePath: URL, sessionKey: String, version: String) throws {
448
- if self.privateKey.isEmpty {
449
- print("\(self.TAG) Cannot found privateKey")
450
- return
451
- } else if sessionKey.isEmpty || sessionKey.components(separatedBy: ":").count != 2 {
452
- print("\(self.TAG) Cannot found sessionKey")
453
- return
454
- }
455
- do {
456
- guard let rsaPrivateKey: RSAPrivateKey = .load(rsaPrivateKey: self.privateKey) else {
457
- print("cannot decode privateKey", self.privateKey)
458
- throw CustomError.cannotDecode
459
- }
460
-
461
- let sessionKeyArray: [String] = sessionKey.components(separatedBy: ":")
462
- guard let ivData: Data = Data(base64Encoded: sessionKeyArray[0]) else {
463
- print("cannot decode sessionKey", sessionKey)
464
- throw CustomError.cannotDecode
465
- }
466
-
467
- guard let sessionKeyDataEncrypted = Data(base64Encoded: sessionKeyArray[1]) else {
468
- throw NSError(domain: "Invalid session key data", code: 1, userInfo: nil)
469
- }
470
-
471
- guard let sessionKeyDataDecrypted = rsaPrivateKey.decrypt(data: sessionKeyDataEncrypted) else {
472
- throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
473
- }
474
-
475
- let aesPrivateKey = AES128Key(iv: ivData, aes128Key: sessionKeyDataDecrypted)
476
-
477
- guard let encryptedData = try? Data(contentsOf: filePath) else {
478
- throw NSError(domain: "Failed to read encrypted data", code: 3, userInfo: nil)
479
- }
480
-
481
- guard let decryptedData = aesPrivateKey.decrypt(data: encryptedData) else {
482
- throw NSError(domain: "Failed to decrypt data", code: 4, userInfo: nil)
483
- }
484
-
485
- try decryptedData.write(to: filePath)
486
-
487
- } catch {
488
- print("\(self.TAG) Cannot decode: \(filePath.path)", error)
489
- self.sendStats(action: "decrypt_fail", versionName: version)
490
- throw CustomError.cannotDecode
491
- }
492
- }
493
-
494
150
  private func unzipProgressHandler(entry: String, zipInfo: unz_file_info, entryNumber: Int, total: Int, destUnZip: URL, id: String, unzipError: inout NSError?) {
495
151
  if entry.contains("\\") {
496
- print("\(self.TAG) unzip: Windows path is not supported, please use unix path as required by zip RFC: \(entry)")
152
+ print("\(CapacitorUpdater.TAG) unzip: Windows path is not supported, please use unix path as required by zip RFC: \(entry)")
497
153
  self.sendStats(action: "windows_path_fail")
498
154
  }
499
155
 
@@ -596,7 +252,7 @@ extension CustomError: LocalizedError {
596
252
  if let channel = channel {
597
253
  parameters.defaultChannel = channel
598
254
  }
599
- print("\(self.TAG) Auto-update parameters: \(parameters)")
255
+ print("\(CapacitorUpdater.TAG) Auto-update parameters: \(parameters)")
600
256
  let request = AF.request(url, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
601
257
 
602
258
  request.validate().responseDecodable(of: AppVersionDec.self) { response in
@@ -630,7 +286,7 @@ extension CustomError: LocalizedError {
630
286
  latest.manifest = manifest
631
287
  }
632
288
  case let .failure(error):
633
- print("\(self.TAG) Error getting Latest", response.value ?? "", error )
289
+ print("\(CapacitorUpdater.TAG) Error getting Latest", response.value ?? "", error )
634
290
  latest.message = "Error getting Latest \(String(describing: response.value))"
635
291
  latest.error = "response_error"
636
292
  }
@@ -643,63 +299,7 @@ extension CustomError: LocalizedError {
643
299
  private func setCurrentBundle(bundle: String) {
644
300
  UserDefaults.standard.set(bundle, forKey: self.CAP_SERVER_PATH)
645
301
  UserDefaults.standard.synchronize()
646
- print("\(self.TAG) Current bundle set to: \((bundle ).isEmpty ? BundleInfo.ID_BUILTIN : bundle)")
647
- }
648
- private func calcChecksum(filePath: URL) -> String {
649
- let bufferSize = 1024 * 1024 * 5 // 5 MB
650
- var checksum = uLong(0)
651
-
652
- do {
653
- let fileHandle = try FileHandle(forReadingFrom: filePath)
654
- defer {
655
- fileHandle.closeFile()
656
- }
657
-
658
- while autoreleasepool(invoking: {
659
- let fileData = fileHandle.readData(ofLength: bufferSize)
660
- if fileData.count > 0 {
661
- checksum = fileData.withUnsafeBytes {
662
- crc32(checksum, $0.bindMemory(to: Bytef.self).baseAddress, uInt(fileData.count))
663
- }
664
- return true // Continue
665
- } else {
666
- return false // End of file
667
- }
668
- }) {}
669
-
670
- return String(format: "%08X", checksum).lowercased()
671
- } catch {
672
- print("\(self.TAG) Cannot get checksum: \(filePath.path)", error)
673
- return ""
674
- }
675
- }
676
-
677
- private func calcChecksumV2(filePath: URL) -> String {
678
- let bufferSize = 1024 * 1024 * 5 // 5 MB
679
- var sha256 = SHA256()
680
-
681
- do {
682
- let fileHandle = try FileHandle(forReadingFrom: filePath)
683
- defer {
684
- fileHandle.closeFile()
685
- }
686
-
687
- while autoreleasepool(invoking: {
688
- let fileData = fileHandle.readData(ofLength: bufferSize)
689
- if fileData.count > 0 {
690
- sha256.update(data: fileData)
691
- return true // Continue
692
- } else {
693
- return false // End of file
694
- }
695
- }) {}
696
-
697
- let digest = sha256.finalize()
698
- return digest.compactMap { String(format: "%02x", $0) }.joined()
699
- } catch {
700
- print("\(self.TAG) Cannot get checksum: \(filePath.path)", error)
701
- return ""
702
- }
302
+ print("\(CapacitorUpdater.TAG) Current bundle set to: \((bundle ).isEmpty ? BundleInfo.ID_BUILTIN : bundle)")
703
303
  }
704
304
 
705
305
  private var tempDataPath: URL {
@@ -711,35 +311,14 @@ extension CustomError: LocalizedError {
711
311
  }
712
312
  private var tempData = Data()
713
313
 
714
- public func decryptChecksum(checksum: String, version: String) throws -> String {
715
- if self.publicKey.isEmpty {
716
- return checksum
717
- }
718
- do {
719
- let checksumBytes: Data = Data(base64Encoded: checksum)!
720
- guard let rsaPublicKey: RSAPublicKey = .load(rsaPublicKey: self.publicKey) else {
721
- print("cannot decode publicKey", self.publicKey)
722
- throw CustomError.cannotDecode
723
- }
724
- guard let decryptedChecksum = try? rsaPublicKey.decrypt(data: checksumBytes) else {
725
- throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
726
- }
727
- return decryptedChecksum.base64EncodedString()
728
- } catch {
729
- print("\(self.TAG) Cannot decrypt checksum: \(checksum)", error)
730
- self.sendStats(action: "decrypt_fail", versionName: version)
731
- throw CustomError.cannotDecode
732
- }
733
- }
734
-
735
314
  private func verifyChecksum(file: URL, expectedHash: String) -> Bool {
736
- let actualHash = calcChecksumV2(filePath: file)
315
+ let actualHash = CryptoCipherV2.calcChecksum(filePath: file)
737
316
  return actualHash == expectedHash
738
317
  }
739
318
 
740
319
  public func downloadManifest(manifest: [ManifestEntry], version: String, sessionKey: String) throws -> BundleInfo {
741
320
  let id = self.randomString(length: 10)
742
- print("\(self.TAG) downloadManifest start \(id)")
321
+ print("\(CapacitorUpdater.TAG) downloadManifest start \(id)")
743
322
  let destFolder = self.getBundleDirectory(id: id)
744
323
  let builtinFolder = Bundle.main.bundleURL.appendingPathComponent("public")
745
324
 
@@ -761,10 +340,19 @@ extension CustomError: LocalizedError {
761
340
 
762
341
  for entry in manifest {
763
342
  guard let fileName = entry.file_name,
764
- let fileHash = entry.file_hash,
343
+ var fileHash = entry.file_hash,
765
344
  let downloadUrl = entry.download_url else {
766
345
  continue
767
346
  }
347
+
348
+ if (!self.hasOldPrivateKeyPropertyInConfig && !self.publicKey.isEmpty && !sessionKey.isEmpty) {
349
+ do {
350
+ fileHash = try CryptoCipherV2.decryptChecksum(checksum: fileHash, publicKey: self.publicKey, version: version)
351
+ } catch {
352
+ downloadError = error
353
+ print("\(CapacitorUpdater.TAG) CryptoCipherV2.decryptChecksum error \(id) \(fileName) error: \(error)")
354
+ }
355
+ }
768
356
 
769
357
  let fileNameWithoutPath = (fileName as NSString).lastPathComponent
770
358
  let cacheFileName = "\(fileHash)_\(fileNameWithoutPath)"
@@ -779,13 +367,13 @@ extension CustomError: LocalizedError {
779
367
 
780
368
  if FileManager.default.fileExists(atPath: builtinFilePath.path) && verifyChecksum(file: builtinFilePath, expectedHash: fileHash) {
781
369
  try FileManager.default.copyItem(at: builtinFilePath, to: destFilePath)
782
- print("\(self.TAG) downloadManifest \(fileName) using builtin file \(id)")
370
+ print("\(CapacitorUpdater.TAG) downloadManifest \(fileName) using builtin file \(id)")
783
371
  completedFiles += 1
784
372
  self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
785
373
  dispatchGroup.leave()
786
374
  } else if FileManager.default.fileExists(atPath: cacheFilePath.path) && verifyChecksum(file: cacheFilePath, expectedHash: fileHash) {
787
375
  try FileManager.default.copyItem(at: cacheFilePath, to: destFilePath)
788
- print("\(self.TAG) downloadManifest \(fileName) copy from cache \(id)")
376
+ print("\(CapacitorUpdater.TAG) downloadManifest \(fileName) copy from cache \(id)")
789
377
  completedFiles += 1
790
378
  self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
791
379
  dispatchGroup.leave()
@@ -797,25 +385,51 @@ extension CustomError: LocalizedError {
797
385
  switch response.result {
798
386
  case .success(let data):
799
387
  do {
388
+ // Add decryption step if public key is set and sessionKey is provided
389
+ var finalData = data
390
+ if !self.publicKey.isEmpty && !sessionKey.isEmpty {
391
+ let tempFile = self.cacheFolder.appendingPathComponent("temp_\(UUID().uuidString)")
392
+ try finalData.write(to: tempFile)
393
+ do {
394
+ try CryptoCipherV2.decryptFile(filePath: tempFile, publicKey: self.publicKey, sessionKey: sessionKey, version: version)
395
+ } catch {
396
+ self.sendStats(action: "decrypt_fail", versionName: version)
397
+ throw error
398
+ }
399
+ // TODO: try and do self.sendStats(action: "decrypt_fail", versionName: version) if fail
400
+ finalData = try Data(contentsOf: tempFile)
401
+ try FileManager.default.removeItem(at: tempFile)
402
+ }
403
+
800
404
  // Decompress the Brotli data
801
- guard let decompressedData = self.decompressBrotli(data: data) else {
405
+ guard let decompressedData = self.decompressBrotli(data: finalData) else {
802
406
  throw NSError(domain: "BrotliDecompressionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to decompress Brotli data"])
803
407
  }
804
- // Save decompressed data to cache
805
- try decompressedData.write(to: cacheFilePath)
806
- // Save decompressed data to destination
807
- try decompressedData.write(to: destFilePath)
408
+ finalData = decompressedData
409
+
410
+ try finalData.write(to: destFilePath)
411
+ if (!self.hasOldPrivateKeyPropertyInConfig && !self.publicKey.isEmpty && !sessionKey.isEmpty) {
412
+ // assume that calcChecksum != null
413
+ let calculatedChecksum = CryptoCipherV2.calcChecksum(filePath: destFilePath)
414
+ if (calculatedChecksum != fileHash) {
415
+ throw NSError(domain: "ChecksumError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Computed checksum is not equal to required checksum (\(calculatedChecksum) != \(fileHash))"])
416
+ }
417
+ }
418
+
419
+ // Save decrypted data to cache and destination
420
+ try finalData.write(to: cacheFilePath)
421
+
808
422
 
809
423
  completedFiles += 1
810
424
  self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
811
- print("\(self.TAG) downloadManifest \(id) \(fileName) downloaded, decompressed, and cached")
425
+ print("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) downloaded, decompressed, decrypted, and cached")
812
426
  } catch {
813
427
  downloadError = error
814
- print("\(self.TAG) downloadManifest \(id) \(fileName) error: \(error)")
428
+ print("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) error: \(error)")
815
429
  }
816
430
  case .failure(let error):
817
431
  downloadError = error
818
- print("\(self.TAG) downloadManifest \(id) \(fileName) download error: \(error)")
432
+ print("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) download error: \(error)")
819
433
  }
820
434
  }
821
435
  }
@@ -834,7 +448,7 @@ extension CustomError: LocalizedError {
834
448
  let updatedBundle = bundleInfo.setStatus(status: BundleStatus.PENDING.localizedString)
835
449
  self.saveBundleInfo(id: id, bundle: updatedBundle)
836
450
 
837
- print("\(self.TAG) downloadManifest done \(id)")
451
+ print("\(CapacitorUpdater.TAG) downloadManifest done \(id)")
838
452
  return updatedBundle
839
453
  }
840
454
 
@@ -846,7 +460,7 @@ extension CustomError: LocalizedError {
846
460
  let streamPointer = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1)
847
461
  var status = compression_stream_init(streamPointer, COMPRESSION_STREAM_DECODE, COMPRESSION_BROTLI)
848
462
  guard status != COMPRESSION_STATUS_ERROR else {
849
- print("\(self.TAG) Unable to initialize the decompression stream.")
463
+ print("\(CapacitorUpdater.TAG) Unable to initialize the decompression stream.")
850
464
  return nil
851
465
  }
852
466
 
@@ -868,7 +482,7 @@ extension CustomError: LocalizedError {
868
482
  if let baseAddress = rawBufferPointer.baseAddress {
869
483
  streamPointer.pointee.src_ptr = baseAddress.assumingMemoryBound(to: UInt8.self)
870
484
  } else {
871
- print("\(self.TAG) Error: Unable to get base address of input data")
485
+ print("\(CapacitorUpdater.TAG) Error: Unable to get base address of input data")
872
486
  status = COMPRESSION_STATUS_ERROR
873
487
  return
874
488
  }
@@ -889,7 +503,7 @@ extension CustomError: LocalizedError {
889
503
  if status == COMPRESSION_STATUS_END {
890
504
  break
891
505
  } else if status == COMPRESSION_STATUS_ERROR {
892
- print("\(self.TAG) Error during Brotli decompression")
506
+ print("\(CapacitorUpdater.TAG) Error during Brotli decompression")
893
507
  return nil
894
508
  }
895
509
 
@@ -927,7 +541,7 @@ extension CustomError: LocalizedError {
927
541
  let monitor = ClosureEventMonitor()
928
542
  monitor.requestDidCompleteTaskWithError = { (_, _, error) in
929
543
  if error != nil {
930
- print("\(self.TAG) Downloading failed - ClosureEventMonitor activated")
544
+ print("\(CapacitorUpdater.TAG) Downloading failed - ClosureEventMonitor activated")
931
545
  mainError = error as NSError?
932
546
  }
933
547
  }
@@ -958,11 +572,11 @@ extension CustomError: LocalizedError {
958
572
  }
959
573
 
960
574
  } else {
961
- print("\(self.TAG) Download failed")
575
+ print("\(CapacitorUpdater.TAG) Download failed")
962
576
  }
963
577
 
964
578
  case .complete:
965
- print("\(self.TAG) Download complete, total received bytes: \(totalReceivedBytes)")
579
+ print("\(CapacitorUpdater.TAG) Download complete, total received bytes: \(totalReceivedBytes)")
966
580
  self.notifyDownload(id: id, percent: 70, ignoreMultipleOfTen: true)
967
581
  semaphore.signal()
968
582
  }
@@ -984,7 +598,7 @@ extension CustomError: LocalizedError {
984
598
  reachabilityManager?.stopListening()
985
599
 
986
600
  if mainError != nil {
987
- print("\(self.TAG) Failed to download: \(String(describing: mainError))")
601
+ print("\(CapacitorUpdater.TAG) Failed to download: \(String(describing: mainError))")
988
602
  self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
989
603
  throw mainError!
990
604
  }
@@ -992,14 +606,19 @@ extension CustomError: LocalizedError {
992
606
  let finalPath = tempDataPath.deletingLastPathComponent().appendingPathComponent("\(id)")
993
607
  do {
994
608
  var checksumDecrypted = checksum
995
- if !self.hasOldPrivateKeyPropertyInConfig {
996
- try self.decryptFileV2(filePath: tempDataPath, sessionKey: sessionKey, version: version)
997
- } else {
998
- try self.decryptFile(filePath: tempDataPath, sessionKey: sessionKey, version: version)
609
+ do {
610
+ if !self.hasOldPrivateKeyPropertyInConfig {
611
+ try CryptoCipherV2.decryptFile(filePath: tempDataPath, publicKey: self.publicKey, sessionKey: sessionKey, version: version)
612
+ } else {
613
+ try CryptoCipher.decryptFile(filePath: tempDataPath, privateKey: self.privateKey, sessionKey: sessionKey, version: version)
614
+ }
615
+ } catch {
616
+ self.sendStats(action: "decrypt_fail", versionName: version)
617
+ throw error
999
618
  }
1000
619
  try FileManager.default.moveItem(at: tempDataPath, to: finalPath)
1001
620
  } catch {
1002
- print("\(self.TAG) Failed decrypt file : \(error)")
621
+ print("\(CapacitorUpdater.TAG) Failed decrypt file : \(error)")
1003
622
  self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
1004
623
  cleanDownloadData()
1005
624
  throw error
@@ -1007,15 +626,15 @@ extension CustomError: LocalizedError {
1007
626
 
1008
627
  do {
1009
628
  if !self.hasOldPrivateKeyPropertyInConfig && !sessionKey.isEmpty {
1010
- checksum = self.calcChecksumV2(filePath: finalPath)
629
+ checksum = CryptoCipherV2.calcChecksum(filePath: finalPath)
1011
630
  } else {
1012
- checksum = self.calcChecksum(filePath: finalPath)
631
+ checksum = CryptoCipher.calcChecksum(filePath: finalPath)
1013
632
  }
1014
- print("\(self.TAG) Downloading: 80% (unzipping)")
633
+ print("\(CapacitorUpdater.TAG) Downloading: 80% (unzipping)")
1015
634
  try self.saveDownloaded(sourceZip: finalPath, id: id, base: self.libraryDir.appendingPathComponent(self.bundleDirectory), notify: true)
1016
635
 
1017
636
  } catch {
1018
- print("\(self.TAG) Failed to unzip file: \(error)")
637
+ print("\(CapacitorUpdater.TAG) Failed to unzip file: \(error)")
1019
638
  self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
1020
639
  cleanDownloadData()
1021
640
  // todo: cleanup zip attempts
@@ -1023,25 +642,25 @@ extension CustomError: LocalizedError {
1023
642
  }
1024
643
 
1025
644
  self.notifyDownload(id: id, percent: 90)
1026
- print("\(self.TAG) Downloading: 90% (wrapping up)")
645
+ print("\(CapacitorUpdater.TAG) Downloading: 90% (wrapping up)")
1027
646
  let info = BundleInfo(id: id, version: version, status: BundleStatus.PENDING, downloaded: Date(), checksum: checksum)
1028
647
  self.saveBundleInfo(id: id, bundle: info)
1029
648
  self.cleanDownloadData()
1030
649
  self.notifyDownload(id: id, percent: 100)
1031
- print("\(self.TAG) Downloading: 100% (complete)")
650
+ print("\(CapacitorUpdater.TAG) Downloading: 100% (complete)")
1032
651
  return info
1033
652
  }
1034
653
  private func ensureResumableFilesExist() {
1035
654
  let fileManager = FileManager.default
1036
655
  if !fileManager.fileExists(atPath: tempDataPath.path) {
1037
656
  if !fileManager.createFile(atPath: tempDataPath.path, contents: Data()) {
1038
- print("\(self.TAG) Cannot ensure that a file at \(tempDataPath.path) exists")
657
+ print("\(CapacitorUpdater.TAG) Cannot ensure that a file at \(tempDataPath.path) exists")
1039
658
  }
1040
659
  }
1041
660
 
1042
661
  if !fileManager.fileExists(atPath: updateInfo.path) {
1043
662
  if !fileManager.createFile(atPath: updateInfo.path, contents: Data()) {
1044
- print("\(self.TAG) Cannot ensure that a file at \(updateInfo.path) exists")
663
+ print("\(CapacitorUpdater.TAG) Cannot ensure that a file at \(updateInfo.path) exists")
1045
664
  }
1046
665
  }
1047
666
  }
@@ -1053,7 +672,7 @@ extension CustomError: LocalizedError {
1053
672
  do {
1054
673
  try fileManager.removeItem(at: tempDataPath)
1055
674
  } catch {
1056
- print("\(self.TAG) Could not delete file at \(tempDataPath): \(error)")
675
+ print("\(CapacitorUpdater.TAG) Could not delete file at \(tempDataPath): \(error)")
1057
676
  }
1058
677
  }
1059
678
  // Deleting update.dat
@@ -1061,7 +680,7 @@ extension CustomError: LocalizedError {
1061
680
  do {
1062
681
  try fileManager.removeItem(at: updateInfo)
1063
682
  } catch {
1064
- print("\(self.TAG) Could not delete file at \(updateInfo): \(error)")
683
+ print("\(CapacitorUpdater.TAG) Could not delete file at \(updateInfo): \(error)")
1065
684
  }
1066
685
  }
1067
686
  }
@@ -1089,7 +708,7 @@ extension CustomError: LocalizedError {
1089
708
  do {
1090
709
  try "\(version)".write(to: updateInfo, atomically: true, encoding: .utf8)
1091
710
  } catch {
1092
- print("\(self.TAG) Failed to save progress: \(error)")
711
+ print("\(CapacitorUpdater.TAG) Failed to save progress: \(error)")
1093
712
  }
1094
713
  }
1095
714
  private func getLocalUpdateVersion() -> String { // Return the version that was tried to be downloaded on last download attempt
@@ -1111,7 +730,7 @@ extension CustomError: LocalizedError {
1111
730
  return fileSize.int64Value
1112
731
  }
1113
732
  } catch {
1114
- print("\(self.TAG) Could not retrieve already downloaded data size : \(error)")
733
+ print("\(CapacitorUpdater.TAG) Could not retrieve already downloaded data size : \(error)")
1115
734
  }
1116
735
  return 0
1117
736
  }
@@ -1121,7 +740,7 @@ extension CustomError: LocalizedError {
1121
740
  do {
1122
741
  let files: [String] = try FileManager.default.contentsOfDirectory(atPath: dest.path)
1123
742
  var res: [BundleInfo] = []
1124
- print("\(self.TAG) list File : \(dest.path)")
743
+ print("\(CapacitorUpdater.TAG) list File : \(dest.path)")
1125
744
  if dest.exist {
1126
745
  for id: String in files {
1127
746
  res.append(self.getBundleInfo(id: id))
@@ -1129,7 +748,7 @@ extension CustomError: LocalizedError {
1129
748
  }
1130
749
  return res
1131
750
  } catch {
1132
- print("\(self.TAG) No version available \(dest.path)")
751
+ print("\(CapacitorUpdater.TAG) No version available \(dest.path)")
1133
752
  return []
1134
753
  }
1135
754
  }
@@ -1137,7 +756,7 @@ extension CustomError: LocalizedError {
1137
756
  public func delete(id: String, removeInfo: Bool) -> Bool {
1138
757
  let deleted: BundleInfo = self.getBundleInfo(id: id)
1139
758
  if deleted.isBuiltin() || self.getCurrentBundleId() == id {
1140
- print("\(self.TAG) Cannot delete \(id)")
759
+ print("\(CapacitorUpdater.TAG) Cannot delete \(id)")
1141
760
  return false
1142
761
  }
1143
762
 
@@ -1146,7 +765,7 @@ extension CustomError: LocalizedError {
1146
765
  !next.isDeleted() &&
1147
766
  !next.isErrorStatus() &&
1148
767
  next.getId() == id {
1149
- print("\(self.TAG) Cannot delete the next bundle \(id)")
768
+ print("\(CapacitorUpdater.TAG) Cannot delete the next bundle \(id)")
1150
769
  return false
1151
770
  }
1152
771
 
@@ -1154,7 +773,7 @@ extension CustomError: LocalizedError {
1154
773
  do {
1155
774
  try FileManager.default.removeItem(atPath: destPersist.path)
1156
775
  } catch {
1157
- print("\(self.TAG) Folder \(destPersist.path), not removed.")
776
+ print("\(CapacitorUpdater.TAG) Folder \(destPersist.path), not removed.")
1158
777
  return false
1159
778
  }
1160
779
  if removeInfo {
@@ -1162,7 +781,7 @@ extension CustomError: LocalizedError {
1162
781
  } else {
1163
782
  self.saveBundleInfo(id: id, bundle: deleted.setStatus(status: BundleStatus.DELETED.localizedString))
1164
783
  }
1165
- print("\(self.TAG) bundle delete \(deleted.getVersionName())")
784
+ print("\(CapacitorUpdater.TAG) bundle delete \(deleted.getVersionName())")
1166
785
  self.sendStats(action: "delete", versionName: deleted.getVersionName())
1167
786
  return true
1168
787
  }
@@ -1215,7 +834,7 @@ extension CustomError: LocalizedError {
1215
834
  public func autoReset() {
1216
835
  let currentBundle: BundleInfo = self.getCurrentBundle()
1217
836
  if !currentBundle.isBuiltin() && !self.bundleExists(id: currentBundle.getId()) {
1218
- print("\(self.TAG) Folder at bundle path does not exist. Triggering reset.")
837
+ print("\(CapacitorUpdater.TAG) Folder at bundle path does not exist. Triggering reset.")
1219
838
  self.reset()
1220
839
  }
1221
840
  }
@@ -1225,7 +844,7 @@ extension CustomError: LocalizedError {
1225
844
  }
1226
845
 
1227
846
  public func reset(isInternal: Bool) {
1228
- print("\(self.TAG) reset: \(isInternal)")
847
+ print("\(CapacitorUpdater.TAG) reset: \(isInternal)")
1229
848
  let currentBundleName = self.getCurrentBundle().getVersionName()
1230
849
  self.setCurrentBundle(bundle: "")
1231
850
  self.setFallbackBundle(fallback: Optional<BundleInfo>.none)
@@ -1238,14 +857,14 @@ extension CustomError: LocalizedError {
1238
857
  public func setSuccess(bundle: BundleInfo, autoDeletePrevious: Bool) {
1239
858
  self.setBundleStatus(id: bundle.getId(), status: BundleStatus.SUCCESS)
1240
859
  let fallback: BundleInfo = self.getFallbackBundle()
1241
- print("\(self.TAG) Fallback bundle is: \(fallback.toString())")
1242
- print("\(self.TAG) Version successfully loaded: \(bundle.toString())")
860
+ print("\(CapacitorUpdater.TAG) Fallback bundle is: \(fallback.toString())")
861
+ print("\(CapacitorUpdater.TAG) Version successfully loaded: \(bundle.toString())")
1243
862
  if autoDeletePrevious && !fallback.isBuiltin() && fallback.getId() != bundle.getId() {
1244
863
  let res = self.delete(id: fallback.getId())
1245
864
  if res {
1246
- print("\(self.TAG) Deleted previous bundle: \(fallback.toString())")
865
+ print("\(CapacitorUpdater.TAG) Deleted previous bundle: \(fallback.toString())")
1247
866
  } else {
1248
- print("\(self.TAG) Failed to delete previous bundle: \(fallback.toString())")
867
+ print("\(CapacitorUpdater.TAG) Failed to delete previous bundle: \(fallback.toString())")
1249
868
  }
1250
869
  }
1251
870
  self.setFallbackBundle(fallback: bundle)
@@ -1258,7 +877,7 @@ extension CustomError: LocalizedError {
1258
877
  func unsetChannel() -> SetChannel {
1259
878
  let setChannel: SetChannel = SetChannel()
1260
879
  if (self.channelUrl ).isEmpty {
1261
- print("\(self.TAG) Channel URL is not set")
880
+ print("\(CapacitorUpdater.TAG) Channel URL is not set")
1262
881
  setChannel.message = "Channel URL is not set"
1263
882
  setChannel.error = "missing_config"
1264
883
  return setChannel
@@ -1281,7 +900,7 @@ extension CustomError: LocalizedError {
1281
900
  setChannel.message = message
1282
901
  }
1283
902
  case let .failure(error):
1284
- print("\(self.TAG) Error unset Channel", response.value ?? "", error)
903
+ print("\(CapacitorUpdater.TAG) Error unset Channel", response.value ?? "", error)
1285
904
  setChannel.message = "Error unset Channel \(String(describing: response.value))"
1286
905
  setChannel.error = "response_error"
1287
906
  }
@@ -1294,7 +913,7 @@ extension CustomError: LocalizedError {
1294
913
  func setChannel(channel: String) -> SetChannel {
1295
914
  let setChannel: SetChannel = SetChannel()
1296
915
  if (self.channelUrl ).isEmpty {
1297
- print("\(self.TAG) Channel URL is not set")
916
+ print("\(CapacitorUpdater.TAG) Channel URL is not set")
1298
917
  setChannel.message = "Channel URL is not set"
1299
918
  setChannel.error = "missing_config"
1300
919
  return setChannel
@@ -1318,7 +937,7 @@ extension CustomError: LocalizedError {
1318
937
  setChannel.message = message
1319
938
  }
1320
939
  case let .failure(error):
1321
- print("\(self.TAG) Error set Channel", response.value ?? "", error)
940
+ print("\(CapacitorUpdater.TAG) Error set Channel", response.value ?? "", error)
1322
941
  setChannel.message = "Error set Channel \(String(describing: response.value))"
1323
942
  setChannel.error = "response_error"
1324
943
  }
@@ -1331,7 +950,7 @@ extension CustomError: LocalizedError {
1331
950
  func getChannel() -> GetChannel {
1332
951
  let getChannel: GetChannel = GetChannel()
1333
952
  if (self.channelUrl ).isEmpty {
1334
- print("\(self.TAG) Channel URL is not set")
953
+ print("\(CapacitorUpdater.TAG) Channel URL is not set")
1335
954
  getChannel.message = "Channel URL is not set"
1336
955
  getChannel.error = "missing_config"
1337
956
  return getChannel
@@ -1370,7 +989,7 @@ extension CustomError: LocalizedError {
1370
989
  }
1371
990
  }
1372
991
 
1373
- print("\(self.TAG) Error get Channel", response.value ?? "", error)
992
+ print("\(CapacitorUpdater.TAG) Error get Channel", response.value ?? "", error)
1374
993
  getChannel.message = "Error get Channel \(String(describing: response.value)))"
1375
994
  getChannel.error = "response_error"
1376
995
  }
@@ -1405,9 +1024,9 @@ extension CustomError: LocalizedError {
1405
1024
  ).responseData { response in
1406
1025
  switch response.result {
1407
1026
  case .success:
1408
- print("\(self.TAG) Stats sent for \(action), version \(versionName)")
1027
+ print("\(CapacitorUpdater.TAG) Stats sent for \(action), version \(versionName)")
1409
1028
  case let .failure(error):
1410
- print("\(self.TAG) Error sending stats: ", response.value ?? "", error.localizedDescription)
1029
+ print("\(CapacitorUpdater.TAG) Error sending stats: ", response.value ?? "", error.localizedDescription)
1411
1030
  }
1412
1031
  semaphore.signal()
1413
1032
  }
@@ -1422,7 +1041,7 @@ extension CustomError: LocalizedError {
1422
1041
  if id != nil {
1423
1042
  trueId = id!
1424
1043
  }
1425
- // print("\(self.TAG) Getting info for bundle [\(trueId)]")
1044
+ // print("\(CapacitorUpdater.TAG) Getting info for bundle [\(trueId)]")
1426
1045
  let result: BundleInfo
1427
1046
  if BundleInfo.ID_BUILTIN == trueId {
1428
1047
  result = BundleInfo(id: trueId, version: "", status: BundleStatus.SUCCESS, checksum: "")
@@ -1432,11 +1051,11 @@ extension CustomError: LocalizedError {
1432
1051
  do {
1433
1052
  result = try UserDefaults.standard.getObj(forKey: "\(trueId)\(self.INFO_SUFFIX)", castTo: BundleInfo.self)
1434
1053
  } catch {
1435
- print("\(self.TAG) Failed to parse info for bundle [\(trueId)]", error.localizedDescription)
1054
+ print("\(CapacitorUpdater.TAG) Failed to parse info for bundle [\(trueId)]", error.localizedDescription)
1436
1055
  result = BundleInfo(id: trueId, version: "", status: BundleStatus.PENDING, checksum: "")
1437
1056
  }
1438
1057
  }
1439
- // print("\(self.TAG) Returning info bundle [\(result.toString())]")
1058
+ // print("\(CapacitorUpdater.TAG) Returning info bundle [\(result.toString())]")
1440
1059
  return result
1441
1060
  }
1442
1061
 
@@ -1456,26 +1075,26 @@ extension CustomError: LocalizedError {
1456
1075
 
1457
1076
  public func saveBundleInfo(id: String, bundle: BundleInfo?) {
1458
1077
  if bundle != nil && (bundle!.isBuiltin() || bundle!.isUnknown()) {
1459
- print("\(self.TAG) Not saving info for bundle [\(id)]", bundle?.toString() ?? "")
1078
+ print("\(CapacitorUpdater.TAG) Not saving info for bundle [\(id)]", bundle?.toString() ?? "")
1460
1079
  return
1461
1080
  }
1462
1081
  if bundle == nil {
1463
- print("\(self.TAG) Removing info for bundle [\(id)]")
1082
+ print("\(CapacitorUpdater.TAG) Removing info for bundle [\(id)]")
1464
1083
  UserDefaults.standard.removeObject(forKey: "\(id)\(self.INFO_SUFFIX)")
1465
1084
  } else {
1466
1085
  let update = bundle!.setId(id: id)
1467
- print("\(self.TAG) Storing info for bundle [\(id)]", update.toString())
1086
+ print("\(CapacitorUpdater.TAG) Storing info for bundle [\(id)]", update.toString())
1468
1087
  do {
1469
1088
  try UserDefaults.standard.setObj(update, forKey: "\(id)\(self.INFO_SUFFIX)")
1470
1089
  } catch {
1471
- print("\(self.TAG) Failed to save info for bundle [\(id)]", error.localizedDescription)
1090
+ print("\(CapacitorUpdater.TAG) Failed to save info for bundle [\(id)]", error.localizedDescription)
1472
1091
  }
1473
1092
  }
1474
1093
  UserDefaults.standard.synchronize()
1475
1094
  }
1476
1095
 
1477
1096
  private func setBundleStatus(id: String, status: BundleStatus) {
1478
- print("\(self.TAG) Setting status for bundle [\(id)] to \(status)")
1097
+ print("\(CapacitorUpdater.TAG) Setting status for bundle [\(id)] to \(status)")
1479
1098
  let info = self.getBundleInfo(id: id)
1480
1099
  self.saveBundleInfo(id: id, bundle: info.setStatus(status: status.localizedString))
1481
1100
  }