@capgo/cordova-updater 8.1.4 → 8.2.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 (36) hide show
  1. package/dist/esm/definitions.d.ts +116 -86
  2. package/dist/esm/definitions.js +2 -2
  3. package/dist/esm/definitions.js.map +1 -1
  4. package/dist/esm/exec.js +11 -1
  5. package/dist/esm/exec.js.map +1 -1
  6. package/dist/esm/history.js +7 -7
  7. package/dist/esm/history.js.map +1 -1
  8. package/dist/plugin.cjs.js +13 -3
  9. package/dist/plugin.cjs.js.map +1 -1
  10. package/dist/plugin.js +13 -3
  11. package/dist/plugin.js.map +1 -1
  12. package/package.json +5 -3
  13. package/plugin.xml +9 -9
  14. package/src/android/CordovaUpdaterPlugin.java +173 -5
  15. package/src/android/app/capgo/cordova/updater/CapgoUpdater.java +313 -55
  16. package/src/android/app/capgo/cordova/updater/CryptoCipher.java +51 -18
  17. package/src/android/app/capgo/cordova/updater/DelayUpdateUtils.java +0 -1
  18. package/src/android/app/capgo/cordova/updater/DownloadService.java +402 -89
  19. package/src/android/app/capgo/cordova/updater/Version.java +312 -0
  20. package/src/android/capgo-cordova-updater.gradle +3 -5
  21. package/src/android/test/app/capgo/cordova/updater/BackgroundDownloadSettlementTest.java +29 -0
  22. package/src/android/test/app/capgo/cordova/updater/CapacitorUpdaterUnitTest.java.skip +2 -12
  23. package/src/android/test/app/capgo/cordova/updater/DelayUpdateUtilsTest.java +0 -1
  24. package/src/android/test/app/capgo/cordova/updater/RsaContractTest.java +137 -0
  25. package/src/android/test/app/capgo/cordova/updater/SecurityHardeningTest.java +197 -0
  26. package/src/android/test/app/capgo/cordova/updater/SessionKeyRequiredTest.java +344 -0
  27. package/src/ios/CapgoSemanticVersion.swift +181 -0
  28. package/src/ios/CapgoUpdater.swift +427 -114
  29. package/src/ios/CordovaUpdaterPlugin.swift +171 -40
  30. package/src/ios/CryptoCipher.swift +41 -38
  31. package/src/ios/DelayUpdateUtils.swift +19 -45
  32. package/src/ios/Logger.swift +81 -5
  33. package/src/ios/RedirectPolicyDelegate.swift +53 -0
  34. package/src/ios/WebViewStatsReporter.swift +5 -0
  35. package/src/ios/ZipArchiveReader.swift +263 -0
  36. package/src/ios/ZipCentralDirectory.swift +283 -0
@@ -10,7 +10,6 @@ import Cordova
10
10
  #endif
11
11
  import UIKit
12
12
  import WebKit
13
- import Version
14
13
 
