@onekeyfe/react-native-range-downloader 3.0.81-alpha.1 → 3.0.81-alpha.11
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/ReactNativeRangeDownloader.podspec +1 -2
- package/android/build.gradle +0 -1
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRules.kt +12 -5
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactOrphanSweep.kt +111 -0
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactStore.kt +194 -214
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ReactNativeRangeDownloader.kt +21 -21
- package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRulesTest.kt +39 -0
- package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactDeadlineTest.kt +26 -0
- package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactOrphanSweepTest.kt +107 -0
- package/ios/FirmwareArtifactStore.swift +667 -435
- package/ios/RangeDownloadLogic.swift +317 -0
- package/ios/ReactNativeRangeDownloader.swift +60 -484
- package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts +4 -7
- package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFirmwareArchiveMaterializeParams.hpp +7 -6
- package/nitrogen/generated/android/c++/JFirmwareArtifactDownloadParams.hpp +7 -7
- package/nitrogen/generated/android/c++/JFirmwareArtifactReceipt.hpp +7 -3
- package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.cpp +0 -19
- package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.hpp +0 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveMaterializeParams.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactDownloadParams.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactReceipt.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/HybridReactNativeRangeDownloaderSpec.kt +0 -4
- package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Bridge.hpp +15 -0
- package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Umbrella.hpp +0 -3
- package/nitrogen/generated/ios/c++/HybridReactNativeRangeDownloaderSpecSwift.hpp +0 -11
- package/nitrogen/generated/ios/swift/FirmwareArchiveMaterializeParams.swift +32 -13
- package/nitrogen/generated/ios/swift/FirmwareArtifactDownloadParams.swift +39 -8
- package/nitrogen/generated/ios/swift/FirmwareArtifactReceipt.swift +13 -2
- package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec.swift +0 -1
- package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec_cxx.swift +0 -19
- package/nitrogen/generated/shared/c++/FirmwareArchiveMaterializeParams.hpp +6 -5
- package/nitrogen/generated/shared/c++/FirmwareArtifactDownloadParams.hpp +9 -9
- package/nitrogen/generated/shared/c++/FirmwareArtifactReceipt.hpp +6 -2
- package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.cpp +0 -1
- package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.hpp +0 -4
- package/package.json +1 -2
- package/src/ReactNativeRangeDownloader.nitro.ts +7 -11
- package/ios/FirmwareBackgroundSessionEventRouter.swift +0 -17
- package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseReconcileParams.hpp +0 -76
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseReconcileParams.kt +0 -38
- package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseReconcileParams.swift +0 -48
- package/nitrogen/generated/shared/c++/FirmwareArtifactLeaseReconcileParams.hpp +0 -76
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
import CryptoKit
|
|
2
2
|
import Foundation
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
import ReactNativeNativeLogger
|
|
4
|
+
|
|
5
|
+
func isFirmwareArtifactTLSError(_ error: Error) -> Bool {
|
|
6
|
+
var current: NSError? = error as NSError
|
|
7
|
+
var visited = Set<ObjectIdentifier>()
|
|
8
|
+
while let candidate = current {
|
|
9
|
+
let identifier = ObjectIdentifier(candidate)
|
|
10
|
+
guard visited.insert(identifier).inserted else {
|
|
11
|
+
break
|
|
12
|
+
}
|
|
13
|
+
if candidate.domain == NSURLErrorDomain,
|
|
14
|
+
[
|
|
15
|
+
NSURLErrorSecureConnectionFailed,
|
|
16
|
+
NSURLErrorServerCertificateHasBadDate,
|
|
17
|
+
NSURLErrorServerCertificateUntrusted,
|
|
18
|
+
NSURLErrorServerCertificateHasUnknownRoot,
|
|
19
|
+
NSURLErrorServerCertificateNotYetValid,
|
|
20
|
+
NSURLErrorClientCertificateRejected,
|
|
21
|
+
NSURLErrorClientCertificateRequired,
|
|
22
|
+
NSURLErrorAppTransportSecurityRequiresSecureConnection,
|
|
23
|
+
].contains(candidate.code)
|
|
24
|
+
{
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
current = candidate.userInfo[NSUnderlyingErrorKey] as? NSError
|
|
28
|
+
}
|
|
29
|
+
return false
|
|
11
30
|
}
|
|
12
31
|
|
|
13
32
|
struct StoredFirmwareArtifact: Sendable {
|
|
@@ -29,7 +48,106 @@ private struct StagedFirmwareArchiveEntry {
|
|
|
29
48
|
let stagingURL: URL
|
|
30
49
|
}
|
|
31
50
|
|
|
32
|
-
private
|
|
51
|
+
private struct FirmwareArchiveRequirement {
|
|
52
|
+
let entryName: String
|
|
53
|
+
let expectedSize: Int64
|
|
54
|
+
let expectedSha256: String?
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private final class FirmwareArtifactStreamDelegate: NSObject, URLSessionDataDelegate {
|
|
58
|
+
private let partialURL: URL
|
|
59
|
+
private let hostname: String
|
|
60
|
+
private let resumeOffset: Int64
|
|
61
|
+
private let expectedSize: Int64?
|
|
62
|
+
private let maxBytes: Int64
|
|
63
|
+
private let isCancelled: () -> Bool
|
|
64
|
+
private let stateLock = NSLock()
|
|
65
|
+
private var continuation: CheckedContinuation<Void, Error>?
|
|
66
|
+
private var dataTask: URLSessionDataTask?
|
|
67
|
+
private var cancellationRequested = false
|
|
68
|
+
private var completed = false
|
|
69
|
+
private var responseAccepted = false
|
|
70
|
+
private var handle: FileHandle?
|
|
71
|
+
private var written: Int64 = 0
|
|
72
|
+
|
|
73
|
+
init(
|
|
74
|
+
partialURL: URL,
|
|
75
|
+
hostname: String,
|
|
76
|
+
resumeOffset: Int64,
|
|
77
|
+
expectedSize: Int64?,
|
|
78
|
+
maxBytes: Int64,
|
|
79
|
+
isCancelled: @escaping () -> Bool
|
|
80
|
+
) {
|
|
81
|
+
self.partialURL = partialURL
|
|
82
|
+
self.hostname = hostname
|
|
83
|
+
self.resumeOffset = resumeOffset
|
|
84
|
+
self.expectedSize = expectedSize
|
|
85
|
+
self.maxBytes = maxBytes
|
|
86
|
+
self.isCancelled = isCancelled
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func run(session: URLSession, request: URLRequest) async throws {
|
|
90
|
+
try await withTaskCancellationHandler {
|
|
91
|
+
try await withCheckedThrowingContinuation {
|
|
92
|
+
(continuation: CheckedContinuation<Void, Error>) in
|
|
93
|
+
let task = session.dataTask(with: request)
|
|
94
|
+
stateLock.lock()
|
|
95
|
+
if cancellationRequested || completed {
|
|
96
|
+
stateLock.unlock()
|
|
97
|
+
task.cancel()
|
|
98
|
+
continuation.resume(throwing: CancellationError())
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
self.continuation = continuation
|
|
102
|
+
dataTask = task
|
|
103
|
+
stateLock.unlock()
|
|
104
|
+
task.resume()
|
|
105
|
+
}
|
|
106
|
+
} onCancel: {
|
|
107
|
+
self.cancel()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private func cancel() {
|
|
112
|
+
stateLock.lock()
|
|
113
|
+
cancellationRequested = true
|
|
114
|
+
let task = dataTask
|
|
115
|
+
stateLock.unlock()
|
|
116
|
+
task?.cancel()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private func finish(_ result: Result<Void, Error>) {
|
|
120
|
+
stateLock.lock()
|
|
121
|
+
guard !completed else {
|
|
122
|
+
stateLock.unlock()
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
completed = true
|
|
126
|
+
let continuation = continuation
|
|
127
|
+
self.continuation = nil
|
|
128
|
+
dataTask = nil
|
|
129
|
+
let handle = handle
|
|
130
|
+
self.handle = nil
|
|
131
|
+
stateLock.unlock()
|
|
132
|
+
|
|
133
|
+
try? handle?.close()
|
|
134
|
+
continuation?.resume(with: result)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private func reject(
|
|
138
|
+
_ dataTask: URLSessionDataTask,
|
|
139
|
+
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void,
|
|
140
|
+
message: String
|
|
141
|
+
) {
|
|
142
|
+
completionHandler(.cancel)
|
|
143
|
+
fail(dataTask, message: message)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private func fail(_ task: URLSessionTask, message: String) {
|
|
147
|
+
task.cancel()
|
|
148
|
+
finish(.failure(FirmwareArtifactStoreError.downloadFailed(message)))
|
|
149
|
+
}
|
|
150
|
+
|
|
33
151
|
func urlSession(
|
|
34
152
|
_ session: URLSession,
|
|
35
153
|
task: URLSessionTask,
|
|
@@ -38,6 +156,193 @@ private final class FirmwareArtifactRedirectDelegate: NSObject, URLSessionTaskDe
|
|
|
38
156
|
completionHandler: @escaping (URLRequest?) -> Void
|
|
39
157
|
) {
|
|
40
158
|
completionHandler(nil)
|
|
159
|
+
fail(
|
|
160
|
+
task,
|
|
161
|
+
message:
|
|
162
|
+
"ARTIFACT_REDIRECT_REJECTED: firmware response changed canonical identity"
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
func urlSession(
|
|
167
|
+
_ session: URLSession,
|
|
168
|
+
dataTask: URLSessionDataTask,
|
|
169
|
+
didReceive response: URLResponse,
|
|
170
|
+
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
|
|
171
|
+
) {
|
|
172
|
+
guard !isCancelled() else {
|
|
173
|
+
reject(
|
|
174
|
+
dataTask,
|
|
175
|
+
completionHandler: completionHandler,
|
|
176
|
+
message: "ARTIFACT_CANCELLED: firmware artifact download was cancelled"
|
|
177
|
+
)
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
guard let httpResponse = response as? HTTPURLResponse else {
|
|
181
|
+
reject(
|
|
182
|
+
dataTask,
|
|
183
|
+
completionHandler: completionHandler,
|
|
184
|
+
message: "Firmware response is not HTTP"
|
|
185
|
+
)
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
guard
|
|
189
|
+
let responseURL = httpResponse.url,
|
|
190
|
+
responseURL.scheme?.lowercased() == "https",
|
|
191
|
+
responseURL.host?.lowercased() == hostname.lowercased(),
|
|
192
|
+
responseURL.port == nil || responseURL.port == 443
|
|
193
|
+
else {
|
|
194
|
+
reject(
|
|
195
|
+
dataTask,
|
|
196
|
+
completionHandler: completionHandler,
|
|
197
|
+
message:
|
|
198
|
+
"ARTIFACT_REDIRECT_REJECTED: firmware response changed canonical identity"
|
|
199
|
+
)
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
guard httpResponse.statusCode == 200 || httpResponse.statusCode == 206 else {
|
|
203
|
+
reject(
|
|
204
|
+
dataTask,
|
|
205
|
+
completionHandler: completionHandler,
|
|
206
|
+
message:
|
|
207
|
+
"ARTIFACT_HTTP_\(httpResponse.statusCode): firmware request failed"
|
|
208
|
+
)
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let append = resumeOffset > 0 && httpResponse.statusCode == 206
|
|
213
|
+
if httpResponse.statusCode == 206 {
|
|
214
|
+
guard
|
|
215
|
+
let contentRange = httpResponse.value(
|
|
216
|
+
forHTTPHeaderField: "Content-Range"
|
|
217
|
+
),
|
|
218
|
+
firmwareArtifactContentRangeIsValid(
|
|
219
|
+
contentRange,
|
|
220
|
+
expectedStart: append ? resumeOffset : 0,
|
|
221
|
+
expectedTotal: expectedSize,
|
|
222
|
+
maxBytes: maxBytes
|
|
223
|
+
)
|
|
224
|
+
else {
|
|
225
|
+
reject(
|
|
226
|
+
dataTask,
|
|
227
|
+
completionHandler: completionHandler,
|
|
228
|
+
message:
|
|
229
|
+
"ARTIFACT_PROTOCOL_INVALID: firmware resume Content-Range is invalid"
|
|
230
|
+
)
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let baseOffset = append ? resumeOffset : 0
|
|
236
|
+
guard
|
|
237
|
+
firmwareArtifactResponseFits(
|
|
238
|
+
expectedContentLength: httpResponse.expectedContentLength,
|
|
239
|
+
baseOffset: baseOffset,
|
|
240
|
+
maxBytes: maxBytes
|
|
241
|
+
)
|
|
242
|
+
else {
|
|
243
|
+
reject(
|
|
244
|
+
dataTask,
|
|
245
|
+
completionHandler: completionHandler,
|
|
246
|
+
message:
|
|
247
|
+
"ARTIFACT_PROTOCOL_INVALID: firmware artifact exceeds maxBytes"
|
|
248
|
+
)
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
do {
|
|
253
|
+
let handle = try FileHandle(forWritingTo: partialURL)
|
|
254
|
+
if append {
|
|
255
|
+
try handle.seekToEnd()
|
|
256
|
+
} else {
|
|
257
|
+
try handle.truncate(atOffset: 0)
|
|
258
|
+
}
|
|
259
|
+
self.handle = handle
|
|
260
|
+
written = baseOffset
|
|
261
|
+
responseAccepted = true
|
|
262
|
+
completionHandler(.allow)
|
|
263
|
+
} catch {
|
|
264
|
+
reject(
|
|
265
|
+
dataTask,
|
|
266
|
+
completionHandler: completionHandler,
|
|
267
|
+
message:
|
|
268
|
+
"ARTIFACT_NETWORK_FAILED: firmware partial file could not be opened"
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
func urlSession(
|
|
274
|
+
_ session: URLSession,
|
|
275
|
+
dataTask: URLSessionDataTask,
|
|
276
|
+
didReceive data: Data
|
|
277
|
+
) {
|
|
278
|
+
guard responseAccepted, let handle else {
|
|
279
|
+
fail(
|
|
280
|
+
dataTask,
|
|
281
|
+
message:
|
|
282
|
+
"ARTIFACT_PROTOCOL_INVALID: firmware response stream was not accepted"
|
|
283
|
+
)
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
guard !isCancelled() else {
|
|
287
|
+
fail(
|
|
288
|
+
dataTask,
|
|
289
|
+
message: "ARTIFACT_CANCELLED: firmware artifact download was cancelled"
|
|
290
|
+
)
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
guard
|
|
294
|
+
firmwareArtifactResponseFits(
|
|
295
|
+
expectedContentLength: Int64(data.count),
|
|
296
|
+
baseOffset: written,
|
|
297
|
+
maxBytes: maxBytes
|
|
298
|
+
)
|
|
299
|
+
else {
|
|
300
|
+
fail(
|
|
301
|
+
dataTask,
|
|
302
|
+
message:
|
|
303
|
+
"ARTIFACT_PROTOCOL_INVALID: firmware artifact exceeds maxBytes"
|
|
304
|
+
)
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
do {
|
|
308
|
+
try handle.write(contentsOf: data)
|
|
309
|
+
written += Int64(data.count)
|
|
310
|
+
} catch {
|
|
311
|
+
fail(
|
|
312
|
+
dataTask,
|
|
313
|
+
message:
|
|
314
|
+
"ARTIFACT_NETWORK_FAILED: firmware response stream could not be persisted"
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
func urlSession(
|
|
320
|
+
_ session: URLSession,
|
|
321
|
+
task: URLSessionTask,
|
|
322
|
+
didCompleteWithError error: Error?
|
|
323
|
+
) {
|
|
324
|
+
if let error {
|
|
325
|
+
finish(.failure(error))
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
guard responseAccepted, let handle else {
|
|
329
|
+
finish(
|
|
330
|
+
.failure(FirmwareArtifactStoreError.downloadFailed(
|
|
331
|
+
"ARTIFACT_PROTOCOL_INVALID: firmware response stream was not accepted"
|
|
332
|
+
))
|
|
333
|
+
)
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
do {
|
|
337
|
+
try handle.synchronize()
|
|
338
|
+
finish(.success(()))
|
|
339
|
+
} catch {
|
|
340
|
+
finish(
|
|
341
|
+
.failure(FirmwareArtifactStoreError.downloadFailed(
|
|
342
|
+
"ARTIFACT_NETWORK_FAILED: firmware partial file could not be synchronized"
|
|
343
|
+
))
|
|
344
|
+
)
|
|
345
|
+
}
|
|
41
346
|
}
|
|
42
347
|
}
|
|
43
348
|
|
|
@@ -78,11 +383,10 @@ private actor FirmwareArtifactDownloadCoordinator {
|
|
|
78
383
|
|
|
79
384
|
final class FirmwareArtifactStore {
|
|
80
385
|
static let shared = FirmwareArtifactStore()
|
|
81
|
-
static let maxReadBytes =
|
|
82
|
-
|
|
83
|
-
private static let
|
|
84
|
-
private static let
|
|
85
|
-
private static let partialArtifactGrace: TimeInterval = 7 * 24 * 60 * 60
|
|
386
|
+
static let maxReadBytes = Int(firmwareArtifactMaxReadBytes)
|
|
387
|
+
static let maxArtifactBytes = firmwareArtifactMaxBytes
|
|
388
|
+
private static let defaultDownloadDeadline: TimeInterval = 180
|
|
389
|
+
private static let maxDownloadDeadline: TimeInterval = 24 * 60 * 60
|
|
86
390
|
|
|
87
391
|
private struct OpenReader {
|
|
88
392
|
let handle: FileHandle
|
|
@@ -90,16 +394,11 @@ final class FirmwareArtifactStore {
|
|
|
90
394
|
let size: Int64
|
|
91
395
|
}
|
|
92
396
|
|
|
93
|
-
private struct LeaseState
|
|
397
|
+
private struct LeaseState {
|
|
94
398
|
let transactionId: String
|
|
95
399
|
var artifactRefs: Set<String>
|
|
96
400
|
}
|
|
97
401
|
|
|
98
|
-
private struct LeaseEnvelope: Codable {
|
|
99
|
-
let schemaVersion: Int
|
|
100
|
-
var leases: [String: LeaseState]
|
|
101
|
-
}
|
|
102
|
-
|
|
103
402
|
private let fileManager = FileManager.default
|
|
104
403
|
private let downloadCoordinator = FirmwareArtifactDownloadCoordinator()
|
|
105
404
|
private let leaseLock = NSLock()
|
|
@@ -109,6 +408,7 @@ final class FirmwareArtifactStore {
|
|
|
109
408
|
private var cancelledTransactions: Set<String> = []
|
|
110
409
|
private let readerLock = NSLock()
|
|
111
410
|
private var readers: [String: OpenReader] = [:]
|
|
411
|
+
private var leases: [String: LeaseState] = [:]
|
|
112
412
|
|
|
113
413
|
private lazy var rootURL: URL = {
|
|
114
414
|
let base = fileManager.urls(
|
|
@@ -122,7 +422,16 @@ final class FirmwareArtifactStore {
|
|
|
122
422
|
|
|
123
423
|
private init() {}
|
|
124
424
|
|
|
125
|
-
|
|
425
|
+
private struct ValidatedDownload {
|
|
426
|
+
let expectedSize: Int64?
|
|
427
|
+
let expectedSha256: String?
|
|
428
|
+
let maxBytes: Int64
|
|
429
|
+
let downloadToken: String
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private static func validateDownloadParams(
|
|
433
|
+
_ params: FirmwareArtifactDownloadParams
|
|
434
|
+
) throws -> ValidatedDownload {
|
|
126
435
|
guard
|
|
127
436
|
!params.taskId.isEmpty,
|
|
128
437
|
params.taskId.count <= 100,
|
|
@@ -150,47 +459,92 @@ final class FirmwareArtifactStore {
|
|
|
150
459
|
else {
|
|
151
460
|
throw FirmwareArtifactStoreError.invalidInput("Firmware URL must use HTTPS port 443")
|
|
152
461
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
462
|
+
let expectedSize: Int64?
|
|
463
|
+
if let value = params.expectedSize {
|
|
464
|
+
guard let converted = firmwareArtifactExactInt64(
|
|
465
|
+
value,
|
|
466
|
+
minimum: 1,
|
|
467
|
+
maximum: Self.maxArtifactBytes
|
|
468
|
+
) else {
|
|
469
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
470
|
+
"Invalid firmware artifact expected size"
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
expectedSize = converted
|
|
474
|
+
} else {
|
|
475
|
+
expectedSize = nil
|
|
476
|
+
}
|
|
477
|
+
guard let maxBytes = firmwareArtifactExactInt64(
|
|
478
|
+
params.maxBytes,
|
|
479
|
+
minimum: 1,
|
|
480
|
+
maximum: Self.maxArtifactBytes
|
|
481
|
+
) else {
|
|
162
482
|
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware artifact size")
|
|
163
483
|
}
|
|
164
|
-
guard
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
484
|
+
guard expectedSize.map({ $0 <= maxBytes }) ?? true else {
|
|
485
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
486
|
+
"Firmware artifact expected size exceeds maxBytes"
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
let expectedSha256 = params.expectedSha256?.lowercased()
|
|
490
|
+
if let expectedSha256 {
|
|
491
|
+
guard expectedSha256.range(
|
|
492
|
+
of: "^[a-f0-9]{64}$",
|
|
493
|
+
options: .regularExpression
|
|
494
|
+
) != nil else {
|
|
495
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
496
|
+
"Invalid firmware artifact SHA-256"
|
|
497
|
+
)
|
|
498
|
+
}
|
|
169
499
|
}
|
|
170
|
-
guard params.routeType == "domain"
|
|
500
|
+
guard params.routeType == "domain" else {
|
|
171
501
|
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware route type")
|
|
172
502
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
503
|
+
let deadline = params.overallDeadlineSeconds ?? defaultDownloadDeadline
|
|
504
|
+
guard
|
|
505
|
+
deadline.isFinite,
|
|
506
|
+
deadline > 0,
|
|
507
|
+
deadline <= maxDownloadDeadline
|
|
508
|
+
else {
|
|
509
|
+
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware download deadline")
|
|
510
|
+
}
|
|
511
|
+
if params.resolvedIp != nil {
|
|
178
512
|
throw FirmwareArtifactStoreError.invalidInput("Domain route must not include resolvedIp")
|
|
179
513
|
}
|
|
514
|
+
return ValidatedDownload(
|
|
515
|
+
expectedSize: expectedSize,
|
|
516
|
+
expectedSha256: expectedSha256,
|
|
517
|
+
maxBytes: maxBytes,
|
|
518
|
+
downloadToken: firmwareArtifactDownloadToken(
|
|
519
|
+
expectedSha256: expectedSha256,
|
|
520
|
+
url: params.url
|
|
521
|
+
)
|
|
522
|
+
)
|
|
180
523
|
}
|
|
181
524
|
|
|
182
525
|
func download(_ params: FirmwareArtifactDownloadParams) async throws -> StoredFirmwareArtifact {
|
|
183
|
-
try Self.validateDownloadParams(params)
|
|
526
|
+
let validated = try Self.validateDownloadParams(params)
|
|
184
527
|
try rejectIfCancelled(transactionId: params.transactionId)
|
|
185
|
-
let expectedSha256 =
|
|
186
|
-
|
|
187
|
-
|
|
528
|
+
if let expectedSha256 = validated.expectedSha256 {
|
|
529
|
+
try retainExpectedArtifact(
|
|
530
|
+
leaseRef: params.leaseRef,
|
|
531
|
+
transactionId: params.transactionId,
|
|
532
|
+
artifactRef: "fw:\(expectedSha256)"
|
|
533
|
+
)
|
|
534
|
+
} else {
|
|
535
|
+
try requireLeaseTransaction(
|
|
536
|
+
leaseRef: params.leaseRef,
|
|
537
|
+
transactionId: params.transactionId
|
|
538
|
+
)
|
|
539
|
+
}
|
|
540
|
+
let key = firmwareArtifactDownloadKey(
|
|
188
541
|
transactionId: params.transactionId,
|
|
189
|
-
|
|
542
|
+
taskId: params.taskId,
|
|
543
|
+
expectedSize: validated.expectedSize,
|
|
544
|
+
expectedSha256: validated.expectedSha256,
|
|
545
|
+
downloadToken: validated.downloadToken
|
|
190
546
|
)
|
|
191
|
-
|
|
192
|
-
"\(params.transactionId):\(expectedSha256):\(Int64(params.expectedSize))"
|
|
193
|
-
markDownloadActive(expectedSha256, delta: 1)
|
|
547
|
+
markDownloadActive(validated.downloadToken, delta: 1)
|
|
194
548
|
do {
|
|
195
549
|
let artifact = try await downloadCoordinator.run(
|
|
196
550
|
key: key,
|
|
@@ -199,13 +553,23 @@ final class FirmwareArtifactStore {
|
|
|
199
553
|
try rejectIfCancelled(transactionId: params.transactionId)
|
|
200
554
|
return try await downloadLocked(
|
|
201
555
|
params,
|
|
202
|
-
|
|
556
|
+
validated: validated
|
|
203
557
|
)
|
|
204
558
|
}
|
|
205
|
-
|
|
559
|
+
try rejectIfCancelled(transactionId: params.transactionId)
|
|
560
|
+
try retainExpectedArtifact(
|
|
561
|
+
leaseRef: params.leaseRef,
|
|
562
|
+
transactionId: params.transactionId,
|
|
563
|
+
artifactRef: artifact.artifactRef
|
|
564
|
+
)
|
|
565
|
+
markDownloadActive(validated.downloadToken, delta: -1)
|
|
206
566
|
return artifact
|
|
207
567
|
} catch {
|
|
208
|
-
markDownloadActive(
|
|
568
|
+
markDownloadActive(validated.downloadToken, delta: -1)
|
|
569
|
+
OneKeyLog.error(
|
|
570
|
+
"FirmwareArtifact",
|
|
571
|
+
"event=download_failed transactionId=\(params.transactionId) artifactId=\(params.artifactId) route=\(params.routeType) errorType=\(String(describing: type(of: error)))"
|
|
572
|
+
)
|
|
209
573
|
if error is CancellationError ||
|
|
210
574
|
isTransactionCancelled(params.transactionId) {
|
|
211
575
|
throw FirmwareArtifactStoreError.downloadFailed(
|
|
@@ -216,6 +580,32 @@ final class FirmwareArtifactStore {
|
|
|
216
580
|
}
|
|
217
581
|
}
|
|
218
582
|
|
|
583
|
+
func cachedArtifact(
|
|
584
|
+
_ params: FirmwareArtifactDownloadParams
|
|
585
|
+
) throws -> StoredFirmwareArtifact? {
|
|
586
|
+
let validated = try Self.validateDownloadParams(params)
|
|
587
|
+
try rejectIfCancelled(transactionId: params.transactionId)
|
|
588
|
+
guard let expectedSha256 = validated.expectedSha256 else {
|
|
589
|
+
return nil
|
|
590
|
+
}
|
|
591
|
+
let finalURL = artifactURL(sha256: expectedSha256)
|
|
592
|
+
guard let artifact = try? validateDownloadedArtifact(
|
|
593
|
+
fileURL: finalURL,
|
|
594
|
+
expectedSize: validated.expectedSize,
|
|
595
|
+
expectedSha256: expectedSha256,
|
|
596
|
+
maxBytes: validated.maxBytes
|
|
597
|
+
) else {
|
|
598
|
+
return nil
|
|
599
|
+
}
|
|
600
|
+
try retainExpectedArtifact(
|
|
601
|
+
leaseRef: params.leaseRef,
|
|
602
|
+
transactionId: params.transactionId,
|
|
603
|
+
artifactRef: artifact.artifactRef
|
|
604
|
+
)
|
|
605
|
+
try rejectIfCancelled(transactionId: params.transactionId)
|
|
606
|
+
return artifact
|
|
607
|
+
}
|
|
608
|
+
|
|
219
609
|
func cancelDownloads(transactionId: String) async throws {
|
|
220
610
|
guard Self.isSafeIdentifier(transactionId) else {
|
|
221
611
|
throw FirmwareArtifactStoreError.invalidInput(
|
|
@@ -226,49 +616,55 @@ final class FirmwareArtifactStore {
|
|
|
226
616
|
cancelledTransactions.insert(transactionId)
|
|
227
617
|
}
|
|
228
618
|
await downloadCoordinator.cancel(transactionId: transactionId)
|
|
229
|
-
try await RangeDownloader.shared.cancelFirmwareArtifactDownloads(
|
|
230
|
-
transactionId: transactionId
|
|
231
|
-
)
|
|
232
619
|
}
|
|
233
620
|
|
|
234
621
|
private func downloadLocked(
|
|
235
622
|
_ params: FirmwareArtifactDownloadParams,
|
|
236
|
-
|
|
623
|
+
validated: ValidatedDownload
|
|
237
624
|
) async throws -> StoredFirmwareArtifact {
|
|
238
|
-
let
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return try await RangeDownloader.shared.downloadFirmwareArtifact(
|
|
249
|
-
params: params
|
|
250
|
-
)
|
|
625
|
+
if let expectedSha256 = validated.expectedSha256 {
|
|
626
|
+
let finalURL = artifactURL(sha256: expectedSha256)
|
|
627
|
+
if let existing = try? validateDownloadedArtifact(
|
|
628
|
+
fileURL: finalURL,
|
|
629
|
+
expectedSize: validated.expectedSize,
|
|
630
|
+
expectedSha256: expectedSha256,
|
|
631
|
+
maxBytes: validated.maxBytes
|
|
632
|
+
) {
|
|
633
|
+
return existing
|
|
634
|
+
}
|
|
251
635
|
}
|
|
252
636
|
|
|
637
|
+
// Firmware preflight is foreground-bound, so both routes use the same
|
|
638
|
+
// cancellable stream instead of waiting on process-owned background tasks.
|
|
253
639
|
let partialURL = rootURL.appendingPathComponent(
|
|
254
|
-
|
|
640
|
+
firmwareArtifactPartialFileName(
|
|
641
|
+
transactionId: params.transactionId,
|
|
642
|
+
taskId: params.taskId,
|
|
643
|
+
downloadToken: validated.downloadToken
|
|
644
|
+
),
|
|
255
645
|
isDirectory: false
|
|
256
646
|
)
|
|
257
647
|
if !fileManager.fileExists(atPath: partialURL.path) {
|
|
258
648
|
fileManager.createFile(atPath: partialURL.path, contents: nil)
|
|
259
649
|
}
|
|
260
650
|
var currentSize = try fileSize(partialURL)
|
|
261
|
-
if currentSize >
|
|
651
|
+
if validated.expectedSha256 == nil && currentSize > 0 {
|
|
652
|
+
try fileManager.removeItem(at: partialURL)
|
|
653
|
+
fileManager.createFile(atPath: partialURL.path, contents: nil)
|
|
654
|
+
currentSize = 0
|
|
655
|
+
} else if currentSize > validated.maxBytes {
|
|
262
656
|
try fileManager.removeItem(at: partialURL)
|
|
263
657
|
fileManager.createFile(atPath: partialURL.path, contents: nil)
|
|
264
658
|
currentSize = 0
|
|
265
659
|
}
|
|
266
|
-
if currentSize ==
|
|
267
|
-
if let completed = try?
|
|
660
|
+
if let expectedSize = validated.expectedSize, currentSize == expectedSize {
|
|
661
|
+
if let completed = try? validateDownloadedArtifact(
|
|
268
662
|
fileURL: partialURL,
|
|
269
|
-
expectedSize:
|
|
270
|
-
expectedSha256: expectedSha256
|
|
663
|
+
expectedSize: expectedSize,
|
|
664
|
+
expectedSha256: validated.expectedSha256,
|
|
665
|
+
maxBytes: validated.maxBytes
|
|
271
666
|
) {
|
|
667
|
+
let finalURL = artifactURL(sha256: completed.sha256)
|
|
272
668
|
try promote(source: partialURL, destination: finalURL)
|
|
273
669
|
return StoredFirmwareArtifact(
|
|
274
670
|
artifactRef: completed.artifactRef,
|
|
@@ -282,22 +678,33 @@ final class FirmwareArtifactStore {
|
|
|
282
678
|
currentSize = 0
|
|
283
679
|
}
|
|
284
680
|
|
|
681
|
+
OneKeyLog.info(
|
|
682
|
+
"FirmwareArtifact",
|
|
683
|
+
"event=stream_start transactionId=\(params.transactionId) artifactId=\(params.artifactId) route=\(params.routeType) expectedBytes=\(validated.expectedSize ?? -1) resumeBytes=\(currentSize)"
|
|
684
|
+
)
|
|
285
685
|
try await streamDownload(
|
|
286
686
|
params,
|
|
287
687
|
partialURL: partialURL,
|
|
288
|
-
resumeOffset: min(currentSize,
|
|
688
|
+
resumeOffset: min(currentSize, validated.maxBytes),
|
|
689
|
+
validated: validated
|
|
690
|
+
)
|
|
691
|
+
OneKeyLog.info(
|
|
692
|
+
"FirmwareArtifact",
|
|
693
|
+
"event=stream_complete transactionId=\(params.transactionId) artifactId=\(params.artifactId) route=\(params.routeType) expectedBytes=\(validated.expectedSize ?? -1)"
|
|
289
694
|
)
|
|
290
695
|
let artifact: StoredFirmwareArtifact
|
|
291
696
|
do {
|
|
292
|
-
artifact = try
|
|
697
|
+
artifact = try validateDownloadedArtifact(
|
|
293
698
|
fileURL: partialURL,
|
|
294
|
-
expectedSize:
|
|
295
|
-
expectedSha256: expectedSha256
|
|
699
|
+
expectedSize: validated.expectedSize,
|
|
700
|
+
expectedSha256: validated.expectedSha256,
|
|
701
|
+
maxBytes: validated.maxBytes
|
|
296
702
|
)
|
|
297
703
|
} catch {
|
|
298
704
|
try? fileManager.removeItem(at: partialURL)
|
|
299
705
|
throw error
|
|
300
706
|
}
|
|
707
|
+
let finalURL = artifactURL(sha256: artifact.sha256)
|
|
301
708
|
try promote(source: partialURL, destination: finalURL)
|
|
302
709
|
return StoredFirmwareArtifact(
|
|
303
710
|
artifactRef: artifact.artifactRef,
|
|
@@ -307,67 +714,11 @@ final class FirmwareArtifactStore {
|
|
|
307
714
|
)
|
|
308
715
|
}
|
|
309
716
|
|
|
310
|
-
func acceptBackgroundDownload(
|
|
311
|
-
temporaryURL: URL,
|
|
312
|
-
leaseRef: String,
|
|
313
|
-
transactionId: String,
|
|
314
|
-
expectedSize: Int64,
|
|
315
|
-
expectedSha256: String
|
|
316
|
-
) throws -> StoredFirmwareArtifact {
|
|
317
|
-
let normalizedExpectedSha256 = expectedSha256.lowercased()
|
|
318
|
-
try retainExpectedArtifact(
|
|
319
|
-
leaseRef: leaseRef,
|
|
320
|
-
transactionId: transactionId,
|
|
321
|
-
artifactRef: "fw:\(normalizedExpectedSha256)"
|
|
322
|
-
)
|
|
323
|
-
let finalURL = artifactURL(sha256: normalizedExpectedSha256)
|
|
324
|
-
if let existing = try? validateStoredArtifact(
|
|
325
|
-
fileURL: finalURL,
|
|
326
|
-
expectedSize: expectedSize,
|
|
327
|
-
expectedSha256: normalizedExpectedSha256
|
|
328
|
-
) {
|
|
329
|
-
return existing
|
|
330
|
-
}
|
|
331
|
-
let artifact = try validateStoredArtifact(
|
|
332
|
-
fileURL: temporaryURL,
|
|
333
|
-
expectedSize: expectedSize,
|
|
334
|
-
expectedSha256: normalizedExpectedSha256
|
|
335
|
-
)
|
|
336
|
-
try promote(source: temporaryURL, destination: finalURL)
|
|
337
|
-
return StoredFirmwareArtifact(
|
|
338
|
-
artifactRef: artifact.artifactRef,
|
|
339
|
-
size: artifact.size,
|
|
340
|
-
sha256: artifact.sha256,
|
|
341
|
-
fileURL: finalURL
|
|
342
|
-
)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
func storedArtifact(
|
|
346
|
-
expectedSize: Int64,
|
|
347
|
-
expectedSha256: String
|
|
348
|
-
) throws -> StoredFirmwareArtifact? {
|
|
349
|
-
let finalURL = artifactURL(sha256: expectedSha256)
|
|
350
|
-
guard fileManager.fileExists(atPath: finalURL.path) else {
|
|
351
|
-
return nil
|
|
352
|
-
}
|
|
353
|
-
return try validateStoredArtifact(
|
|
354
|
-
fileURL: finalURL,
|
|
355
|
-
expectedSize: expectedSize,
|
|
356
|
-
expectedSha256: expectedSha256
|
|
357
|
-
)
|
|
358
|
-
}
|
|
359
|
-
|
|
360
717
|
func discard(artifactRef: String) throws {
|
|
361
718
|
let fileURL = try resolveArtifactURL(artifactRef)
|
|
362
719
|
leaseLock.lock()
|
|
363
|
-
let isRetained
|
|
364
|
-
|
|
365
|
-
isRetained = try loadLeasesLocked().leases.values.contains {
|
|
366
|
-
$0.artifactRefs.contains(artifactRef)
|
|
367
|
-
}
|
|
368
|
-
} catch {
|
|
369
|
-
leaseLock.unlock()
|
|
370
|
-
throw error
|
|
720
|
+
let isRetained = leases.values.contains {
|
|
721
|
+
$0.artifactRefs.contains(artifactRef)
|
|
371
722
|
}
|
|
372
723
|
leaseLock.unlock()
|
|
373
724
|
guard !isRetained else {
|
|
@@ -447,7 +798,7 @@ final class FirmwareArtifactStore {
|
|
|
447
798
|
func materializeArchive(
|
|
448
799
|
leaseRef: String,
|
|
449
800
|
artifactRef: String,
|
|
450
|
-
expectedEntries: [FirmwareArchiveExpectedEntry]
|
|
801
|
+
expectedEntries: [FirmwareArchiveExpectedEntry]?
|
|
451
802
|
) throws -> [StoredFirmwareArchiveEntry] {
|
|
452
803
|
try requireLease(leaseRef)
|
|
453
804
|
let archiveURL = try resolveArtifactURL(artifactRef)
|
|
@@ -458,10 +809,13 @@ final class FirmwareArtifactStore {
|
|
|
458
809
|
try fileManager.createDirectory(at: scratchURL, withIntermediateDirectories: true)
|
|
459
810
|
defer { try? fileManager.removeItem(at: scratchURL) }
|
|
460
811
|
|
|
461
|
-
let requirements = try validateArchiveRequirements(expectedEntries)
|
|
462
812
|
let archiveEntries = try FirmwareArchiveMinizipBridge.scanArchive(
|
|
463
813
|
atPath: archiveURL.path
|
|
464
814
|
)
|
|
815
|
+
let requirements = try resolveArchiveRequirements(
|
|
816
|
+
expectedEntries,
|
|
817
|
+
archiveEntries: archiveEntries
|
|
818
|
+
)
|
|
465
819
|
try validateArchiveEntries(
|
|
466
820
|
archiveEntries,
|
|
467
821
|
requirements: requirements
|
|
@@ -488,7 +842,7 @@ final class FirmwareArtifactStore {
|
|
|
488
842
|
}
|
|
489
843
|
do {
|
|
490
844
|
actualSize += Int64(chunk.count)
|
|
491
|
-
guard actualSize <=
|
|
845
|
+
guard actualSize <= requirement.expectedSize else {
|
|
492
846
|
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
493
847
|
"Firmware archive entry exceeds its expected size"
|
|
494
848
|
)
|
|
@@ -514,8 +868,8 @@ final class FirmwareArtifactStore {
|
|
|
514
868
|
String(format: "%02x", $0)
|
|
515
869
|
}.joined()
|
|
516
870
|
guard
|
|
517
|
-
actualSize ==
|
|
518
|
-
|
|
871
|
+
actualSize == requirement.expectedSize,
|
|
872
|
+
requirement.expectedSha256.map({ sha256 == $0 }) ?? true
|
|
519
873
|
else {
|
|
520
874
|
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
521
875
|
"Firmware archive entry integrity mismatch"
|
|
@@ -566,18 +920,16 @@ final class FirmwareArtifactStore {
|
|
|
566
920
|
}
|
|
567
921
|
leaseLock.lock()
|
|
568
922
|
defer { leaseLock.unlock() }
|
|
569
|
-
|
|
570
|
-
guard envelope.leases.count < 32 else {
|
|
923
|
+
guard leases.count < 32 else {
|
|
571
924
|
throw FirmwareArtifactStoreError.invalidInput(
|
|
572
925
|
"Too many firmware artifact leases"
|
|
573
926
|
)
|
|
574
927
|
}
|
|
575
928
|
let leaseRef = "fwlease:\(UUID().uuidString.lowercased())"
|
|
576
|
-
|
|
929
|
+
leases[leaseRef] = LeaseState(
|
|
577
930
|
transactionId: transactionId,
|
|
578
931
|
artifactRefs: []
|
|
579
932
|
)
|
|
580
|
-
try saveLeasesLocked(envelope)
|
|
581
933
|
return leaseRef
|
|
582
934
|
}
|
|
583
935
|
|
|
@@ -603,9 +955,8 @@ final class FirmwareArtifactStore {
|
|
|
603
955
|
let transactionId: String = try {
|
|
604
956
|
leaseLock.lock()
|
|
605
957
|
defer { leaseLock.unlock() }
|
|
606
|
-
var envelope = try loadLeasesLocked()
|
|
607
958
|
guard
|
|
608
|
-
let lease =
|
|
959
|
+
let lease = leases.removeValue(
|
|
609
960
|
forKey: try validateLeaseRef(leaseRef)
|
|
610
961
|
)
|
|
611
962
|
else {
|
|
@@ -613,7 +964,6 @@ final class FirmwareArtifactStore {
|
|
|
613
964
|
"Firmware artifact lease is unavailable"
|
|
614
965
|
)
|
|
615
966
|
}
|
|
616
|
-
try saveLeasesLocked(envelope)
|
|
617
967
|
return lease.transactionId
|
|
618
968
|
}()
|
|
619
969
|
cancellationLock.withFirmwareArtifactLock {
|
|
@@ -621,43 +971,13 @@ final class FirmwareArtifactStore {
|
|
|
621
971
|
}
|
|
622
972
|
}
|
|
623
973
|
|
|
624
|
-
func reconcileLeases(activeLeaseRefs: [String]) throws {
|
|
625
|
-
guard activeLeaseRefs.count <= 32 else {
|
|
626
|
-
throw FirmwareArtifactStoreError.invalidInput(
|
|
627
|
-
"Too many active firmware artifact leases"
|
|
628
|
-
)
|
|
629
|
-
}
|
|
630
|
-
let active = try Set(activeLeaseRefs.map(validateLeaseRef))
|
|
631
|
-
guard active.count == activeLeaseRefs.count else {
|
|
632
|
-
throw FirmwareArtifactStoreError.invalidInput(
|
|
633
|
-
"Duplicate active firmware artifact lease"
|
|
634
|
-
)
|
|
635
|
-
}
|
|
636
|
-
leaseLock.lock()
|
|
637
|
-
defer { leaseLock.unlock() }
|
|
638
|
-
var envelope = try loadLeasesLocked()
|
|
639
|
-
guard active.allSatisfy({ envelope.leases[$0] != nil }) else {
|
|
640
|
-
throw FirmwareArtifactStoreError.invalidInput(
|
|
641
|
-
"Firmware artifact lease reconciliation is incomplete"
|
|
642
|
-
)
|
|
643
|
-
}
|
|
644
|
-
envelope.leases = envelope.leases.filter { active.contains($0.key) }
|
|
645
|
-
try saveLeasesLocked(envelope)
|
|
646
|
-
}
|
|
647
|
-
|
|
648
974
|
func sweepOrphans() throws -> (deletedFiles: Int, deletedBytes: Int64) {
|
|
649
975
|
leaseLock.lock()
|
|
650
|
-
let retained
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
.map { String($0.dropFirst(3)) }
|
|
656
|
-
)
|
|
657
|
-
} catch {
|
|
658
|
-
leaseLock.unlock()
|
|
659
|
-
throw error
|
|
660
|
-
}
|
|
976
|
+
let retained = Set(
|
|
977
|
+
leases.values
|
|
978
|
+
.flatMap(\.artifactRefs)
|
|
979
|
+
.map { String($0.dropFirst(3)) }
|
|
980
|
+
)
|
|
661
981
|
leaseLock.unlock()
|
|
662
982
|
activeDownloadLock.lock()
|
|
663
983
|
let active = Set(activeDownloadCounts.filter { $0.value > 0 }.keys)
|
|
@@ -666,61 +986,41 @@ final class FirmwareArtifactStore {
|
|
|
666
986
|
let openPaths = Set(readers.values.map(\.fileURL.path))
|
|
667
987
|
readerLock.unlock()
|
|
668
988
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
options: [.skipsHiddenFiles]
|
|
989
|
+
return try sweepFirmwareArtifactOrphansAtRoot(
|
|
990
|
+
rootURL,
|
|
991
|
+
retainedSha256: retained,
|
|
992
|
+
activeSha256: active,
|
|
993
|
+
openPaths: openPaths,
|
|
994
|
+
fileManager: fileManager
|
|
676
995
|
)
|
|
677
|
-
for fileURL in files {
|
|
678
|
-
let name = fileURL.lastPathComponent
|
|
679
|
-
guard name != "leases.json", name.count >= 64 else { continue }
|
|
680
|
-
let sha256 = String(name.prefix(64))
|
|
681
|
-
guard
|
|
682
|
-
sha256.range(of: "^[a-f0-9]{64}$", options: .regularExpression) != nil,
|
|
683
|
-
!retained.contains(sha256),
|
|
684
|
-
!active.contains(sha256),
|
|
685
|
-
!openPaths.contains(fileURL.path)
|
|
686
|
-
else {
|
|
687
|
-
continue
|
|
688
|
-
}
|
|
689
|
-
let grace: TimeInterval
|
|
690
|
-
if name.hasSuffix(".bin") {
|
|
691
|
-
grace = Self.finalArtifactGrace
|
|
692
|
-
} else if name.hasSuffix(".partial") {
|
|
693
|
-
grace = Self.partialArtifactGrace
|
|
694
|
-
} else {
|
|
695
|
-
continue
|
|
696
|
-
}
|
|
697
|
-
let values = try fileURL.resourceValues(
|
|
698
|
-
forKeys: [.contentModificationDateKey, .fileSizeKey]
|
|
699
|
-
)
|
|
700
|
-
guard
|
|
701
|
-
let modifiedAt = values.contentModificationDate,
|
|
702
|
-
now.timeIntervalSince(modifiedAt) >= grace
|
|
703
|
-
else {
|
|
704
|
-
continue
|
|
705
|
-
}
|
|
706
|
-
let size = Int64(values.fileSize ?? 0)
|
|
707
|
-
try fileManager.removeItem(at: fileURL)
|
|
708
|
-
deletedFiles += 1
|
|
709
|
-
deletedBytes += size
|
|
710
|
-
}
|
|
711
|
-
return (deletedFiles, deletedBytes)
|
|
712
996
|
}
|
|
713
997
|
|
|
714
998
|
private func requireLease(_ leaseRef: String) throws {
|
|
715
999
|
leaseLock.lock()
|
|
716
1000
|
defer { leaseLock.unlock() }
|
|
717
|
-
guard try
|
|
1001
|
+
guard leases[try validateLeaseRef(leaseRef)] != nil else {
|
|
718
1002
|
throw FirmwareArtifactStoreError.invalidInput(
|
|
719
1003
|
"Firmware artifact lease is unavailable"
|
|
720
1004
|
)
|
|
721
1005
|
}
|
|
722
1006
|
}
|
|
723
1007
|
|
|
1008
|
+
private func requireLeaseTransaction(
|
|
1009
|
+
leaseRef: String,
|
|
1010
|
+
transactionId: String
|
|
1011
|
+
) throws {
|
|
1012
|
+
leaseLock.lock()
|
|
1013
|
+
defer { leaseLock.unlock() }
|
|
1014
|
+
guard
|
|
1015
|
+
let lease = leases[try validateLeaseRef(leaseRef)],
|
|
1016
|
+
lease.transactionId == transactionId
|
|
1017
|
+
else {
|
|
1018
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
1019
|
+
"Firmware artifact lease transaction mismatch"
|
|
1020
|
+
)
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
724
1024
|
private func retainExpectedArtifact(
|
|
725
1025
|
leaseRef: String,
|
|
726
1026
|
transactionId: String?,
|
|
@@ -736,9 +1036,8 @@ final class FirmwareArtifactStore {
|
|
|
736
1036
|
}
|
|
737
1037
|
leaseLock.lock()
|
|
738
1038
|
defer { leaseLock.unlock() }
|
|
739
|
-
var envelope = try loadLeasesLocked()
|
|
740
1039
|
let validatedLeaseRef = try validateLeaseRef(leaseRef)
|
|
741
|
-
guard var lease =
|
|
1040
|
+
guard var lease = leases[validatedLeaseRef] else {
|
|
742
1041
|
throw FirmwareArtifactStoreError.invalidInput(
|
|
743
1042
|
"Firmware artifact lease is unavailable"
|
|
744
1043
|
)
|
|
@@ -749,8 +1048,7 @@ final class FirmwareArtifactStore {
|
|
|
749
1048
|
)
|
|
750
1049
|
}
|
|
751
1050
|
if lease.artifactRefs.insert(artifactRef).inserted {
|
|
752
|
-
|
|
753
|
-
try saveLeasesLocked(envelope)
|
|
1051
|
+
leases[validatedLeaseRef] = lease
|
|
754
1052
|
}
|
|
755
1053
|
}
|
|
756
1054
|
|
|
@@ -770,84 +1068,8 @@ final class FirmwareArtifactStore {
|
|
|
770
1068
|
) != nil
|
|
771
1069
|
}
|
|
772
1070
|
|
|
773
|
-
|
|
774
|
-
value
|
|
775
|
-
of: "^[A-Za-z0-9._:-]{1,160}$",
|
|
776
|
-
options: .regularExpression
|
|
777
|
-
) != nil
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
private func loadLeasesLocked() throws -> LeaseEnvelope {
|
|
781
|
-
let url = rootURL.appendingPathComponent("leases.json", isDirectory: false)
|
|
782
|
-
guard fileManager.fileExists(atPath: url.path) else {
|
|
783
|
-
return LeaseEnvelope(schemaVersion: 1, leases: [:])
|
|
784
|
-
}
|
|
785
|
-
let fileSize = try url.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0
|
|
786
|
-
guard fileSize > 0, Int64(fileSize) <= Self.maxLeaseMetadataBytes else {
|
|
787
|
-
throw FirmwareArtifactStoreError.invalidInput(
|
|
788
|
-
"Firmware lease metadata is too large"
|
|
789
|
-
)
|
|
790
|
-
}
|
|
791
|
-
let envelope = try JSONDecoder().decode(
|
|
792
|
-
LeaseEnvelope.self,
|
|
793
|
-
from: Data(contentsOf: url)
|
|
794
|
-
)
|
|
795
|
-
guard envelope.schemaVersion == 1, envelope.leases.count <= 32 else {
|
|
796
|
-
throw FirmwareArtifactStoreError.invalidInput(
|
|
797
|
-
"Unsupported firmware lease schema"
|
|
798
|
-
)
|
|
799
|
-
}
|
|
800
|
-
guard
|
|
801
|
-
envelope.leases.values.reduce(0, { $0 + $1.artifactRefs.count })
|
|
802
|
-
<= Self.maxTotalLeaseRefs
|
|
803
|
-
else {
|
|
804
|
-
throw FirmwareArtifactStoreError.invalidInput(
|
|
805
|
-
"Firmware lease metadata is too large"
|
|
806
|
-
)
|
|
807
|
-
}
|
|
808
|
-
for (leaseRef, lease) in envelope.leases {
|
|
809
|
-
guard
|
|
810
|
-
Self.isValidLeaseRef(leaseRef),
|
|
811
|
-
Self.isSafeIdentifier(lease.transactionId),
|
|
812
|
-
lease.artifactRefs.count <= 4096,
|
|
813
|
-
lease.artifactRefs.allSatisfy({
|
|
814
|
-
$0.range(
|
|
815
|
-
of: "^fw:[a-f0-9]{64}$",
|
|
816
|
-
options: .regularExpression
|
|
817
|
-
) != nil
|
|
818
|
-
})
|
|
819
|
-
else {
|
|
820
|
-
throw FirmwareArtifactStoreError.invalidInput(
|
|
821
|
-
"Invalid persisted firmware lease"
|
|
822
|
-
)
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
return envelope
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
private func saveLeasesLocked(_ envelope: LeaseEnvelope) throws {
|
|
829
|
-
guard
|
|
830
|
-
envelope.schemaVersion == 1,
|
|
831
|
-
envelope.leases.count <= 32,
|
|
832
|
-
envelope.leases.values.allSatisfy({
|
|
833
|
-
$0.artifactRefs.count <= 4096
|
|
834
|
-
}),
|
|
835
|
-
envelope.leases.values.reduce(0, {
|
|
836
|
-
$0 + $1.artifactRefs.count
|
|
837
|
-
}) <= Self.maxTotalLeaseRefs
|
|
838
|
-
else {
|
|
839
|
-
throw FirmwareArtifactStoreError.invalidInput(
|
|
840
|
-
"Firmware lease metadata is too large"
|
|
841
|
-
)
|
|
842
|
-
}
|
|
843
|
-
let data = try JSONEncoder().encode(envelope)
|
|
844
|
-
guard Int64(data.count) <= Self.maxLeaseMetadataBytes else {
|
|
845
|
-
throw FirmwareArtifactStoreError.invalidInput(
|
|
846
|
-
"Firmware lease metadata is too large"
|
|
847
|
-
)
|
|
848
|
-
}
|
|
849
|
-
let url = rootURL.appendingPathComponent("leases.json", isDirectory: false)
|
|
850
|
-
try data.write(to: url, options: [.atomic])
|
|
1071
|
+
static func isSafeIdentifier(_ value: String) -> Bool {
|
|
1072
|
+
firmwareArtifactIdentifierIsSafe(value)
|
|
851
1073
|
}
|
|
852
1074
|
|
|
853
1075
|
private func markDownloadActive(_ sha256: String, delta: Int) {
|
|
@@ -863,7 +1085,7 @@ final class FirmwareArtifactStore {
|
|
|
863
1085
|
|
|
864
1086
|
private func validateArchiveRequirements(
|
|
865
1087
|
_ expectedEntries: [FirmwareArchiveExpectedEntry]
|
|
866
|
-
) throws -> [
|
|
1088
|
+
) throws -> [FirmwareArchiveRequirement] {
|
|
867
1089
|
guard !expectedEntries.isEmpty, expectedEntries.count <= 4096 else {
|
|
868
1090
|
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
869
1091
|
"Firmware archive expected entry count is invalid"
|
|
@@ -904,12 +1126,48 @@ final class FirmwareArtifactStore {
|
|
|
904
1126
|
)
|
|
905
1127
|
}
|
|
906
1128
|
}
|
|
907
|
-
return expectedEntries
|
|
1129
|
+
return expectedEntries.map {
|
|
1130
|
+
FirmwareArchiveRequirement(
|
|
1131
|
+
entryName: $0.entryName,
|
|
1132
|
+
expectedSize: Int64($0.expectedSize),
|
|
1133
|
+
expectedSha256: $0.expectedSha256.lowercased()
|
|
1134
|
+
)
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
private func resolveArchiveRequirements(
|
|
1139
|
+
_ expectedEntries: [FirmwareArchiveExpectedEntry]?,
|
|
1140
|
+
archiveEntries: [FirmwareArchiveEntryInfo]
|
|
1141
|
+
) throws -> [FirmwareArchiveRequirement] {
|
|
1142
|
+
if let expectedEntries {
|
|
1143
|
+
return try validateArchiveRequirements(expectedEntries)
|
|
1144
|
+
}
|
|
1145
|
+
if let issue = firmwareArchiveDiscoveredEntriesIssue(
|
|
1146
|
+
archiveEntries.map(\.name)
|
|
1147
|
+
) {
|
|
1148
|
+
switch issue {
|
|
1149
|
+
case .empty:
|
|
1150
|
+
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
1151
|
+
"Firmware archive must contain at least one entry"
|
|
1152
|
+
)
|
|
1153
|
+
case .duplicateName:
|
|
1154
|
+
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
1155
|
+
"Firmware archive contains duplicate entry names"
|
|
1156
|
+
)
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return archiveEntries.map {
|
|
1160
|
+
FirmwareArchiveRequirement(
|
|
1161
|
+
entryName: $0.name,
|
|
1162
|
+
expectedSize: $0.uncompressedSize,
|
|
1163
|
+
expectedSha256: nil
|
|
1164
|
+
)
|
|
1165
|
+
}
|
|
908
1166
|
}
|
|
909
1167
|
|
|
910
1168
|
private func validateArchiveEntries(
|
|
911
1169
|
_ entries: [FirmwareArchiveEntryInfo],
|
|
912
|
-
requirements: [
|
|
1170
|
+
requirements: [FirmwareArchiveRequirement]
|
|
913
1171
|
) throws {
|
|
914
1172
|
guard entries.count == requirements.count else {
|
|
915
1173
|
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
@@ -921,16 +1179,32 @@ final class FirmwareArtifactStore {
|
|
|
921
1179
|
)
|
|
922
1180
|
var names = Set<String>()
|
|
923
1181
|
var canonicalNames = Set<String>()
|
|
1182
|
+
var totalSize: Int64 = 0
|
|
924
1183
|
for entry in entries {
|
|
1184
|
+
guard let requirement = requirementsByName[entry.name] else {
|
|
1185
|
+
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
1186
|
+
"Firmware archive contains an unexpected entry"
|
|
1187
|
+
)
|
|
1188
|
+
}
|
|
1189
|
+
let (nextTotalSize, overflowed) = totalSize.addingReportingOverflow(
|
|
1190
|
+
entry.uncompressedSize
|
|
1191
|
+
)
|
|
1192
|
+
guard !overflowed else {
|
|
1193
|
+
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
1194
|
+
"Firmware archive expanded size is invalid"
|
|
1195
|
+
)
|
|
1196
|
+
}
|
|
1197
|
+
totalSize = nextTotalSize
|
|
925
1198
|
guard
|
|
926
1199
|
names.insert(entry.name).inserted,
|
|
927
1200
|
isPortableArchiveEntryName(
|
|
928
1201
|
entry.name,
|
|
929
1202
|
canonicalNames: &canonicalNames
|
|
930
1203
|
),
|
|
931
|
-
|
|
932
|
-
entry.uncompressedSize == Int64(requirement.expectedSize),
|
|
1204
|
+
entry.uncompressedSize == requirement.expectedSize,
|
|
933
1205
|
entry.uncompressedSize > 0,
|
|
1206
|
+
entry.uncompressedSize <= 128 * 1024 * 1024,
|
|
1207
|
+
totalSize <= 512 * 1024 * 1024,
|
|
934
1208
|
entry.compressedSize >= 0,
|
|
935
1209
|
entry.compressedSize <= 512 * 1024 * 1024,
|
|
936
1210
|
entry.uncompressedSize <= max(entry.compressedSize, 1) * 1000,
|
|
@@ -1001,107 +1275,68 @@ final class FirmwareArtifactStore {
|
|
|
1001
1275
|
private func streamDownload(
|
|
1002
1276
|
_ params: FirmwareArtifactDownloadParams,
|
|
1003
1277
|
partialURL: URL,
|
|
1004
|
-
resumeOffset: Int64
|
|
1278
|
+
resumeOffset: Int64,
|
|
1279
|
+
validated: ValidatedDownload
|
|
1280
|
+
) async throws {
|
|
1281
|
+
do {
|
|
1282
|
+
try await FirmwareArtifactWallClockDeadline.run(
|
|
1283
|
+
timeoutSeconds:
|
|
1284
|
+
params.overallDeadlineSeconds ?? Self.defaultDownloadDeadline
|
|
1285
|
+
) { [self] in
|
|
1286
|
+
try await streamDownloadWithinDeadline(
|
|
1287
|
+
params,
|
|
1288
|
+
partialURL: partialURL,
|
|
1289
|
+
resumeOffset: resumeOffset,
|
|
1290
|
+
validated: validated
|
|
1291
|
+
)
|
|
1292
|
+
}
|
|
1293
|
+
} catch FirmwareArtifactDeadlineError.exceeded {
|
|
1294
|
+
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1295
|
+
"ARTIFACT_DEADLINE_EXCEEDED: firmware download exceeded its deadline"
|
|
1296
|
+
)
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
private func streamDownloadWithinDeadline(
|
|
1301
|
+
_ params: FirmwareArtifactDownloadParams,
|
|
1302
|
+
partialURL: URL,
|
|
1303
|
+
resumeOffset: Int64,
|
|
1304
|
+
validated: ValidatedDownload
|
|
1005
1305
|
) async throws {
|
|
1006
1306
|
guard let url = URL(string: params.url), let hostname = url.host else {
|
|
1007
1307
|
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware URL")
|
|
1008
1308
|
}
|
|
1009
1309
|
var request = URLRequest(url: url)
|
|
1010
1310
|
request.cachePolicy = .reloadIgnoringLocalCacheData
|
|
1011
|
-
request.timeoutInterval =
|
|
1311
|
+
request.timeoutInterval =
|
|
1312
|
+
params.overallDeadlineSeconds ?? Self.defaultDownloadDeadline
|
|
1012
1313
|
request.setValue("identity", forHTTPHeaderField: "Accept-Encoding")
|
|
1013
1314
|
if resumeOffset > 0 {
|
|
1014
1315
|
request.setValue("bytes=\(resumeOffset)-", forHTTPHeaderField: "Range")
|
|
1015
1316
|
}
|
|
1016
1317
|
|
|
1017
|
-
let
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1318
|
+
let streamDelegate = FirmwareArtifactStreamDelegate(
|
|
1319
|
+
partialURL: partialURL,
|
|
1320
|
+
hostname: hostname,
|
|
1321
|
+
resumeOffset: resumeOffset,
|
|
1322
|
+
expectedSize: validated.expectedSize,
|
|
1323
|
+
maxBytes: validated.maxBytes,
|
|
1324
|
+
isCancelled: { [weak self] in
|
|
1325
|
+
Task.isCancelled ||
|
|
1326
|
+
self?.isTransactionCancelled(params.transactionId) == true
|
|
1327
|
+
}
|
|
1328
|
+
)
|
|
1329
|
+
let session = URLSession(
|
|
1028
1330
|
configuration: .ephemeral,
|
|
1029
|
-
delegate:
|
|
1331
|
+
delegate: streamDelegate,
|
|
1030
1332
|
delegateQueue: nil
|
|
1031
1333
|
)
|
|
1032
1334
|
defer {
|
|
1033
|
-
|
|
1034
|
-
pinnedSession.close()
|
|
1035
|
-
} else {
|
|
1036
|
-
session.finishTasksAndInvalidate()
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
let bytes: URLSession.AsyncBytes
|
|
1041
|
-
let response: URLResponse
|
|
1042
|
-
do {
|
|
1043
|
-
(bytes, response) = try await session.bytes(for: request)
|
|
1044
|
-
} catch {
|
|
1045
|
-
if isCancellationError(
|
|
1046
|
-
error,
|
|
1047
|
-
transactionId: params.transactionId
|
|
1048
|
-
) {
|
|
1049
|
-
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1050
|
-
"ARTIFACT_CANCELLED: firmware artifact download was cancelled"
|
|
1051
|
-
)
|
|
1052
|
-
}
|
|
1053
|
-
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1054
|
-
"ARTIFACT_NETWORK_FAILED: firmware request failed"
|
|
1055
|
-
)
|
|
1056
|
-
}
|
|
1057
|
-
guard let httpResponse = response as? HTTPURLResponse else {
|
|
1058
|
-
throw FirmwareArtifactStoreError.downloadFailed("Firmware response is not HTTP")
|
|
1059
|
-
}
|
|
1060
|
-
guard httpResponse.statusCode == 200 || httpResponse.statusCode == 206 else {
|
|
1061
|
-
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1062
|
-
"ARTIFACT_HTTP_\(httpResponse.statusCode): firmware request failed"
|
|
1063
|
-
)
|
|
1064
|
-
}
|
|
1065
|
-
let append = resumeOffset > 0 && httpResponse.statusCode == 206
|
|
1066
|
-
if httpResponse.statusCode == 206 {
|
|
1067
|
-
guard
|
|
1068
|
-
let contentRange = httpResponse.value(forHTTPHeaderField: "Content-Range"),
|
|
1069
|
-
validateContentRange(
|
|
1070
|
-
contentRange,
|
|
1071
|
-
expectedStart: append ? resumeOffset : 0,
|
|
1072
|
-
expectedTotal: Int64(params.expectedSize)
|
|
1073
|
-
)
|
|
1074
|
-
else {
|
|
1075
|
-
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1076
|
-
"ARTIFACT_PROTOCOL_INVALID: firmware resume Content-Range is invalid"
|
|
1077
|
-
)
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
let handle = try FileHandle(forWritingTo: partialURL)
|
|
1081
|
-
defer { try? handle.close() }
|
|
1082
|
-
if append {
|
|
1083
|
-
try handle.seekToEnd()
|
|
1084
|
-
} else {
|
|
1085
|
-
try handle.truncate(atOffset: 0)
|
|
1335
|
+
session.finishTasksAndInvalidate()
|
|
1086
1336
|
}
|
|
1087
1337
|
|
|
1088
|
-
var written = append ? resumeOffset : 0
|
|
1089
|
-
var buffer = Data()
|
|
1090
|
-
buffer.reserveCapacity(64 * 1024)
|
|
1091
1338
|
do {
|
|
1092
|
-
|
|
1093
|
-
buffer.append(byte)
|
|
1094
|
-
if buffer.count >= 64 * 1024 {
|
|
1095
|
-
written += Int64(buffer.count)
|
|
1096
|
-
guard written <= Int64(params.maxBytes) else {
|
|
1097
|
-
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1098
|
-
"ARTIFACT_PROTOCOL_INVALID: firmware artifact exceeds maxBytes"
|
|
1099
|
-
)
|
|
1100
|
-
}
|
|
1101
|
-
try handle.write(contentsOf: buffer)
|
|
1102
|
-
buffer.removeAll(keepingCapacity: true)
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1339
|
+
try await streamDelegate.run(session: session, request: request)
|
|
1105
1340
|
} catch let error as FirmwareArtifactStoreError {
|
|
1106
1341
|
throw error
|
|
1107
1342
|
} catch {
|
|
@@ -1113,20 +1348,15 @@ final class FirmwareArtifactStore {
|
|
|
1113
1348
|
"ARTIFACT_CANCELLED: firmware artifact download was cancelled"
|
|
1114
1349
|
)
|
|
1115
1350
|
}
|
|
1116
|
-
|
|
1117
|
-
"ARTIFACT_NETWORK_FAILED: firmware response stream failed"
|
|
1118
|
-
)
|
|
1119
|
-
}
|
|
1120
|
-
if !buffer.isEmpty {
|
|
1121
|
-
written += Int64(buffer.count)
|
|
1122
|
-
guard written <= Int64(params.maxBytes) else {
|
|
1351
|
+
if isFirmwareArtifactTLSError(error) {
|
|
1123
1352
|
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1124
|
-
"
|
|
1353
|
+
"ARTIFACT_TLS_FAILED: firmware TLS validation failed"
|
|
1125
1354
|
)
|
|
1126
1355
|
}
|
|
1127
|
-
|
|
1356
|
+
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1357
|
+
"ARTIFACT_NETWORK_FAILED: firmware request failed"
|
|
1358
|
+
)
|
|
1128
1359
|
}
|
|
1129
|
-
try handle.synchronize()
|
|
1130
1360
|
try rejectIfCancelled(transactionId: params.transactionId)
|
|
1131
1361
|
}
|
|
1132
1362
|
|
|
@@ -1157,34 +1387,6 @@ final class FirmwareArtifactStore {
|
|
|
1157
1387
|
nsError.code == NSURLErrorCancelled
|
|
1158
1388
|
}
|
|
1159
1389
|
|
|
1160
|
-
private func validateContentRange(
|
|
1161
|
-
_ value: String,
|
|
1162
|
-
expectedStart: Int64,
|
|
1163
|
-
expectedTotal: Int64
|
|
1164
|
-
) -> Bool {
|
|
1165
|
-
let pattern = #"^bytes ([0-9]+)-([0-9]+)/([0-9]+)$"#
|
|
1166
|
-
guard
|
|
1167
|
-
let expression = try? NSRegularExpression(pattern: pattern),
|
|
1168
|
-
let match = expression.firstMatch(
|
|
1169
|
-
in: value.lowercased(),
|
|
1170
|
-
range: NSRange(value.startIndex..., in: value)
|
|
1171
|
-
),
|
|
1172
|
-
match.range.location != NSNotFound,
|
|
1173
|
-
let startRange = Range(match.range(at: 1), in: value),
|
|
1174
|
-
let endRange = Range(match.range(at: 2), in: value),
|
|
1175
|
-
let totalRange = Range(match.range(at: 3), in: value),
|
|
1176
|
-
let start = Int64(value[startRange]),
|
|
1177
|
-
let end = Int64(value[endRange]),
|
|
1178
|
-
let total = Int64(value[totalRange])
|
|
1179
|
-
else {
|
|
1180
|
-
return false
|
|
1181
|
-
}
|
|
1182
|
-
return start == expectedStart &&
|
|
1183
|
-
end >= start &&
|
|
1184
|
-
end < total &&
|
|
1185
|
-
total == expectedTotal
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
1390
|
private func validateStoredArtifact(
|
|
1189
1391
|
fileURL: URL,
|
|
1190
1392
|
expectedSize: Int64,
|
|
@@ -1210,6 +1412,36 @@ final class FirmwareArtifactStore {
|
|
|
1210
1412
|
)
|
|
1211
1413
|
}
|
|
1212
1414
|
|
|
1415
|
+
private func validateDownloadedArtifact(
|
|
1416
|
+
fileURL: URL,
|
|
1417
|
+
expectedSize: Int64?,
|
|
1418
|
+
expectedSha256: String?,
|
|
1419
|
+
maxBytes: Int64
|
|
1420
|
+
) throws -> StoredFirmwareArtifact {
|
|
1421
|
+
let size = try fileSize(fileURL)
|
|
1422
|
+
guard
|
|
1423
|
+
size > 0,
|
|
1424
|
+
size <= maxBytes,
|
|
1425
|
+
expectedSize.map({ size == $0 }) ?? true
|
|
1426
|
+
else {
|
|
1427
|
+
throw FirmwareArtifactStoreError.integrityMismatch(
|
|
1428
|
+
"ARTIFACT_INTEGRITY_FAILED: firmware artifact size mismatch"
|
|
1429
|
+
)
|
|
1430
|
+
}
|
|
1431
|
+
let sha256 = try hashFile(fileURL)
|
|
1432
|
+
guard expectedSha256.map({ sha256 == $0 }) ?? true else {
|
|
1433
|
+
throw FirmwareArtifactStoreError.integrityMismatch(
|
|
1434
|
+
"ARTIFACT_INTEGRITY_FAILED: firmware artifact SHA-256 mismatch"
|
|
1435
|
+
)
|
|
1436
|
+
}
|
|
1437
|
+
return StoredFirmwareArtifact(
|
|
1438
|
+
artifactRef: "fw:\(sha256)",
|
|
1439
|
+
size: size,
|
|
1440
|
+
sha256: sha256,
|
|
1441
|
+
fileURL: fileURL
|
|
1442
|
+
)
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1213
1445
|
private func resolveArtifactURL(_ artifactRef: String) throws -> URL {
|
|
1214
1446
|
guard artifactRef.hasPrefix("fw:") else {
|
|
1215
1447
|
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware artifactRef")
|