@viettelpost/react-native-ota 0.1.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.
- package/README.md +38 -0
- package/android/build.gradle +48 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAHashUtils.kt +21 -0
- package/android/src/main/java/com/viettelpost/otakit/OTATestReceiver.kt +51 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateBundleResolver.kt +405 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateCleanup.kt +186 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateDownloader.kt +649 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateMetadata.kt +72 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateModule.kt +140 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdatePackage.kt +30 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateSignatureVerifier.kt +63 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateStorage.kt +62 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAZipUtils.kt +100 -0
- package/android/src/main/res/raw/ota_public_key.pem +9 -0
- package/bin/cli/assets-zip.js +77 -0
- package/bin/cli/bundle.js +72 -0
- package/bin/cli/deploy.js +224 -0
- package/bin/cli/sign.js +97 -0
- package/bin/cli/upload.js +109 -0
- package/bin/ota.js +200 -0
- package/docs/BACKEND_CONTRACT.md +93 -0
- package/docs/DEPLOY_CLI.md +39 -0
- package/docs/INTEGRATION_ANDROID.md +20 -0
- package/docs/INTEGRATION_IOS.md +21 -0
- package/docs/RELEASE_WORKFLOW.md +14 -0
- package/ios/OTAHashUtils.swift +22 -0
- package/ios/OTAUpdateBundleResolver.swift +359 -0
- package/ios/OTAUpdateCleanup.swift +269 -0
- package/ios/OTAUpdateDownloader.swift +709 -0
- package/ios/OTAUpdateMetadata.swift +47 -0
- package/ios/OTAUpdateModule.mm +190 -0
- package/ios/OTAUpdateSignatureVerifier.swift +81 -0
- package/ios/OTAUpdateStorage.swift +83 -0
- package/ios/OTAZipUtils.swift +103 -0
- package/ios/ota_public_key.pem +9 -0
- package/lib/NativeOTAUpdate.d.ts +77 -0
- package/lib/NativeOTAUpdate.js +59 -0
- package/lib/OTAClient.d.ts +27 -0
- package/lib/OTAClient.js +101 -0
- package/lib/config.d.ts +14 -0
- package/lib/config.js +29 -0
- package/lib/devtools.d.ts +10 -0
- package/lib/devtools.js +54 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.js +32 -0
- package/lib/spec/NativeOTAUpdate.d.ts +16 -0
- package/lib/spec/NativeOTAUpdate.js +4 -0
- package/package.json +82 -0
- package/react-native-ota.podspec +21 -0
- package/scripts/run-bin.js +67 -0
- package/src/NativeOTAUpdate.ts +144 -0
- package/src/OTAClient.ts +151 -0
- package/src/config.ts +41 -0
- package/src/devtools.ts +64 -0
- package/src/index.ts +69 -0
- package/src/spec/NativeOTAUpdate.ts +21 -0
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
struct OTADownloadInfo: Codable {
|
|
4
|
+
let version: String
|
|
5
|
+
let tempBundlePath: String
|
|
6
|
+
let finalBundlePath: String
|
|
7
|
+
let tempAssetsZipPath: String
|
|
8
|
+
let finalAssetsDirectoryPath: String
|
|
9
|
+
let tempExists: Bool
|
|
10
|
+
let finalExists: Bool
|
|
11
|
+
let tempAssetsZipExists: Bool
|
|
12
|
+
let finalAssetFileCount: Int
|
|
13
|
+
let tempSize: UInt64
|
|
14
|
+
let finalSize: UInt64
|
|
15
|
+
let tempAssetsZipSize: UInt64
|
|
16
|
+
let finalAssetsSize: UInt64
|
|
17
|
+
let tempSha256: String?
|
|
18
|
+
let finalSha256: String?
|
|
19
|
+
let tempAssetsSha256: String?
|
|
20
|
+
let signatureRequired: Bool
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
struct OTAUpdateDownloaderError: Error {
|
|
24
|
+
let code: String
|
|
25
|
+
let message: String
|
|
26
|
+
let underlyingError: Error?
|
|
27
|
+
|
|
28
|
+
init(code: String, message: String, underlyingError: Error? = nil) {
|
|
29
|
+
self.code = code
|
|
30
|
+
self.message = message
|
|
31
|
+
self.underlyingError = underlyingError
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var nsError: NSError {
|
|
35
|
+
var userInfo: [String: Any] = [
|
|
36
|
+
NSLocalizedDescriptionKey: message,
|
|
37
|
+
"otaCode": code,
|
|
38
|
+
]
|
|
39
|
+
if let underlyingError = underlyingError {
|
|
40
|
+
userInfo[NSUnderlyingErrorKey] = underlyingError as NSError
|
|
41
|
+
}
|
|
42
|
+
return NSError(domain: "OTAUpdateDownloader", code: 1, userInfo: userInfo)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private struct OTAUpdatePayload {
|
|
47
|
+
let version: String
|
|
48
|
+
let bundleUrl: String?
|
|
49
|
+
let assetsUrl: String?
|
|
50
|
+
let platform: String
|
|
51
|
+
let fileName: String
|
|
52
|
+
let sha256: String?
|
|
53
|
+
let assetsSha256: String?
|
|
54
|
+
let signature: String?
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@objc(OTAUpdateDownloader)
|
|
58
|
+
final class OTAUpdateDownloader: NSObject {
|
|
59
|
+
@objc static let shared = OTAUpdateDownloader()
|
|
60
|
+
|
|
61
|
+
private let storage = OTAUpdateStorage.shared
|
|
62
|
+
private let fileManager = FileManager.default
|
|
63
|
+
private let encoder = JSONEncoder()
|
|
64
|
+
|
|
65
|
+
private let bundleFileName = "main.jsbundle"
|
|
66
|
+
private let assetsZipFileName = "assets.zip"
|
|
67
|
+
private let platformIOS = "ios"
|
|
68
|
+
private let partialSuffix = ".download"
|
|
69
|
+
|
|
70
|
+
private let statusDownloaded = "downloaded"
|
|
71
|
+
private let statusDownloadFailed = "download_failed"
|
|
72
|
+
private let statusVerifying = "verifying"
|
|
73
|
+
private let statusVerified = "verified"
|
|
74
|
+
private let statusVerifyFailed = "verify_failed"
|
|
75
|
+
private let statusPending = "pending"
|
|
76
|
+
|
|
77
|
+
private let reasonDownloadFailed = "download_failed"
|
|
78
|
+
private let reasonMissingSha256 = "missing_sha256"
|
|
79
|
+
private let reasonMissingAssetsSha256 = "missing_assets_sha256"
|
|
80
|
+
private let reasonMissingSignature = "missing_signature"
|
|
81
|
+
private let reasonSignatureInvalid = "signature_invalid"
|
|
82
|
+
private let reasonSha256Mismatch = "sha256_mismatch"
|
|
83
|
+
private let reasonAssetsSha256Mismatch = "assets_sha256_mismatch"
|
|
84
|
+
private let reasonAssetsZipInvalid = "assets_zip_invalid"
|
|
85
|
+
private let reasonAssetsExtractFailed = "assets_extract_failed"
|
|
86
|
+
private let reasonTempBundleNotFoundForVerify = "temp_bundle_not_found_for_verify"
|
|
87
|
+
private let reasonTempAssetsNotFoundForVerify = "temp_assets_not_found_for_verify"
|
|
88
|
+
private let signatureMismatchMessage = "OTA signature mismatch. Check that version, platform, fileName, lowercase sha256, and assetsSha256 when assets are included exactly match the signed payload."
|
|
89
|
+
|
|
90
|
+
@objc func downloadAndInstallBundle(_ updateJson: String, completion: @escaping (NSNumber?, NSError?) -> Void) {
|
|
91
|
+
DispatchQueue.global(qos: .utility).async {
|
|
92
|
+
do {
|
|
93
|
+
_ = try self.downloadBundle(updateJson)
|
|
94
|
+
let didInstall = try self.installDownloadedBundle(updateJson)
|
|
95
|
+
completion(didInstall as NSNumber, nil)
|
|
96
|
+
} catch {
|
|
97
|
+
completion(nil, self.toNSError(error))
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@objc func getDownloadInfo(_ version: String) throws -> String {
|
|
103
|
+
let normalizedVersion = try normalizeVersion(version)
|
|
104
|
+
let tempBundleURL = try tempBundleURL(version: normalizedVersion)
|
|
105
|
+
let finalBundleURL = try finalBundleURL(version: normalizedVersion)
|
|
106
|
+
let tempAssetsURL = try tempAssetsURL(version: normalizedVersion)
|
|
107
|
+
let finalAssetsDirectoryURL = try finalAssetsDirectoryURL(version: normalizedVersion)
|
|
108
|
+
let info = OTADownloadInfo(
|
|
109
|
+
version: normalizedVersion,
|
|
110
|
+
tempBundlePath: tempBundleURL.path,
|
|
111
|
+
finalBundlePath: finalBundleURL.path,
|
|
112
|
+
tempAssetsZipPath: tempAssetsURL.path,
|
|
113
|
+
finalAssetsDirectoryPath: finalAssetsDirectoryURL.path,
|
|
114
|
+
tempExists: fileExistsAndNonEmpty(tempBundleURL),
|
|
115
|
+
finalExists: fileExistsAndNonEmpty(finalBundleURL),
|
|
116
|
+
tempAssetsZipExists: fileExistsAndNonEmpty(tempAssetsURL),
|
|
117
|
+
finalAssetFileCount: directoryFileCount(finalAssetsDirectoryURL),
|
|
118
|
+
tempSize: fileSize(tempBundleURL),
|
|
119
|
+
finalSize: fileSize(finalBundleURL),
|
|
120
|
+
tempAssetsZipSize: fileSize(tempAssetsURL),
|
|
121
|
+
finalAssetsSize: directorySize(finalAssetsDirectoryURL),
|
|
122
|
+
tempSha256: try fileSha256IfExists(tempBundleURL),
|
|
123
|
+
finalSha256: try fileSha256IfExists(finalBundleURL),
|
|
124
|
+
tempAssetsSha256: try fileSha256IfExists(tempAssetsURL),
|
|
125
|
+
signatureRequired: true
|
|
126
|
+
)
|
|
127
|
+
let data = try encoder.encode(info)
|
|
128
|
+
guard let json = String(data: data, encoding: .utf8) else {
|
|
129
|
+
throw OTAUpdateDownloaderError(code: "OTA_GET_DOWNLOAD_INFO_FAILED", message: "Failed to encode OTA download info")
|
|
130
|
+
}
|
|
131
|
+
return json
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private func downloadBundle(_ updateJson: String) throws -> String {
|
|
135
|
+
let update = try parsePayload(updateJson, requireBundleUrl: true)
|
|
136
|
+
try validateVersionCanInstall(update.version)
|
|
137
|
+
|
|
138
|
+
let tempDir = try tempBundleDirectory(version: update.version)
|
|
139
|
+
let partialURL = tempDir.appendingPathComponent(bundleFileName + partialSuffix)
|
|
140
|
+
let tempBundleURL = tempDir.appendingPathComponent(bundleFileName)
|
|
141
|
+
let partialAssetsURL = tempDir.appendingPathComponent(assetsZipFileName + partialSuffix)
|
|
142
|
+
let tempAssetsURL = tempDir.appendingPathComponent(assetsZipFileName)
|
|
143
|
+
|
|
144
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: update.version)
|
|
145
|
+
try fileManager.createDirectory(at: tempDir, withIntermediateDirectories: true, attributes: nil)
|
|
146
|
+
|
|
147
|
+
do {
|
|
148
|
+
try downloadToFile(
|
|
149
|
+
urlString: update.bundleUrl ?? "",
|
|
150
|
+
destinationURL: partialURL
|
|
151
|
+
)
|
|
152
|
+
guard fileExistsAndNonEmpty(partialURL) else {
|
|
153
|
+
throw OTAUpdateDownloaderError(code: "OTA_DOWNLOAD_FAILED", message: "Downloaded OTA bundle is empty")
|
|
154
|
+
}
|
|
155
|
+
if fileManager.fileExists(atPath: tempBundleURL.path) {
|
|
156
|
+
try fileManager.removeItem(at: tempBundleURL)
|
|
157
|
+
}
|
|
158
|
+
do {
|
|
159
|
+
try fileManager.moveItem(at: partialURL, to: tempBundleURL)
|
|
160
|
+
} catch {
|
|
161
|
+
try fileManager.copyItem(at: partialURL, to: tempBundleURL)
|
|
162
|
+
try? fileManager.removeItem(at: partialURL)
|
|
163
|
+
}
|
|
164
|
+
guard fileExistsAndNonEmpty(tempBundleURL) else {
|
|
165
|
+
throw OTAUpdateDownloaderError(code: "OTA_DOWNLOAD_FAILED", message: "Downloaded OTA bundle was not finalized")
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if let assetsUrl = update.assetsUrl, !assetsUrl.isEmpty {
|
|
169
|
+
try downloadToFile(
|
|
170
|
+
urlString: assetsUrl,
|
|
171
|
+
destinationURL: partialAssetsURL,
|
|
172
|
+
label: "OTA assets ZIP"
|
|
173
|
+
)
|
|
174
|
+
guard fileExistsAndNonEmpty(partialAssetsURL) else {
|
|
175
|
+
throw OTAUpdateDownloaderError(code: "OTA_ASSETS_DOWNLOAD_FAILED", message: "Downloaded OTA assets ZIP is empty")
|
|
176
|
+
}
|
|
177
|
+
if fileManager.fileExists(atPath: tempAssetsURL.path) {
|
|
178
|
+
try fileManager.removeItem(at: tempAssetsURL)
|
|
179
|
+
}
|
|
180
|
+
do {
|
|
181
|
+
try fileManager.moveItem(at: partialAssetsURL, to: tempAssetsURL)
|
|
182
|
+
} catch {
|
|
183
|
+
try fileManager.copyItem(at: partialAssetsURL, to: tempAssetsURL)
|
|
184
|
+
try? fileManager.removeItem(at: partialAssetsURL)
|
|
185
|
+
}
|
|
186
|
+
guard fileExistsAndNonEmpty(tempAssetsURL) else {
|
|
187
|
+
throw OTAUpdateDownloaderError(code: "OTA_ASSETS_DOWNLOAD_FAILED", message: "Downloaded OTA assets ZIP was not finalized")
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
var metadata = try storage.readMetadata()
|
|
192
|
+
metadata.status = statusDownloaded
|
|
193
|
+
metadata.failedBundleVersion = nil
|
|
194
|
+
metadata.lastFailureReason = nil
|
|
195
|
+
try storage.writeMetadata(metadata)
|
|
196
|
+
return tempBundleURL.path
|
|
197
|
+
} catch {
|
|
198
|
+
try? fileManager.removeItem(at: partialURL)
|
|
199
|
+
try? fileManager.removeItem(at: partialAssetsURL)
|
|
200
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: update.version)
|
|
201
|
+
var metadata = try storage.readMetadata()
|
|
202
|
+
metadata.status = statusDownloadFailed
|
|
203
|
+
metadata.failedBundleVersion = update.version
|
|
204
|
+
let otaError = error as? OTAUpdateDownloaderError
|
|
205
|
+
metadata.lastFailureReason = otaError?.code == "OTA_ASSETS_DOWNLOAD_FAILED" ? "assets_download_failed" : reasonDownloadFailed
|
|
206
|
+
try storage.writeMetadata(metadata)
|
|
207
|
+
if let otaError = otaError, otaError.code == "OTA_ASSETS_DOWNLOAD_FAILED" {
|
|
208
|
+
throw otaError
|
|
209
|
+
}
|
|
210
|
+
throw OTAUpdateDownloaderError(code: "OTA_DOWNLOAD_FAILED", message: "Failed to download OTA bundle for version \(update.version)", underlyingError: error)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private func verifyDownloadedBundle(_ updateJson: String) throws -> Bool {
|
|
215
|
+
let update = try parsePayload(updateJson, requireSha256: true)
|
|
216
|
+
return try verifyDownloadedBundle(update)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private func verifyUpdateSignature(_ updateJson: String) throws -> Bool {
|
|
220
|
+
let update = try parsePayload(
|
|
221
|
+
updateJson,
|
|
222
|
+
requireSha256: true,
|
|
223
|
+
requireSignature: true,
|
|
224
|
+
requireSigningFields: true
|
|
225
|
+
)
|
|
226
|
+
return try verifyUpdateSignature(update)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private func installDownloadedBundle(_ updateJson: String) throws -> Bool {
|
|
230
|
+
let update = try parsePayload(
|
|
231
|
+
updateJson,
|
|
232
|
+
requireSha256: true,
|
|
233
|
+
requireSignature: true,
|
|
234
|
+
requireSigningFields: true
|
|
235
|
+
)
|
|
236
|
+
try validateVersionCanInstall(update.version)
|
|
237
|
+
try verifyDownloadedUpdate(update)
|
|
238
|
+
|
|
239
|
+
let tempDir = try tempBundleDirectory(version: update.version)
|
|
240
|
+
let tempBundle = tempDir.appendingPathComponent(bundleFileName)
|
|
241
|
+
let tempAssets = tempDir.appendingPathComponent(assetsZipFileName)
|
|
242
|
+
guard fileExistsAndNonEmpty(tempBundle) else {
|
|
243
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: update.version)
|
|
244
|
+
throw OTAUpdateDownloaderError(code: "OTA_TEMP_BUNDLE_NOT_FOUND", message: "Downloaded OTA temp bundle was not found at \(tempBundle.path)")
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let finalDir = try finalBundleDirectory(version: update.version)
|
|
248
|
+
let finalBundle = finalDir.appendingPathComponent(bundleFileName)
|
|
249
|
+
do {
|
|
250
|
+
_ = try OTAUpdateCleanup.shared.deleteFinalBundleForFailedInstall(version: update.version)
|
|
251
|
+
if fileManager.fileExists(atPath: finalDir.path) {
|
|
252
|
+
throw OTAUpdateDownloaderError(code: "OTA_INSTALL_FAILED", message: "Unable to clear previous OTA bundle directory")
|
|
253
|
+
}
|
|
254
|
+
try fileManager.createDirectory(at: finalDir, withIntermediateDirectories: true, attributes: nil)
|
|
255
|
+
try fileManager.copyItem(at: tempBundle, to: finalBundle)
|
|
256
|
+
guard fileExistsAndNonEmpty(finalBundle) else {
|
|
257
|
+
throw OTAUpdateDownloaderError(code: "OTA_INSTALL_FAILED", message: "Installed OTA bundle is missing or empty")
|
|
258
|
+
}
|
|
259
|
+
if update.assetsUrl?.isEmpty == false {
|
|
260
|
+
guard fileExistsAndNonEmpty(tempAssets) else {
|
|
261
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: update.version)
|
|
262
|
+
try? OTAUpdateCleanup.shared.deleteFinalBundleForFailedInstall(version: update.version)
|
|
263
|
+
throw OTAUpdateDownloaderError(code: "OTA_TEMP_ASSETS_NOT_FOUND", message: "Downloaded OTA temp assets ZIP was not found at \(tempAssets.path)")
|
|
264
|
+
}
|
|
265
|
+
do {
|
|
266
|
+
try OTAZipUtils.unzip(zipURL: tempAssets, destinationURL: finalDir)
|
|
267
|
+
} catch {
|
|
268
|
+
try markVerifyFailed(version: update.version, reason: reasonAssetsExtractFailed)
|
|
269
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: update.version)
|
|
270
|
+
try? OTAUpdateCleanup.shared.deleteFinalBundleForFailedInstall(version: update.version)
|
|
271
|
+
if let otaError = error as? OTAUpdateDownloaderError {
|
|
272
|
+
throw otaError
|
|
273
|
+
}
|
|
274
|
+
throw OTAUpdateDownloaderError(code: "OTA_ASSETS_EXTRACT_FAILED", message: "Failed to extract OTA assets for version \(update.version)", underlyingError: error)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
do {
|
|
278
|
+
_ = try OTAUpdateCleanup.shared.cleanupTemp(version: update.version)
|
|
279
|
+
} catch {
|
|
280
|
+
NSLog("OTAKit cleanup temp after install failed: %@", error.localizedDescription)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
var metadata = try storage.readMetadata()
|
|
284
|
+
metadata.previousBundleVersion = metadata.activeBundleVersion
|
|
285
|
+
metadata.pendingBundleVersion = update.version
|
|
286
|
+
metadata.status = statusPending
|
|
287
|
+
metadata.launchCountForPending = 0
|
|
288
|
+
metadata.failedBundleVersion = nil
|
|
289
|
+
metadata.lastFailureReason = nil
|
|
290
|
+
try storage.writeMetadata(metadata)
|
|
291
|
+
return true
|
|
292
|
+
} catch {
|
|
293
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: update.version)
|
|
294
|
+
try? OTAUpdateCleanup.shared.deleteFinalBundleForFailedInstall(version: update.version)
|
|
295
|
+
if let otaError = error as? OTAUpdateDownloaderError {
|
|
296
|
+
throw otaError
|
|
297
|
+
}
|
|
298
|
+
throw OTAUpdateDownloaderError(code: "OTA_INSTALL_FAILED", message: "Failed to install downloaded OTA bundle for version \(update.version)", underlyingError: error)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private func verifyDownloadedUpdate(_ update: OTAUpdatePayload) throws {
|
|
303
|
+
do {
|
|
304
|
+
_ = try verifyDownloadedBundle(update)
|
|
305
|
+
_ = try verifyUpdateSignature(update)
|
|
306
|
+
} catch {
|
|
307
|
+
if let otaError = error as? OTAUpdateDownloaderError,
|
|
308
|
+
otaError.code == "OTA_MISSING_SIGNATURE" ||
|
|
309
|
+
otaError.code == "OTA_SIGNATURE_INVALID" ||
|
|
310
|
+
otaError.code == "OTA_ASSETS_SHA256_MISMATCH" ||
|
|
311
|
+
otaError.code == "OTA_ASSETS_ZIP_INVALID" {
|
|
312
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: update.version)
|
|
313
|
+
}
|
|
314
|
+
throw error
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private func verifyDownloadedBundle(_ update: OTAUpdatePayload) throws -> Bool {
|
|
319
|
+
guard let expectedSha256 = update.sha256, !expectedSha256.isEmpty else {
|
|
320
|
+
try markVerifyFailed(version: update.version, reason: reasonMissingSha256)
|
|
321
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: update.version)
|
|
322
|
+
throw OTAUpdateDownloaderError(code: "OTA_MISSING_SHA256", message: "OTA update \(update.version) is missing required sha256")
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let tempDir = try tempBundleDirectory(version: update.version)
|
|
326
|
+
let tempBundle = tempDir.appendingPathComponent(bundleFileName)
|
|
327
|
+
guard fileExistsAndNonEmpty(tempBundle) else {
|
|
328
|
+
try markVerifyFailed(version: update.version, reason: reasonTempBundleNotFoundForVerify)
|
|
329
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: update.version)
|
|
330
|
+
throw OTAUpdateDownloaderError(code: "OTA_TEMP_BUNDLE_NOT_FOUND", message: "Downloaded OTA temp bundle was not found at \(tempBundle.path)")
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
var metadata = try storage.readMetadata()
|
|
334
|
+
metadata.status = statusVerifying
|
|
335
|
+
metadata.failedBundleVersion = nil
|
|
336
|
+
metadata.lastFailureReason = nil
|
|
337
|
+
try storage.writeMetadata(metadata)
|
|
338
|
+
|
|
339
|
+
let actualSha256 = try OTAHashUtils.sha256(fileURL: tempBundle)
|
|
340
|
+
guard actualSha256.caseInsensitiveCompare(expectedSha256) == .orderedSame else {
|
|
341
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: update.version)
|
|
342
|
+
try markVerifyFailed(version: update.version, reason: reasonSha256Mismatch)
|
|
343
|
+
throw OTAUpdateDownloaderError(code: "OTA_SHA256_MISMATCH", message: "OTA SHA-256 verification failed for version \(update.version)")
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if let assetsUrl = update.assetsUrl, !assetsUrl.isEmpty {
|
|
347
|
+
guard let expectedAssetsSha256 = update.assetsSha256, !expectedAssetsSha256.isEmpty else {
|
|
348
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: update.version)
|
|
349
|
+
try markVerifyFailed(version: update.version, reason: reasonMissingAssetsSha256)
|
|
350
|
+
throw OTAUpdateDownloaderError(code: "OTA_MISSING_ASSETS_SHA256", message: "OTA update \(update.version) includes assetsUrl but is missing required assetsSha256")
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let tempAssets = tempDir.appendingPathComponent(assetsZipFileName)
|
|
354
|
+
guard fileExistsAndNonEmpty(tempAssets) else {
|
|
355
|
+
try markVerifyFailed(version: update.version, reason: reasonTempAssetsNotFoundForVerify)
|
|
356
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: update.version)
|
|
357
|
+
throw OTAUpdateDownloaderError(code: "OTA_TEMP_ASSETS_NOT_FOUND", message: "Downloaded OTA temp assets ZIP was not found at \(tempAssets.path)")
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let actualAssetsSha256 = try OTAHashUtils.sha256(fileURL: tempAssets)
|
|
361
|
+
guard actualAssetsSha256.caseInsensitiveCompare(expectedAssetsSha256) == .orderedSame else {
|
|
362
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: update.version)
|
|
363
|
+
try markVerifyFailed(version: update.version, reason: reasonAssetsSha256Mismatch)
|
|
364
|
+
throw OTAUpdateDownloaderError(code: "OTA_ASSETS_SHA256_MISMATCH", message: "OTA assets SHA-256 verification failed for version \(update.version)")
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
do {
|
|
368
|
+
try OTAZipUtils.validate(zipURL: tempAssets)
|
|
369
|
+
} catch {
|
|
370
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: update.version)
|
|
371
|
+
try markVerifyFailed(version: update.version, reason: reasonAssetsZipInvalid)
|
|
372
|
+
if let otaError = error as? OTAUpdateDownloaderError {
|
|
373
|
+
throw otaError
|
|
374
|
+
}
|
|
375
|
+
throw OTAUpdateDownloaderError(code: "OTA_ASSETS_ZIP_INVALID", message: "OTA assets ZIP validation failed for version \(update.version)", underlyingError: error)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
var verifiedMetadata = try storage.readMetadata()
|
|
380
|
+
verifiedMetadata.status = statusVerified
|
|
381
|
+
verifiedMetadata.failedBundleVersion = nil
|
|
382
|
+
verifiedMetadata.lastFailureReason = nil
|
|
383
|
+
try storage.writeMetadata(verifiedMetadata)
|
|
384
|
+
return true
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private func verifyUpdateSignature(_ update: OTAUpdatePayload) throws -> Bool {
|
|
388
|
+
guard let signature = update.signature, !signature.isEmpty else {
|
|
389
|
+
try markVerifyFailed(version: update.version, reason: reasonMissingSignature)
|
|
390
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: update.version)
|
|
391
|
+
throw OTAUpdateDownloaderError(code: "OTA_MISSING_SIGNATURE", message: "OTA update \(update.version) is missing required signature")
|
|
392
|
+
}
|
|
393
|
+
let canonicalPayload = OTAUpdateSignatureVerifier.canonicalPayload(
|
|
394
|
+
version: update.version,
|
|
395
|
+
platform: update.platform,
|
|
396
|
+
fileName: update.fileName,
|
|
397
|
+
sha256: update.sha256 ?? "",
|
|
398
|
+
assetsSha256: update.assetsSha256
|
|
399
|
+
)
|
|
400
|
+
let isValid: Bool
|
|
401
|
+
do {
|
|
402
|
+
isValid = try OTAUpdateSignatureVerifier.verifySignature(
|
|
403
|
+
canonicalPayload: canonicalPayload,
|
|
404
|
+
base64Signature: signature
|
|
405
|
+
)
|
|
406
|
+
} catch {
|
|
407
|
+
NSLog(
|
|
408
|
+
"OTA signature verification error for version %@. Canonical payload used by iOS:\n%@",
|
|
409
|
+
update.version,
|
|
410
|
+
canonicalPayload
|
|
411
|
+
)
|
|
412
|
+
try? markVerifyFailed(version: update.version, reason: reasonSignatureInvalid)
|
|
413
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: update.version)
|
|
414
|
+
throw OTAUpdateDownloaderError(
|
|
415
|
+
code: "OTA_SIGNATURE_INVALID",
|
|
416
|
+
message: signatureMismatchMessage,
|
|
417
|
+
underlyingError: error
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
guard isValid else {
|
|
422
|
+
NSLog(
|
|
423
|
+
"OTA signature mismatch for version %@. Canonical payload used by iOS:\n%@",
|
|
424
|
+
update.version,
|
|
425
|
+
canonicalPayload
|
|
426
|
+
)
|
|
427
|
+
try markVerifyFailed(version: update.version, reason: reasonSignatureInvalid)
|
|
428
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: update.version)
|
|
429
|
+
throw OTAUpdateDownloaderError(code: "OTA_SIGNATURE_INVALID", message: signatureMismatchMessage)
|
|
430
|
+
}
|
|
431
|
+
return true
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private func parsePayload(
|
|
435
|
+
_ updateJson: String,
|
|
436
|
+
requireBundleUrl: Bool = false,
|
|
437
|
+
requireSha256: Bool = false,
|
|
438
|
+
requireSignature: Bool = false,
|
|
439
|
+
requireSigningFields: Bool = false
|
|
440
|
+
) throws -> OTAUpdatePayload {
|
|
441
|
+
guard let data = updateJson.data(using: .utf8),
|
|
442
|
+
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
443
|
+
throw OTAUpdateDownloaderError(code: "OTA_INVALID_UPDATE", message: "updateJson must be valid JSON")
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let version = try normalizeVersion(stringValue(json["version"]))
|
|
447
|
+
let bundleUrl = stringValue(json["bundleUrl"])?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
448
|
+
if requireBundleUrl && (bundleUrl?.isEmpty ?? true) {
|
|
449
|
+
throw OTAUpdateDownloaderError(code: "OTA_INVALID_UPDATE", message: "bundleUrl must not be empty")
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
let rawAssetsUrl = stringValue(json["assetsUrl"])
|
|
453
|
+
let assetsUrl = rawAssetsUrl?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
454
|
+
|
|
455
|
+
let rawPlatform = stringValue(json["platform"])?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
456
|
+
if requireSigningFields && (rawPlatform?.isEmpty ?? true) {
|
|
457
|
+
throw OTAUpdateDownloaderError(code: "OTA_INVALID_UPDATE", message: "platform must not be empty")
|
|
458
|
+
}
|
|
459
|
+
if let rawPlatform = rawPlatform, !rawPlatform.isEmpty, rawPlatform != platformIOS {
|
|
460
|
+
throw OTAUpdateDownloaderError(code: "OTA_INVALID_UPDATE", message: "platform must be ios")
|
|
461
|
+
}
|
|
462
|
+
let platform = rawPlatform?.isEmpty == false ? rawPlatform! : platformIOS
|
|
463
|
+
|
|
464
|
+
let rawFileName = stringValue(json["fileName"])?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
465
|
+
if requireSigningFields && (rawFileName?.isEmpty ?? true) {
|
|
466
|
+
throw OTAUpdateDownloaderError(code: "OTA_INVALID_UPDATE", message: "fileName must not be empty")
|
|
467
|
+
}
|
|
468
|
+
if let rawFileName = rawFileName, !rawFileName.isEmpty, rawFileName != bundleFileName {
|
|
469
|
+
throw OTAUpdateDownloaderError(code: "OTA_INVALID_UPDATE", message: "fileName must be \(bundleFileName)")
|
|
470
|
+
}
|
|
471
|
+
let fileName = rawFileName?.isEmpty == false ? rawFileName! : bundleFileName
|
|
472
|
+
|
|
473
|
+
let sha256 = stringValue(json["sha256"])?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
474
|
+
if requireSha256 && (sha256?.isEmpty ?? true) {
|
|
475
|
+
try? markVerifyFailed(version: version, reason: reasonMissingSha256)
|
|
476
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: version)
|
|
477
|
+
throw OTAUpdateDownloaderError(code: "OTA_MISSING_SHA256", message: "OTA update \(version) is missing required sha256")
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
let assetsSha256 = stringValue(json["assetsSha256"])?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
481
|
+
if requireSha256 && (assetsUrl?.isEmpty == false) && (assetsSha256?.isEmpty ?? true) {
|
|
482
|
+
try? markVerifyFailed(version: version, reason: reasonMissingAssetsSha256)
|
|
483
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: version)
|
|
484
|
+
throw OTAUpdateDownloaderError(code: "OTA_MISSING_ASSETS_SHA256", message: "OTA update \(version) includes assetsUrl but is missing required assetsSha256")
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
let signature = stringValue(json["signature"])?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
488
|
+
if requireSignature && (signature?.isEmpty ?? true) {
|
|
489
|
+
try? markVerifyFailed(version: version, reason: reasonMissingSignature)
|
|
490
|
+
try? OTAUpdateCleanup.shared.cleanupTemp(version: version)
|
|
491
|
+
throw OTAUpdateDownloaderError(code: "OTA_MISSING_SIGNATURE", message: "OTA update \(version) is missing required signature")
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return OTAUpdatePayload(
|
|
495
|
+
version: version,
|
|
496
|
+
bundleUrl: bundleUrl,
|
|
497
|
+
assetsUrl: assetsUrl,
|
|
498
|
+
platform: platform,
|
|
499
|
+
fileName: fileName,
|
|
500
|
+
sha256: sha256,
|
|
501
|
+
assetsSha256: assetsSha256,
|
|
502
|
+
signature: signature
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private func validateVersionCanInstall(_ version: String) throws {
|
|
507
|
+
let metadata = try storage.readMetadata()
|
|
508
|
+
if metadata.activeBundleVersion == version || metadata.pendingBundleVersion == version {
|
|
509
|
+
throw OTAUpdateDownloaderError(
|
|
510
|
+
code: "OTA_VERSION_ALREADY_EXISTS",
|
|
511
|
+
message: "OTA version already exists as active or pending. Use a new version and recalculate hashes for every rebuild. version=\(version)"
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private func downloadToFile(urlString: String, destinationURL: URL, label: String = "OTA bundle") throws {
|
|
517
|
+
guard let url = URL(string: urlString) else {
|
|
518
|
+
throw OTAUpdateDownloaderError(code: "OTA_INVALID_UPDATE", message: "\(label) URL is invalid")
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
522
|
+
var result: Result<URL, Error>?
|
|
523
|
+
let configuration = URLSessionConfiguration.ephemeral
|
|
524
|
+
configuration.timeoutIntervalForRequest = 15
|
|
525
|
+
configuration.timeoutIntervalForResource = 60
|
|
526
|
+
let session = URLSession(configuration: configuration)
|
|
527
|
+
let task = session.downloadTask(with: url) { temporaryURL, response, error in
|
|
528
|
+
defer {
|
|
529
|
+
semaphore.signal()
|
|
530
|
+
}
|
|
531
|
+
if let error = error {
|
|
532
|
+
result = .failure(error)
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
if let response = response as? HTTPURLResponse, !(200...299).contains(response.statusCode) {
|
|
536
|
+
let code = label == "OTA assets ZIP" ? "OTA_ASSETS_DOWNLOAD_FAILED" : "OTA_DOWNLOAD_FAILED"
|
|
537
|
+
result = .failure(OTAUpdateDownloaderError(code: code, message: "HTTP \(response.statusCode) while downloading \(label)"))
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
guard let temporaryURL = temporaryURL else {
|
|
541
|
+
let code = label == "OTA assets ZIP" ? "OTA_ASSETS_DOWNLOAD_FAILED" : "OTA_DOWNLOAD_FAILED"
|
|
542
|
+
result = .failure(OTAUpdateDownloaderError(code: code, message: "URLSession did not produce a temporary file for \(label)"))
|
|
543
|
+
return
|
|
544
|
+
}
|
|
545
|
+
result = .success(temporaryURL)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
task.resume()
|
|
549
|
+
semaphore.wait()
|
|
550
|
+
session.finishTasksAndInvalidate()
|
|
551
|
+
|
|
552
|
+
switch result {
|
|
553
|
+
case .success(let temporaryURL):
|
|
554
|
+
try fileManager.moveItem(at: temporaryURL, to: destinationURL)
|
|
555
|
+
case .failure(let error):
|
|
556
|
+
if let otaError = error as? OTAUpdateDownloaderError {
|
|
557
|
+
throw otaError
|
|
558
|
+
}
|
|
559
|
+
let code = label == "OTA assets ZIP" ? "OTA_ASSETS_DOWNLOAD_FAILED" : "OTA_DOWNLOAD_FAILED"
|
|
560
|
+
throw OTAUpdateDownloaderError(code: code, message: "Failed to download \(label)", underlyingError: error)
|
|
561
|
+
case .none:
|
|
562
|
+
let code = label == "OTA assets ZIP" ? "OTA_ASSETS_DOWNLOAD_FAILED" : "OTA_DOWNLOAD_FAILED"
|
|
563
|
+
throw OTAUpdateDownloaderError(code: code, message: "\(label) download did not complete")
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private func tempBundleDirectory(version: String) throws -> URL {
|
|
568
|
+
try otaDirectory()
|
|
569
|
+
.appendingPathComponent("tmp", isDirectory: true)
|
|
570
|
+
.appendingPathComponent(version, isDirectory: true)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
private func finalBundleDirectory(version: String) throws -> URL {
|
|
574
|
+
try otaDirectory()
|
|
575
|
+
.appendingPathComponent("bundles", isDirectory: true)
|
|
576
|
+
.appendingPathComponent(version, isDirectory: true)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private func tempBundleURL(version: String) throws -> URL {
|
|
580
|
+
try tempBundleDirectory(version: version).appendingPathComponent(bundleFileName)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private func tempAssetsURL(version: String) throws -> URL {
|
|
584
|
+
try tempBundleDirectory(version: version).appendingPathComponent(assetsZipFileName)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
private func finalBundleURL(version: String) throws -> URL {
|
|
588
|
+
try finalBundleDirectory(version: version).appendingPathComponent(bundleFileName)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
private func finalAssetsDirectoryURL(version: String) throws -> URL {
|
|
592
|
+
try finalBundleDirectory(version: version).appendingPathComponent("assets", isDirectory: true)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private func otaDirectory() throws -> URL {
|
|
596
|
+
let supportURL = try fileManager.url(
|
|
597
|
+
for: .applicationSupportDirectory,
|
|
598
|
+
in: .userDomainMask,
|
|
599
|
+
appropriateFor: nil,
|
|
600
|
+
create: true
|
|
601
|
+
)
|
|
602
|
+
let otaURL = supportURL.appendingPathComponent("OTA", isDirectory: true)
|
|
603
|
+
try fileManager.createDirectory(at: otaURL, withIntermediateDirectories: true, attributes: nil)
|
|
604
|
+
return otaURL
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private func markVerifyFailed(version: String, reason: String) throws {
|
|
608
|
+
var metadata = try storage.readMetadata()
|
|
609
|
+
metadata.status = statusVerifyFailed
|
|
610
|
+
metadata.pendingBundleVersion = nil
|
|
611
|
+
metadata.failedBundleVersion = version
|
|
612
|
+
metadata.lastFailureReason = reason
|
|
613
|
+
try storage.writeMetadata(metadata)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private func fileExistsAndNonEmpty(_ url: URL) -> Bool {
|
|
617
|
+
fileManager.fileExists(atPath: url.path) && fileSize(url) > 0
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private func directoryExistsAndHasFiles(_ url: URL) -> Bool {
|
|
621
|
+
guard let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: nil) else {
|
|
622
|
+
return false
|
|
623
|
+
}
|
|
624
|
+
return enumerator.nextObject() != nil
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
private func fileSize(_ url: URL) -> UInt64 {
|
|
628
|
+
guard let attributes = try? fileManager.attributesOfItem(atPath: url.path),
|
|
629
|
+
let size = attributes[.size] as? NSNumber else {
|
|
630
|
+
return 0
|
|
631
|
+
}
|
|
632
|
+
return size.uint64Value
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private func directorySize(_ url: URL) -> UInt64 {
|
|
636
|
+
guard let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: [.fileSizeKey, .isRegularFileKey]) else {
|
|
637
|
+
return 0
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
var totalSize: UInt64 = 0
|
|
641
|
+
for case let fileURL as URL in enumerator {
|
|
642
|
+
let values = try? fileURL.resourceValues(forKeys: [.fileSizeKey, .isRegularFileKey])
|
|
643
|
+
if values?.isRegularFile == true, let fileSize = values?.fileSize {
|
|
644
|
+
totalSize += UInt64(fileSize)
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return totalSize
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
private func directoryFileCount(_ url: URL) -> Int {
|
|
651
|
+
guard let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey]) else {
|
|
652
|
+
return 0
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
var fileCount = 0
|
|
656
|
+
for case let fileURL as URL in enumerator {
|
|
657
|
+
let values = try? fileURL.resourceValues(forKeys: [.isRegularFileKey])
|
|
658
|
+
if values?.isRegularFile == true {
|
|
659
|
+
fileCount += 1
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return fileCount
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
private func fileSha256IfExists(_ url: URL) throws -> String? {
|
|
666
|
+
guard fileExistsAndNonEmpty(url) else {
|
|
667
|
+
return nil
|
|
668
|
+
}
|
|
669
|
+
return try OTAHashUtils.sha256(fileURL: url)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
private func normalizeVersion(_ version: String?) throws -> String {
|
|
673
|
+
let normalizedVersion = version?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
674
|
+
if normalizedVersion.isEmpty {
|
|
675
|
+
throw OTAUpdateDownloaderError(code: "OTA_INVALID_UPDATE", message: "version must not be empty")
|
|
676
|
+
}
|
|
677
|
+
if normalizedVersion.contains("/") || normalizedVersion.contains("\\") {
|
|
678
|
+
throw OTAUpdateDownloaderError(code: "OTA_INVALID_UPDATE", message: "version must not contain path separators")
|
|
679
|
+
}
|
|
680
|
+
return normalizedVersion
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private func stringValue(_ value: Any?) -> String? {
|
|
684
|
+
switch value {
|
|
685
|
+
case let value as String:
|
|
686
|
+
return value
|
|
687
|
+
case let value as NSNumber:
|
|
688
|
+
return value.stringValue
|
|
689
|
+
default:
|
|
690
|
+
return nil
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private func toNSError(_ error: Error) -> NSError {
|
|
695
|
+
if let otaError = error as? OTAUpdateDownloaderError {
|
|
696
|
+
return otaError.nsError
|
|
697
|
+
}
|
|
698
|
+
var userInfo: [String: Any] = [
|
|
699
|
+
NSLocalizedDescriptionKey: error.localizedDescription,
|
|
700
|
+
"otaCode": "OTA_DOWNLOAD_AND_INSTALL_FAILED",
|
|
701
|
+
]
|
|
702
|
+
userInfo[NSUnderlyingErrorKey] = error as NSError
|
|
703
|
+
return NSError(
|
|
704
|
+
domain: "OTAUpdateDownloader",
|
|
705
|
+
code: 1,
|
|
706
|
+
userInfo: userInfo
|
|
707
|
+
)
|
|
708
|
+
}
|
|
709
|
+
}
|