@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,283 @@
1
+ import Foundation
2
+ import os.log
3
+ import WebKit
4
+
5
+ public class Logger {
6
+ public enum LogLevel: Int {
7
+ case silent = 0
8
+ case error
9
+ case warn
10
+ case info
11
+ case debug
12
+
13
+ public static subscript(_ str: String) -> LogLevel? {
14
+ var index = 0
15
+
16
+ while let item = LogLevel(rawValue: index) {
17
+ if str == "\(item)" {
18
+ return item
19
+ }
20
+
21
+ index += 1
22
+ }
23
+
24
+ return nil
25
+ }
26
+
27
+ public func asOSLogType() -> OSLogType {
28
+ switch self {
29
+ case .debug:
30
+ return OSLogType.debug
31
+ case .info:
32
+ return OSLogType.info
33
+ case .warn:
34
+ return OSLogType.default
35
+ case .error:
36
+ return OSLogType.error
37
+ case .silent:
38
+ return OSLogType.info // Compiler wants this, it will never be used
39
+ }
40
+ }
41
+ public func asString() -> String {
42
+ switch self {
43
+ case .debug:
44
+ return "debug"
45
+ case .info:
46
+ return "info"
47
+ case .warn:
48
+ return "warn"
49
+ case .error:
50
+ return "error"
51
+ case .silent:
52
+ return "info"
53
+ }
54
+ }
55
+ }
56
+
57
+ public struct Options {
58
+ public var level: LogLevel
59
+ public var labels: [String: String]
60
+ public var useSyslog: Bool
61
+
62
+ public init(level: LogLevel = LogLevel.info, labels: [String: String] = [:], useSyslog: Bool = false) {
63
+ self.level = level
64
+ self.labels = labels
65
+ self.useSyslog = useSyslog
66
+ }
67
+ }
68
+
69
+ private var _labels: [LogLevel: String] = [
70
+ LogLevel.silent: "",
71
+ LogLevel.error: "🔴",
72
+ LogLevel.warn: "🟠",
73
+ LogLevel.info: "🟢",
74
+ LogLevel.debug: "🔎"
75
+ ]
76
+
77
+ public var labels: [String: String] {
78
+ get {
79
+ var result: [String: String] = [:]
80
+
81
+ for (level, label) in _labels {
82
+ result[String(describing: level)] = label
83
+ }
84
+
85
+ return result
86
+ }
87
+
88
+ set {
89
+ for (level, label) in newValue {
90
+ if let logLevel = LogLevel[level] {
91
+ _labels[logLevel] = label
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ public var level = LogLevel.info
98
+
99
+ public var levelName: String {
100
+ get {
101
+ String(describing: level)
102
+ }
103
+ set {
104
+ if let newLevel = LogLevel[newValue] {
105
+ level = newLevel
106
+ }
107
+ }
108
+ }
109
+
110
+ private var _tag = ""
111
+
112
+ public var tag: String {
113
+ get {
114
+ _tag
115
+ }
116
+ set {
117
+ if !newValue.isEmpty {
118
+ _tag = newValue
119
+ }
120
+ }
121
+ }
122
+
123
+ private let kDefaultTimerLabel = "default"
124
+ private var timers: [String: Date] = [:]
125
+ public var useSyslog = false
126
+ public var webView: WKWebView?
127
+
128
+ public func setWebView(webView: WKWebView) {
129
+ self.webView = webView
130
+ }
131
+
132
+ public init(
133
+ withTag tag: String,
134
+ options: Options? = nil
135
+ ) {
136
+ self.tag = tag
137
+ if let options = options {
138
+ self.level = options.level
139
+ self.labels = options.labels
140
+ self.useSyslog = options.useSyslog
141
+ }
142
+ }
143
+
144
+ public func error(_ message: String) {
145
+ log(atLevel: LogLevel.error, message: message)
146
+ }
147
+
148
+ public func warn(_ message: String) {
149
+ log(atLevel: LogLevel.warn, message: message)
150
+ }
151
+
152
+ public func info(_ message: String) {
153
+ log(atLevel: LogLevel.info, message: message)
154
+ }
155
+
156
+ public func log(_ message: String) {
157
+ log(atLevel: LogLevel.info, message: message)
158
+ }
159
+
160
+ public func debug(_ message: String) {
161
+ log(atLevel: LogLevel.debug, message: message)
162
+ }
163
+
164
+ public func dir(_ value: Any?) {
165
+ // Check the log level here to avoid conversion to string
166
+ if canLog(atLevel: LogLevel.info) {
167
+ log(atLevel: LogLevel.info, message: String(describing: value))
168
+ }
169
+ }
170
+
171
+ public func log(atLevel level: LogLevel, message: String) {
172
+ // This will never fail, but we have to keep swift happy
173
+ if let label = _labels[level] {
174
+ log(atLevel: level, label: label, tag: tag, message: message)
175
+ 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)
180
+ }
181
+ }
182
+ }
183
+ }
184
+
185
+ private func toJSStringLiteral(_ value: String) -> String {
186
+ // Prefer JSON encoding to produce a valid JS string literal
187
+ if let data = try? JSONEncoder().encode(value),
188
+ let encoded = String(data: data, encoding: .utf8) {
189
+ return encoded
190
+ }
191
+
192
+ // Fallback manual escaping (unlikely to be used)
193
+ var escaped = value
194
+ escaped = escaped.replacingOccurrences(of: "\\", with: "\\\\")
195
+ escaped = escaped.replacingOccurrences(of: "\"", with: "\\\"")
196
+ escaped = escaped.replacingOccurrences(of: "'", with: "\\'")
197
+ escaped = escaped.replacingOccurrences(of: "\n", with: "\\n")
198
+ escaped = escaped.replacingOccurrences(of: "\r", with: "\\r")
199
+ escaped = escaped.replacingOccurrences(of: "\u{2028}", with: "\\u2028")
200
+ escaped = escaped.replacingOccurrences(of: "\u{2029}", with: "\\u2029")
201
+ return "\"\(escaped)\""
202
+ }
203
+
204
+ public func log(atLevel level: LogLevel, label: String?, tag: String, message: String) {
205
+ // This will never fail, but we have to keep swift happy
206
+ if let label = label ?? _labels[level] {
207
+ print(atLevel: level, label: label, tag: tag, message: message)
208
+ // eval
209
+ }
210
+ }
211
+
212
+ private func canLog(atLevel level: LogLevel) -> Bool {
213
+ self.level.rawValue >= level.rawValue
214
+ }
215
+
216
+ private func print(atLevel level: LogLevel, label: String, tag: String, message: String) {
217
+ guard canLog(atLevel: level) else {
218
+ return
219
+ }
220
+
221
+ var msg = message
222
+
223
+ if !label.isEmpty {
224
+ // If the label is ASCII, put it after the tag, otherwise before.
225
+ if label[label.startIndex].isASCII {
226
+ msg = "[\(tag)] \(label): \(message)"
227
+ } else {
228
+ msg = "\(label) [\(tag)]: \(message)"
229
+ }
230
+ } else {
231
+ msg = "[\(tag)]: \(message)"
232
+ }
233
+
234
+ if useSyslog {
235
+ os_log("%{public}@", type: level.asOSLogType(), msg)
236
+ } else {
237
+ Swift.print(msg)
238
+ }
239
+ }
240
+
241
+ public func time(_ label: String?) {
242
+ timers[label ?? ""] = Date()
243
+ }
244
+
245
+ public func timeLog(_ label: String?) {
246
+ if let timer = timers[label ?? ""] {
247
+ info(formatTimeInterval(timer.timeIntervalSinceNow))
248
+ } else {
249
+ warn("timer \(label ?? kDefaultTimerLabel) does not exist")
250
+ }
251
+ }
252
+
253
+ public func timeEnd(_ label: String?) {
254
+ timeLog(label)
255
+ timers.removeValue(forKey: label ?? kDefaultTimerLabel)
256
+ }
257
+
258
+ private func formatTimeInterval(_ interval: TimeInterval) -> String {
259
+ let int = Int(interval)
260
+ let millis = Int(((1 + interval.remainder(dividingBy: 1)) * 1000).rounded())
261
+ let seconds = int % 60
262
+ let minutes = (int / 60) % 60
263
+ let hours = (int / 3600)
264
+
265
+ if seconds < 1 {
266
+ return "\(millis)ms"
267
+ }
268
+
269
+ if minutes < 1 {
270
+ return "\(seconds).\(String(format: "%0.3d", millis))s"
271
+ }
272
+
273
+ if hours < 1 {
274
+ return "\(minutes):\(String(format: "%0.2d", seconds)).\(String(format: "%0.3d", millis)) (min:sec.ms)"
275
+ }
276
+
277
+ return "\(hours):\(String(format: "%0.2d", minutes)):\(String(format: "%0.2d", seconds)) (hr:min:sec)"
278
+ }
279
+
280
+ public func trace() {
281
+ info(String(format: "%@", Thread.callStackSymbols))
282
+ }
283
+ }
@@ -0,0 +1,96 @@
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
+ import os.log
10
+
11
+ /// Capgo v2 uses Node's privateEncrypt with PKCS#1 v1.5 type-1 padding for
12
+ /// checksums and session keys. Recover the signed payload using Apple's RSA
13
+ /// implementation instead of custom modular exponentiation.
14
+ public struct RSAPublicKey {
15
+ private let key: SecKey
16
+ private static let keyLock = NSLock()
17
+ private static let log = OSLog(subsystem: "app.capgo.cordova.updater", category: "RSA")
18
+ // One public key only: bounded storage, invalid/rotated keys never fall back
19
+ // to a cached key with different material. SecKey is immutable after import.
20
+ private static var cachedKey: (der: Data, key: SecKey)?
21
+
22
+ public static func load(rsaPublicKey: String) -> RSAPublicKey? {
23
+ guard let der = pemToDer(rsaPublicKey) else {
24
+ os_log("RSA key import failed: invalid PEM/base64", log: Self.log, type: .error)
25
+ return nil
26
+ }
27
+
28
+ keyLock.lock()
29
+ defer { keyLock.unlock() }
30
+ if let cached = cachedKey, cached.der == der {
31
+ return RSAPublicKey(key: cached.key)
32
+ }
33
+
34
+ let attributes: [CFString: Any] = [
35
+ kSecAttrKeyType: kSecAttrKeyTypeRSA,
36
+ kSecAttrKeyClass: kSecAttrKeyClassPublic
37
+ ]
38
+ var error: Unmanaged<CFError>?
39
+ guard let key = SecKeyCreateWithData(der as CFData, attributes as CFDictionary, &error) else {
40
+ os_log(
41
+ "RSA key import failed: %{public}@",
42
+ log: Self.log,
43
+ type: .error,
44
+ String(describing: error?.takeRetainedValue())
45
+ )
46
+ return nil
47
+ }
48
+ guard CapgoRawRsa.supportsCapgoRecovery(key: key) else {
49
+ os_log(
50
+ "RSA key import failed: unsupported key (Capgo requires RSA-2048 with raw encryption)",
51
+ log: Self.log,
52
+ type: .error
53
+ )
54
+ return nil
55
+ }
56
+ cachedKey = (der, key)
57
+ return RSAPublicKey(key: key)
58
+ }
59
+
60
+ public func decrypt(data: Data) -> Data? {
61
+ let blockSize = SecKeyGetBlockSize(key)
62
+ guard data.count == blockSize else { return nil }
63
+ // Raw public-key encryption is the same c^e mod n operation as Node's
64
+ // publicDecrypt. Do NOT use encryption PKCS#1 padding here: the recovered
65
+ // block already contains signature (type-1) padding, checked below.
66
+ guard let recovered = CapgoRawRsa.recoverSignatureBlock(key: key, data: data) else {
67
+ return nil
68
+ }
69
+ return Self.unpadSignature(recovered, blockSize: blockSize)
70
+ }
71
+
72
+ static func unpadSignature(_ block: Data, blockSize: Int = 256) -> Data? {
73
+ let bytes = [UInt8](block)
74
+ // RFC 8017: 00 01, at least eight FF bytes, 00, then the payload.
75
+ guard bytes.count == blockSize, bytes[0] == 0, bytes[1] == 1 else { return nil }
76
+ var separator = 2
77
+ while separator < bytes.count && bytes[separator] == 0xff {
78
+ separator += 1
79
+ }
80
+ guard separator >= 10, separator < bytes.count - 1, bytes[separator] == 0 else {
81
+ return nil
82
+ }
83
+ return Data(bytes[(separator + 1)...])
84
+ }
85
+
86
+ private static func pemToDer(_ pem: String) -> Data? {
87
+ let encoded = pem
88
+ .replacingOccurrences(of: "-----BEGIN RSA PUBLIC KEY-----", with: "")
89
+ .replacingOccurrences(of: "-----END RSA PUBLIC KEY-----", with: "")
90
+ .replacingOccurrences(of: "-----BEGIN PUBLIC KEY-----", with: "")
91
+ .replacingOccurrences(of: "-----END PUBLIC KEY-----", with: "")
92
+ .replacingOccurrences(of: "\\n", with: "")
93
+ .components(separatedBy: .whitespacesAndNewlines).joined()
94
+ return Data(base64Encoded: encoded)
95
+ }
96
+ }
@@ -0,0 +1,52 @@
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 UIKit
8
+
9
+ /// Cordova shake menu stub. Full preview/channel shake UI is planned for a follow-up release.
10
+ final class ShakeMenu {
11
+ private let logger: Logger
12
+
13
+ init(plugin: CordovaUpdaterPlugin, viewController: UIViewController?, logger: Logger, gesture: String) {
14
+ self.logger = logger
15
+ logger.debug("ShakeMenu: gesture \(gesture) registered (Cordova stub)")
16
+ }
17
+
18
+ func usesGesture(_ gesture: String) -> Bool {
19
+ false
20
+ }
21
+
22
+ func stop() {
23
+ // no-op
24
+ }
25
+ }
26
+
27
+ extension CordovaUpdaterPlugin {
28
+ func syncShakeMenuGestureRecognizer() {
29
+ // Cordova stub: gesture UI not implemented yet
30
+ }
31
+
32
+ func removeShakeMenuGestureRecognizer() {
33
+ // no-op
34
+ }
35
+ }
36
+
37
+ extension UIApplication {
38
+ class func topViewController(_ base: UIViewController? = UIApplication.shared.connectedScenes
39
+ .compactMap { ($0 as? UIWindowScene)?.keyWindow }
40
+ .first?.rootViewController) -> UIViewController? {
41
+ if let nav = base as? UINavigationController {
42
+ return topViewController(nav.visibleViewController)
43
+ }
44
+ if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
45
+ return topViewController(selected)
46
+ }
47
+ if let presented = base?.presentedViewController {
48
+ return topViewController(presented)
49
+ }
50
+ return base
51
+ }
52
+ }
@@ -0,0 +1,81 @@
1
+ import Foundation
2
+ import WebKit
3
+
4
+ public class UpdaterSchemeHandler: NSObject {
5
+ private let activeBundleDirLock = NSLock()
6
+ private var _activeBundleDir: URL?
7
+
8
+ private let activeTasksQueue = DispatchQueue(label: "app.capgo.cordova.updater.schemeHandler")
9
+ private var activeTasks = Set<URLSchemeTaskWrapper>()
10
+
11
+ public var activeBundleDir: URL? {
12
+ get {
13
+ activeBundleDirLock.lock()
14
+ defer { activeBundleDirLock.unlock() }
15
+ return _activeBundleDir
16
+ }
17
+ set {
18
+ activeBundleDirLock.lock()
19
+ _activeBundleDir = newValue
20
+ activeBundleDirLock.unlock()
21
+ }
22
+ }
23
+
24
+ public func handle(task: WKURLSchemeTask) -> Bool {
25
+ guard let baseDir = activeBundleDir, let requestURL = task.request.url else {
26
+ return false
27
+ }
28
+ var relativePath = requestURL.path
29
+ if relativePath.hasPrefix("/") { relativePath = String(relativePath.dropFirst()) }
30
+ if relativePath.isEmpty || relativePath.hasSuffix("/") { relativePath += "index.html" }
31
+ let fileURL = baseDir.appendingPathComponent(relativePath).standardizedFileURL
32
+ let canonicalBase = baseDir.standardizedFileURL.path
33
+ guard fileURL.path == canonicalBase || fileURL.path.hasPrefix(canonicalBase + "/") else { return false }
34
+ var isDirectory: ObjCBool = false
35
+ guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory), !isDirectory.boolValue else { return false }
36
+ let wrapper = URLSchemeTaskWrapper(task: task)
37
+ activeTasksQueue.sync { _ = activeTasks.insert(wrapper) }
38
+ DispatchQueue.global(qos: .userInitiated).async { [weak self] in
39
+ self?.serve(task: task, wrapper: wrapper, fileURL: fileURL, requestURL: requestURL)
40
+ }
41
+ return true
42
+ }
43
+
44
+ public func stop(task: WKURLSchemeTask) {
45
+ activeTasksQueue.sync { activeTasks = activeTasks.filter { $0.task !== task } }
46
+ }
47
+
48
+ private func serve(task: WKURLSchemeTask, wrapper: URLSchemeTaskWrapper, fileURL: URL, requestURL: URL) {
49
+ guard let data = try? Data(contentsOf: fileURL) else {
50
+ if activeTasksQueue.sync(execute: { activeTasks.contains(wrapper) }) { task.didFailWithError(NSError(domain: "CapgoCordovaUpdater", code: 1)) }
51
+ activeTasksQueue.sync { _ = activeTasks.remove(wrapper) }
52
+ return
53
+ }
54
+ let headers = ["Content-Type": mimeType(for: fileURL), "Cache-Control": "no-cache", "Content-Length": String(data.count)]
55
+ guard let response = HTTPURLResponse(url: requestURL, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: headers) else { return }
56
+ if activeTasksQueue.sync(execute: { activeTasks.contains(wrapper) }) {
57
+ task.didReceive(response)
58
+ task.didReceive(data)
59
+ task.didFinish()
60
+ }
61
+ activeTasksQueue.sync { _ = activeTasks.remove(wrapper) }
62
+ }
63
+
64
+ private func mimeType(for url: URL) -> String {
65
+ switch url.pathExtension.lowercased() {
66
+ case "js", "mjs": return "application/javascript"
67
+ case "html", "htm": return "text/html"
68
+ case "css": return "text/css"
69
+ case "json": return "application/json"
70
+ case "wasm": return "application/wasm"
71
+ default: return "application/octet-stream"
72
+ }
73
+ }
74
+ }
75
+
76
+ private final class URLSchemeTaskWrapper: Hashable {
77
+ let task: WKURLSchemeTask
78
+ init(task: WKURLSchemeTask) { self.task = task }
79
+ static func == (lhs: URLSchemeTaskWrapper, rhs: URLSchemeTaskWrapper) -> Bool { lhs.task === rhs.task }
80
+ func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(task as AnyObject)) }
81
+ }
@@ -0,0 +1,46 @@
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
+ protocol ObjectSavable {
10
+ func setObj<Object>(_ object: Object, forKey: String) throws where Object: Encodable
11
+ func getObj<Object>(forKey: String, castTo type: Object.Type) throws -> Object where Object: Decodable
12
+ }
13
+
14
+ enum ObjectSavableError: String, LocalizedError {
15
+ case unableToEncode = "Unable to encode object into data"
16
+ case noValue = "No data object found for the given key"
17
+ case unableToDecode = "Unable to decode object into given type"
18
+ case checksum = "Checksum failed"
19
+
20
+ var errorDescription: String? {
21
+ rawValue
22
+ }
23
+ }
24
+
25
+ extension UserDefaults: ObjectSavable {
26
+ func setObj<Object>(_ object: Object, forKey: String) throws where Object: Encodable {
27
+ let encoder: JSONEncoder = JSONEncoder()
28
+ do {
29
+ let data: Data = try encoder.encode(object)
30
+ set(data, forKey: forKey)
31
+ } catch {
32
+ throw ObjectSavableError.unableToEncode
33
+ }
34
+ }
35
+
36
+ func getObj<Object>(forKey: String, castTo type: Object.Type) throws -> Object where Object: Decodable {
37
+ guard let data: Data = data(forKey: forKey) else { throw ObjectSavableError.noValue }
38
+ let decoder: JSONDecoder = JSONDecoder()
39
+ do {
40
+ let object: Object = try decoder.decode(type, from: data)
41
+ return object
42
+ } catch {
43
+ throw ObjectSavableError.unableToDecode
44
+ }
45
+ }
46
+ }