15
14
  /**
16
15
  * Please read the Capacitor iOS Plugin Development Guide
@@ -88,7 +87,7 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
88
87
  }()
89
88
 
90
89
  public var implementation = CapgoUpdater()
91
- private let pluginVersion: String = "8.1.4"
90
+ private let pluginVersion: String = "8.2.1"
92
91
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
93
92
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
94
93
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
@@ -139,7 +138,7 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
139
138
  // Note: DELAY_CONDITION_PREFERENCES is now defined in DelayUpdateUtils.DELAY_CONDITION_PREFERENCES
140
139
  private var updateUrl = ""
141
140
  private var backgroundTaskID: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid
142
- private var currentVersionNative: Version = "0.0.0"
141
+ private var currentVersionNative = CapgoSemanticVersion(major: 0, minor: 0, patch: 0)
143
142
  private var currentBuildVersion: String = "0"
144
143
  private var autoUpdate = false
145
144
  private var autoUpdateMode = CordovaUpdaterPlugin.autoUpdateModeOff
@@ -200,6 +199,8 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
200
199
  private var isLeavingPreviewForIncomingLink = false
201
200
  private var previewTransitionClearWorkItem: DispatchWorkItem?
202
201
  let semaphoreReady = DispatchSemaphore(value: 0)
202
+ private var readyGuardArmed = false
203
+ private var readyGeneration = 0
203
204
 
204
205
  private var delayUpdateUtils: DelayUpdateUtils!
205
206
 
@@ -243,7 +244,7 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
243
244
  fatalError("Cannot get version name")
244
245
  }
245
246
  do {
246
- currentVersionNative = try Version(versionName)
247
+ currentVersionNative = try CapgoSemanticVersion(versionName)
247
248
  } catch {
248
249
  logger.error("Cannot parse versionName \(versionName)")
249
250
  }
@@ -285,6 +286,7 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
285
286
  periodCheckDelay = Self.normalizedPeriodCheckDelaySeconds(readConfigInt("periodCheckDelay", 0))
286
287
 
287
288
  implementation.setPublicKey(readConfigString("publicKey") ?? "")
289
+ implementation.allowHttpsToHttpRedirect = readConfigBool("allowHttpsToHttpRedirect", false)
288
290
  implementation.notifyDownloadRaw = notifyDownload
289
291
  implementation.notifyListeners = { [weak self] eventName, data in
290
292
  let emit = {
@@ -473,13 +475,21 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
473
475
  )
474
476
  }
475
477
 
476
- private func syncKeepUrlPathFlag(enabled: Bool) {
477
- let script: String
478
+ private func keepUrlPathFlagScriptSource(enabled: Bool) -> String {
478
479
  if enabled {
479
- script = "(function(){ try { localStorage.setItem('\(keepUrlPathFlagKey)', '1'); } catch (err) {} window.__capgoKeepUrlPathAfterReload = true; var evt; try { evt = new CustomEvent('CapacitorUpdaterKeepUrlPathAfterReload', { detail: { enabled: true } }); } catch (e) { evt = document.createEvent('CustomEvent'); evt.initCustomEvent('CapacitorUpdaterKeepUrlPathAfterReload', false, false, { enabled: true }); } window.dispatchEvent(evt); })();"
480
- } else {
481
- script = "(function(){ try { localStorage.removeItem('\(keepUrlPathFlagKey)'); } catch (err) {} delete window.__capgoKeepUrlPathAfterReload; var evt; try { evt = new CustomEvent('CapacitorUpdaterKeepUrlPathAfterReload', { detail: { enabled: false } }); } catch (e) { evt = document.createEvent('CustomEvent'); evt.initCustomEvent('CapacitorUpdaterKeepUrlPathAfterReload', false, false, { enabled: false }); } window.dispatchEvent(evt); })();"
480
+ return "(function(){ try { localStorage.setItem('\(keepUrlPathFlagKey)', '1'); } catch (err) {} window.__capgoKeepUrlPathAfterReload = true; var evt; try { evt = new CustomEvent('CapacitorUpdaterKeepUrlPathAfterReload', { detail: { enabled: true } }); } catch (e) { evt = document.createEvent('CustomEvent'); evt.initCustomEvent('CapacitorUpdaterKeepUrlPathAfterReload', false, false, { enabled: true }); } window.dispatchEvent(evt); })();"
482
481
  }
482
+ return "(function(){ try { localStorage.removeItem('\(keepUrlPathFlagKey)'); } catch (err) {} delete window.__capgoKeepUrlPathAfterReload; var evt; try { evt = new CustomEvent('CapacitorUpdaterKeepUrlPathAfterReload', { detail: { enabled: false } }); } catch (e) { evt = document.createEvent('CustomEvent'); evt.initCustomEvent('CapacitorUpdaterKeepUrlPathAfterReload', false, false, { enabled: false }); } window.dispatchEvent(evt); })();"
483
+ }
484
+
485
+ private func addKeepUrlPathDocumentStartScript(to controller: WKUserContentController, enabled: Bool) {
486
+ let userScript = WKUserScript(source: keepUrlPathFlagScriptSource(enabled: enabled), injectionTime: .atDocumentStart, forMainFrameOnly: true)
487
+ controller.addUserScript(userScript)
488
+ self.keepUrlPathFlagLastValue = enabled
489
+ }
490
+
491
+ private func syncKeepUrlPathFlag(enabled: Bool) {
492
+ let script = keepUrlPathFlagScriptSource(enabled: enabled)
483
493
  DispatchQueue.main.async { [weak self] in
484
494
  guard let self = self, let webView = self.updaterWebView else {
485
495
  return
@@ -545,8 +555,10 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
545
555
  var dest: URL
546
556
  if BundleInfo.ID_BUILTIN == id {
547
557
  dest = Bundle.main.resourceURL!.appendingPathComponent("public")
558
+ } else if let bundleDir = try? self.implementation.getBundleDirectory(id: id) {
559
+ dest = bundleDir
548
560
  } else {
549
- dest = self.implementation.getBundleDirectory(id: id)
561
+ dest = Bundle.main.resourceURL!.appendingPathComponent("public")
550
562
  }
551
563
 
552
564
  if !FileManager.default.fileExists(atPath: dest.path) {
@@ -1594,7 +1606,19 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
1594
1606
  throw makePreviewError("Invalid download URL")
1595
1607
  }
1596
1608
 
1609
+ if !self.implementation.publicKey.isEmpty && !CryptoCipher.isValidSessionKey(sessionKey) {
1610
+ self.logger.error("Public key present but no valid session key provided")
1611
+ self.implementation.sendStats(action: "session_key_required", versionName: version)
1612
+ throw makePreviewError("Session key required when public key is present")
1613
+ }
1614
+
1597
1615
  var checksum = rawChecksum
1616
+ if manifestEntries == nil && checksum.isEmpty {
1617
+ self.logger.error("No checksum provided")
1618
+ self.implementation.sendStats(action: "checksum_required", versionName: version)
1619
+ throw makePreviewError("Checksum required")
1620
+ }
1621
+
1598
1622
  let next: BundleInfo
1599
1623
  if let manifestEntries = manifestEntries {
1600
1624
  next = try self.implementation.downloadManifest(manifest: manifestEntries, version: version, sessionKey: sessionKey)
@@ -1602,29 +1626,20 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
1602
1626
  next = try self.implementation.download(url: url, version: version, sessionKey: sessionKey)
1603
1627
  }
1604
1628
 
1605
- if self.implementation.publicKey != "" && checksum == "" {
1606
- self.logger.error("Public key present but no checksum provided")
1607
- self.implementation.sendStats(action: "checksum_required", versionName: next.getVersionName())
1608
- let id = next.getId()
1609
- let resDel = self.implementation.delete(id: id)
1610
- if !resDel {
1611
- self.logger.error("Delete failed, id \(id) doesn't exist")
1612
- }
1613
- throw ObjectSavableError.checksum
1614
- }
1615
-
1616
- checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
1617
- CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
1618
- CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
1619
- if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
1620
- self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
1621
- self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
1622
- let id = next.getId()
1623
- let resDel = self.implementation.delete(id: id)
1624
- if !resDel {
1625
- self.logger.error("Delete failed, id \(id) doesn't exist")
1629
+ if manifestEntries == nil {
1630
+ checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
1631
+ CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
1632
+ CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
1633
+ if next.getChecksum() != checksum {
1634
+ self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
1635
+ self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
1636
+ let id = next.getId()
1637
+ let resDel = self.implementation.delete(id: id)
1638
+ if !resDel {
1639
+ self.logger.error("Delete failed, id \(id) doesn't exist")
1640
+ }
1641
+ throw ObjectSavableError.checksum
1626
1642
  }
1627
- throw ObjectSavableError.checksum
1628
1643
  }
1629
1644
 
1630
1645
  self.logger.info("Good checksum \(next.getChecksum()) \(checksum)")
@@ -1676,8 +1691,10 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
1676
1691
  let id = self.implementation.getCurrentBundleId()
1677
1692
  if BundleInfo.ID_BUILTIN == id {
1678
1693
  return Bundle.main.resourceURL!.appendingPathComponent("public")
1694
+ } else if let bundleDir = try? self.implementation.getBundleDirectory(id: id) {
1695
+ return bundleDir
1679
1696
  } else {
1680
- return self.implementation.getBundleDirectory(id: id)
1697
+ return Bundle.main.resourceURL!.appendingPathComponent("public")
1681
1698
  }
1682
1699
  }
1683
1700
 
@@ -1685,6 +1702,7 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
1685
1702
  let id = self.implementation.getCurrentBundleId()
1686
1703
  let dest = self.currentReloadDestination()
1687
1704
  logger.info("Reloading \(id)")
1705
+ self.armReadyGuard(webView: self.updaterWebView)
1688
1706
 
1689
1707
  if self.keepUrlPathAfterReload {
1690
1708
  self.syncKeepUrlPathFlag(enabled: true)
@@ -2889,11 +2907,19 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
2889
2907
  self.saveCallForAsyncHandling(call)
2890
2908
  DispatchQueue.global(qos: .utility).async {
2891
2909
  let configDefaultChannel = self.readConfigString("defaultChannel", "")!
2892
- let res = self.implementation.unsetChannel(defaultChannelKey: self.defaultChannelDefaultsKey, configDefaultChannel: configDefaultChannel)
2910
+ let res = self.implementation.unsetChannel(
2911
+ defaultChannelKey: self.defaultChannelDefaultsKey,
2912
+ configDefaultChannel: configDefaultChannel,
2913
+ allowSetDefaultChannel: self.allowSetDefaultChannel
2914
+ )
2893
2915
  if res.error != "" {
2894
- self.rejectCall(call, message: res.error, code: "UNSETCHANNEL_FAILED", data: [
2895
- "message": res.error,
2896
- "error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
2916
+ let errorCode = res.error == "disabled_by_config"
2917
+ ? "disabled_by_config"
2918
+ : (res.error.contains("Channel URL") ? "missing_config" : "request_failed")
2919
+ let message = res.message.isEmpty ? res.error : res.message
2920
+ self.rejectCall(call, message: message, code: "UNSETCHANNEL_FAILED", data: [
2921
+ "message": message,
2922
+ "error": errorCode
2897
2923
  ])
2898
2924
  } else {
2899
2925
  guard self.persistDefaultChannelStateFromDefaults() else {
@@ -3116,7 +3142,90 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
3116
3142
  ])
3117
3143
  }
3118
3144
 
3145
+ static func shouldAcceptReadyCall(guardArmed: Bool, expectedGeneration: Int, reportedGeneration: Int?) -> Bool {
3146
+ if !guardArmed {
3147
+ return true
3148
+ }
3149
+ guard let reportedGeneration else {
3150
+ return false
3151
+ }
3152
+ return reportedGeneration == expectedGeneration
3153
+ }
3154
+
3155
+ static func readyGenerationScript(_ generation: Int) -> String {
3156
+ return "(function(){window.__CAPGO_READY_GEN=\(generation);})();"
3157
+ }
3158
+
3159
+ static func readyGenerationBootstrapScript(_ generation: Int) -> String {
3160
+ return """
3161
+ (function(){
3162
+ var slot = { value: \(generation) };
3163
+ window.__capgoReadyGenSlot = slot;
3164
+ Object.defineProperty(window, '__CAPGO_READY_GEN', { get: function() { return slot.value; }, configurable: true });
3165
+ })();
3166
+ """
3167
+ }
3168
+
3169
+ private func reinstallDocumentStartUserScripts(webView: WKWebView, readyGeneration: Int) {
3170
+ let controller = webView.configuration.userContentController
3171
+ controller.removeAllUserScripts()
3172
+ self.keepUrlPathFlagLastValue = nil
3173
+ let readyScript = WKUserScript(
3174
+ source: CordovaUpdaterPlugin.readyGenerationBootstrapScript(readyGeneration),
3175
+ injectionTime: .atDocumentStart,
3176
+ forMainFrameOnly: true
3177
+ )
3178
+ controller.addUserScript(readyScript)
3179
+ if self.keepUrlPathAfterReload {
3180
+ self.addKeepUrlPathDocumentStartScript(to: controller, enabled: true)
3181
+ }
3182
+ self.webViewStatsReporter?.reinstallDocumentStartScript(on: webView)
3183
+ }
3184
+
3185
+ private func armReadyGuard(webView: WKWebView?) {
3186
+ self.readyGeneration += 1
3187
+ let generation = self.readyGeneration
3188
+ self.readyGuardArmed = true
3189
+ guard let webView else {
3190
+ logger.warn("Cannot stamp notifyAppReady generation without a webview")
3191
+ self.readyGuardArmed = false
3192
+ return
3193
+ }
3194
+ let installScripts = {
3195
+ self.reinstallDocumentStartUserScripts(webView: webView, readyGeneration: generation)
3196
+ }
3197
+ if Thread.isMainThread {
3198
+ installScripts()
3199
+ } else {
3200
+ DispatchQueue.main.sync(execute: installScripts)
3201
+ }
3202
+ }
3203
+
3204
+ private func reportedReadyGeneration(_ call: CAPPluginCall) -> Int? {
3205
+ if let value = call.getValue("loadGeneration") as? Int {
3206
+ return value
3207
+ }
3208
+ if let number = call.getValue("loadGeneration") as? NSNumber {
3209
+ return number.intValue
3210
+ }
3211
+ return nil
3212
+ }
3213
+
3214
+ private func acceptsReadyCall(_ call: CAPPluginCall) -> Bool {
3215
+ let reported = self.readyGuardArmed ? self.reportedReadyGeneration(call) : nil
3216
+ return CordovaUpdaterPlugin.shouldAcceptReadyCall(
3217
+ guardArmed: self.readyGuardArmed,
3218
+ expectedGeneration: self.readyGeneration,
3219
+ reportedGeneration: reported
3220
+ )
3221
+ }
3222
+
3119
3223
  func notifyAppReady(_ call: CAPPluginCall) {
3224
+ if !self.acceptsReadyCall(call) {
3225
+ logger.info("Ignoring notifyAppReady from a page that is no longer current")
3226
+ call.resolve(["bundle": self.implementation.getCurrentBundle().toJSON()])
3227
+ return
3228
+ }
3120
3229
  self.semaphoreDown()
3121
3230
  let bundle = self.implementation.getCurrentBundle()
3122
3231
  self.implementation.setSuccess(bundle: bundle, autoDeletePrevious: self.autoDeletePrevious)
@@ -4123,6 +4232,28 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
4123
4232
  if latestVersionName != "" && current.getVersionName() != latestVersionName {
4124
4233
  do {
4125
4234
  self.logger.info("New bundle: \(latestVersionName) found. Current is: \(current.getVersionName()). \(messageUpdate)")
4235
+ if !self.implementation.publicKey.isEmpty && !CryptoCipher.isValidSessionKey(sessionKey) {
4236
+ self.logger.error("Public key present but no valid session key provided")
4237
+ self.implementation.sendStats(action: "session_key_required", versionName: latestVersionName)
4238
+ self.endBackGroundTaskWithNotif(
4239
+ msg: "Session key required when public key is present",
4240
+ latestVersionName: latestVersionName,
4241
+ current: current,
4242
+ plannedDirectUpdate: plannedDirectUpdate
4243
+ )
4244
+ return
4245
+ }
4246
+ if res.manifest == nil && (res.checksum ?? "").isEmpty {
4247
+ self.logger.error("No checksum provided")
4248
+ self.implementation.sendStats(action: "checksum_required", versionName: latestVersionName)
4249
+ self.endBackGroundTaskWithNotif(
4250
+ msg: "Checksum required",
4251
+ latestVersionName: latestVersionName,
4252
+ current: current,
4253
+ plannedDirectUpdate: plannedDirectUpdate
4254
+ )
4255
+ return
4256
+ }
4126
4257
  var nextImpl = self.implementation.getBundleInfoByVersionName(version: latestVersionName)
4127
4258
  if nextImpl == nil || nextImpl?.isDeleted() == true {
4128
4259
  if nextImpl?.isDeleted() == true {
@@ -4169,7 +4300,7 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
4169
4300
  res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
4170
4301
  CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
4171
4302
  CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: res.checksum)
4172
- if res.checksum != "" && next.getChecksum() != res.checksum && res.manifest == nil {
4303
+ if res.manifest == nil && next.getChecksum() != res.checksum {
4173
4304
  self.logger.error("Error checksum \(next.getChecksum()) \(res.checksum)")
4174
4305
  self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
4175
4306
  let id = next.getId()
@@ -4584,8 +4715,8 @@ public class CordovaUpdaterPlugin: CDVPlugin, CDVPluginSchemeHandler {
4584
4715
  // Determine update availability by comparing versions
4585
4716
  if let availableVersion = availableVersion {
4586
4717
  do {
4587
- let currentVer = try Version(currentVersionName)
4588
- let availableVer = try Version(availableVersion)
4718
+ let currentVer = try CapgoSemanticVersion(currentVersionName)
4719
+ let availableVer = try CapgoSemanticVersion(availableVersion)
4589
4720
  if availableVer > currentVer {
4590
4721
  result["updateAvailability"] = AppUpdateAvailability.updateAvailable.rawValue
4591
4722
  } else {
@@ -160,50 +160,46 @@ public struct CryptoCipher {
160
160
  public static func calcChecksum(filePath: URL) -> String {
161
161
  let bufferSize = checksumBufferBytes()
162
162
  var sha256 = SHA256()
163
+ var readFailed = false
163
164
 
165
+ let fileHandle: FileHandle
164
166
  do {
165
- let fileHandle: FileHandle
167
+ fileHandle = try FileHandle(forReadingFrom: filePath)
168
+ } catch {
169
+ logger.error("Cannot open file for checksum calculation")
170
+ logger.debug("Path: \(filePath.path), Error: \(error)")
171
+ return ""
172
+ }
173
+
174
+ defer {
166
175
  do {
167
- fileHandle = try FileHandle(forReadingFrom: filePath)
176
+ try fileHandle.close()
168
177
  } catch {
169
- logger.error("Cannot open file for checksum calculation")
170
- logger.debug("Path: \(filePath.path), Error: \(error)")
171
- return ""
178
+ logger.error("Error closing file during checksum")
179
+ logger.debug("Error: \(error)")
172
180
  }
181
+ }
173
182
 
174
- defer {
175
- do {
176
- try fileHandle.close()
177
- } catch {
178
- logger.error("Error closing file during checksum")
179
- logger.debug("Error: \(error)")
180
- }
183
+ while autoreleasepool(invoking: {
184
+ let fileData: Data
185
+ do {
186
+ fileData = try fileHandle.read(upToCount: bufferSize) ?? Data()
187
+ } catch {
188
+ logger.error("Error reading file during checksum")
189
+ logger.debug("Error: \(error)")
190
+ readFailed = true
191
+ return false
181
192
  }
182
193
 
183
- while autoreleasepool(invoking: {
184
- let fileData: Data
185
- do {
186
- fileData = try fileHandle.read(upToCount: bufferSize) ?? Data()
187
- } catch {
188
- logger.error("Error reading file during checksum")
189
- logger.debug("Error: \(error)")
190
- return false
191
- }
192
-
193
- if fileData.count > 0 {
194
- sha256.update(data: fileData)
195
- return true // Continue
196
- } else {
197
- return false // End of file
198
- }
199
- }) {}
194
+ if fileData.count > 0 {
195
+ sha256.update(data: fileData)
196
+ return true // Continue
197
+ } else {
198
+ return false // End of file
199
+ }
200
+ }) {}
200
201
 
201
- return hexString(from: sha256)
202
- } catch {
203
- logger.error("Cannot calculate checksum")
204
- logger.debug("Path: \(filePath.path), Error: \(error)")
205
- return ""
206
- }
202
+ return readFailed ? "" : hexString(from: sha256)
207
203
  }
208
204
 
209
205
  final class RunningChecksum {
@@ -222,8 +218,7 @@ public struct CryptoCipher {
222
218
  }
223
219
 
224
220
  static func hexString(from sha256: SHA256) -> String {
225
- var copy = sha256
226
- return copy.finalize().compactMap { String(format: "%02x", $0) }.joined()
221
+ return sha256.finalize().compactMap { String(format: "%02x", $0) }.joined()
227
222
  }
228
223
 
229
224
  static func shortPathKey(_ fileName: String) -> String {
@@ -232,8 +227,16 @@ public struct CryptoCipher {
232
227
  return String(hexString(from: sha256).prefix(16))
233
228
  }
234
229
 
230
+ public static func isValidSessionKey(_ sessionKey: String) -> Bool {
231
+ if sessionKey.isEmpty {
232
+ return false
233
+ }
234
+ let sessionKeyParts = sessionKey.components(separatedBy: ":")
235
+ return sessionKeyParts.count == 2 && !sessionKeyParts[0].isEmpty && !sessionKeyParts[1].isEmpty
236
+ }
237
+
235
238
  public static func decryptFile(filePath: URL, publicKey: String, sessionKey: String, version: String) throws {
236
- if publicKey.isEmpty || sessionKey.isEmpty || sessionKey.components(separatedBy: ":").count != 2 {
239
+ if publicKey.isEmpty || !isValidSessionKey(sessionKey) {
237
240
  logger.info("Encryption not set, no public key or session, ignored")
238
241
  return
239
242
  }
@@ -13,7 +13,6 @@
13
13
  //
14
14
 
15
15
  import Foundation
16
- import Version
17
16
 
18
17
  public class DelayUpdateUtils {
19
18
 
@@ -23,7 +22,7 @@ public class DelayUpdateUtils {
23
22
  // swiftlint:enable identifier_name
24
23
  private let logger: Logger
25
24
 
26
- private let currentVersionNative: Version
25
+ private let currentVersionNative: CapgoSemanticVersion
27
26
 
28
27
  public enum CancelDelaySource {
29
28
  case killed
@@ -39,7 +38,7 @@ public class DelayUpdateUtils {
39
38
  }
40
39
  }
41
40
 
42
- public init(currentVersionNative: Version, logger: Logger) {
41
+ public init(currentVersionNative: CapgoSemanticVersion, logger: Logger) {
43
42
  self.currentVersionNative = currentVersionNative
44
43
  self.logger = logger
45
44
  }
@@ -117,7 +116,7 @@ public class DelayUpdateUtils {
117
116
  case "nativeVersion":
118
117
  if let value = value, !value.isEmpty {
119
118
  do {
120
- let versionLimit = try Version(value)
119
+ let versionLimit = try CapgoSemanticVersion(value)
121
120
  if currentVersionNative >= versionLimit {
122
121
  // swiftlint:disable:next line_length
123
122
  logger.info("Native version delay (value: \(value)) condition removed due to above limit at index \(index)")
@@ -151,59 +150,34 @@ public class DelayUpdateUtils {
151
150
  }
152
151
 
153
152
  public func setMultiDelay(delayConditions: String) -> Bool {
154
- do {
155
- UserDefaults.standard.set(delayConditions, forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES)
156
- UserDefaults.standard.synchronize()
157
- logger.info("Delay update saved")
158
- return true
159
- } catch {
160
- logger.error("Failed to delay update, [Error calling 'setMultiDelay()']: \(error)")
161
- return false
162
- }
153
+ UserDefaults.standard.set(delayConditions, forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES)
154
+ UserDefaults.standard.synchronize()
155
+ logger.info("Delay update saved")
156
+ return true
163
157
  }
164
158
 
165
159
  public func setBackgroundTimestamp(_ backgroundTimestamp: Int64) {
166
- do {
167
- UserDefaults.standard.set(backgroundTimestamp, forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY)
168
- UserDefaults.standard.synchronize()
169
- logger.info("Background timestamp saved")
170
- } catch {
171
- logger.error("Failed to save background timestamp, [Error calling 'setBackgroundTimestamp()']: \(error)")
172
- }
160
+ UserDefaults.standard.set(backgroundTimestamp, forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY)
161
+ UserDefaults.standard.synchronize()
162
+ logger.info("Background timestamp saved")
173
163
  }
174
164
 
175
165
  public func unsetBackgroundTimestamp() {
176
- do {
177
- UserDefaults.standard.removeObject(forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY)
178
- UserDefaults.standard.synchronize()
179
- logger.info("Background timestamp removed")
180
- } catch {
181
- // swiftlint:disable:next line_length
182
- logger.error("Failed to remove background timestamp, [Error calling 'unsetBackgroundTimestamp()']: \(error)")
183
- }
166
+ UserDefaults.standard.removeObject(forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY)
167
+ UserDefaults.standard.synchronize()
168
+ logger.info("Background timestamp removed")
184
169
  }
185
170
 
186
171
  private func getBackgroundTimestamp() -> Int64 {
187
- do {
188
- let key = DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY
189
- let timestamp = UserDefaults.standard.object(forKey: key) as? Int64 ?? 0
190
- return timestamp
191
- } catch {
192
- logger.error("Failed to get background timestamp, [Error calling 'getBackgroundTimestamp()']: \(error)")
193
- return 0
194
- }
172
+ let key = DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY
173
+ return UserDefaults.standard.object(forKey: key) as? Int64 ?? 0
195
174
  }
196
175
 
197
176
  public func cancelDelay(source: String) -> Bool {
198
- do {
199
- UserDefaults.standard.removeObject(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES)
200
- UserDefaults.standard.synchronize()
201
- logger.info("All delays canceled from \(source)")
202
- return true
203
- } catch {
204
- logger.error("Failed to cancel update delay: \(error)")
205
- return false
206
- }
177
+ UserDefaults.standard.removeObject(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES)
178
+ UserDefaults.standard.synchronize()
179
+ logger.info("All delays canceled from \(source)")
180
+ return true
207
181
  }
208
182
 
209
183
  // MARK: - Helper methods
@@ -3,6 +3,8 @@ import os.log
3
3
  import WebKit
4
4
 
5
5
  public class Logger {
6
+ static let maxWebViewLogPayloadBytes = 4096
7
+
6
8
  public enum LogLevel: Int {
7
9
  case silent = 0
8
10
  case error
@@ -173,20 +175,94 @@ public class Logger {
173
175
  if let label = _labels[level] {
174
176
  log(atLevel: level, label: label, tag: tag, message: message)
175
177
  if let webView = self.webView {
176
- DispatchQueue.main.async {
177
- let combined = "\(label) \(self.tag) : \(message)"
178
- let jsArg = self.toJSStringLiteral(combined)
179
- webView.evaluateJavaScript("console.\(level.asString())(\(jsArg))", completionHandler: nil)
178
+ let logTag = self.tag
179
+ DispatchQueue.main.async { [weak self] in
180
+ guard let self else { return }
181
+ if let script = self.buildWebViewConsoleScript(level: level, label: label, tag: logTag, message: message) {
182
+ webView.evaluateJavaScript(script, completionHandler: nil)
183
+ }
180
184
  }
181
185
  }
182
186
  }
183
187
  }
184
188
 
185
- private func toJSStringLiteral(_ value: String) -> String {
189
+ func consoleMethodName(for level: LogLevel) -> String? {
190
+ switch level {
191
+ case .error:
192
+ return "error"
193
+ case .warn:
194
+ return "warn"
195
+ case .info:
196
+ return "info"
197
+ case .debug:
198
+ return "debug"
199
+ case .silent:
200
+ return nil
201
+ }
202
+ }
203
+
204
+ private func utf8SequenceLength(_ lead: UInt8) -> Int {
205
+ if (lead & 0x80) == 0 {
206
+ return 1
207
+ }
208
+ if (lead & 0xE0) == 0xC0 {
209
+ return 2
210
+ }
211
+ if (lead & 0xF0) == 0xE0 {
212
+ return 3
213
+ }
214
+ if (lead & 0xF8) == 0xF0 {
215
+ return 4
216
+ }
217
+ return 0
218
+ }
219
+
220
+ func capWebViewLogPayload(_ payload: String) -> String {
221
+ let suffix = "..."
222
+ let maxPayloadBytes = Logger.maxWebViewLogPayloadBytes
223
+ let payloadBytes = Array(payload.utf8.prefix(maxPayloadBytes + 1))
224
+ if payloadBytes.count <= maxPayloadBytes {
225
+ return payload
226
+ }
227
+
228
+ var end = maxPayloadBytes
229
+ while end > 0 && (payloadBytes[end - 1] & 0xC0) == 0x80 {
230
+ end -= 1
231
+ }
232
+ while end > 0 {
233
+ let leadIndex = end - 1
234
+ let lead = payloadBytes[leadIndex]
235
+ let seqLength = utf8SequenceLength(lead)
236
+ if seqLength == 0 {
237
+ end -= 1
238
+ continue
239
+ }
240
+ if leadIndex + seqLength <= maxPayloadBytes {
241
+ end = leadIndex + seqLength
242
+ break
243
+ }
244
+ end -= 1
245
+ }
246
+
247
+ return String(decoding: payloadBytes[0..<end], as: UTF8.self) + suffix
248
+ }
249
+
250
+ func buildWebViewConsoleScript(level: LogLevel, label: String, tag: String, message: String) -> String? {
251
+ guard let consoleMethod = consoleMethodName(for: level) else {
252
+ return nil
253
+ }
254
+
255
+ let payload = capWebViewLogPayload("\(label) \(tag) : \(message)")
256
+ return "console.\(consoleMethod)(\(toJSStringLiteral(payload)))"
257
+ }
258
+
259
+ func toJSStringLiteral(_ value: String) -> String {
186
260
  // Prefer JSON encoding to produce a valid JS string literal
187
261
  if let data = try? JSONEncoder().encode(value),
188
262
  let encoded = String(data: data, encoding: .utf8) {
189
263
  return encoded
264
+ .replacingOccurrences(of: "\u{2028}", with: "\\u2028")
265
+ .replacingOccurrences(of: "\u{2029}", with: "\\u2029")
190
266
  }
191
267
 
192
268
  // Fallback manual escaping (unlikely to be used)