@onekeyfe/react-native-range-downloader 3.0.81-alpha.0 → 3.0.81-alpha.10
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 -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 +185 -189
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ReactNativeRangeDownloader.kt +16 -20
- 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 +662 -408
- package/ios/RangeDownloadLogic.swift +297 -0
- package/ios/ReactNativeRangeDownloader.swift +42 -471
- package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts +3 -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++/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/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/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++/HybridReactNativeRangeDownloaderSpec.cpp +0 -1
- package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.hpp +0 -4
- package/package.json +2 -2
- package/src/ReactNativeRangeDownloader.nitro.ts +4 -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,33 @@
|
|
|
1
1
|
import CryptoKit
|
|
2
2
|
import Foundation
|
|
3
|
+
import ReactNativeNativeLogger
|
|
3
4
|
import SniConnect
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
func isFirmwareArtifactTLSError(_ error: Error) -> Bool {
|
|
7
|
+
var current: NSError? = error as NSError
|
|
8
|
+
var visited = Set<ObjectIdentifier>()
|
|
9
|
+
while let candidate = current {
|
|
10
|
+
let identifier = ObjectIdentifier(candidate)
|
|
11
|
+
guard visited.insert(identifier).inserted else {
|
|
12
|
+
break
|
|
13
|
+
}
|
|
14
|
+
if candidate.domain == NSURLErrorDomain,
|
|
15
|
+
[
|
|
16
|
+
NSURLErrorSecureConnectionFailed,
|
|
17
|
+
NSURLErrorServerCertificateHasBadDate,
|
|
18
|
+
NSURLErrorServerCertificateUntrusted,
|
|
19
|
+
NSURLErrorServerCertificateHasUnknownRoot,
|
|
20
|
+
NSURLErrorServerCertificateNotYetValid,
|
|
21
|
+
NSURLErrorClientCertificateRejected,
|
|
22
|
+
NSURLErrorClientCertificateRequired,
|
|
23
|
+
NSURLErrorAppTransportSecurityRequiresSecureConnection,
|
|
24
|
+
].contains(candidate.code)
|
|
25
|
+
{
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
current = candidate.userInfo[NSUnderlyingErrorKey] as? NSError
|
|
29
|
+
}
|
|
30
|
+
return false
|
|
11
31
|
}
|
|
12
32
|
|
|
13
33
|
struct StoredFirmwareArtifact: Sendable {
|
|
@@ -29,7 +49,106 @@ private struct StagedFirmwareArchiveEntry {
|
|
|
29
49
|
let stagingURL: URL
|
|
30
50
|
}
|
|
31
51
|
|
|
32
|
-
private
|
|
52
|
+
private struct FirmwareArchiveRequirement {
|
|
53
|
+
let entryName: String
|
|
54
|
+
let expectedSize: Int64
|
|
55
|
+
let expectedSha256: String?
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private final class FirmwareArtifactStreamDelegate: NSObject, URLSessionDataDelegate {
|
|
59
|
+
private let partialURL: URL
|
|
60
|
+
private let hostname: String
|
|
61
|
+
private let resumeOffset: Int64
|
|
62
|
+
private let expectedSize: Int64?
|
|
63
|
+
private let maxBytes: Int64
|
|
64
|
+
private let isCancelled: () -> Bool
|
|
65
|
+
private let stateLock = NSLock()
|
|
66
|
+
private var continuation: CheckedContinuation<Void, Error>?
|
|
67
|
+
private var dataTask: URLSessionDataTask?
|
|
68
|
+
private var cancellationRequested = false
|
|
69
|
+
private var completed = false
|
|
70
|
+
private var responseAccepted = false
|
|
71
|
+
private var handle: FileHandle?
|
|
72
|
+
private var written: Int64 = 0
|
|
73
|
+
|
|
74
|
+
init(
|
|
75
|
+
partialURL: URL,
|
|
76
|
+
hostname: String,
|
|
77
|
+
resumeOffset: Int64,
|
|
78
|
+
expectedSize: Int64?,
|
|
79
|
+
maxBytes: Int64,
|
|
80
|
+
isCancelled: @escaping () -> Bool
|
|
81
|
+
) {
|
|
82
|
+
self.partialURL = partialURL
|
|
83
|
+
self.hostname = hostname
|
|
84
|
+
self.resumeOffset = resumeOffset
|
|
85
|
+
self.expectedSize = expectedSize
|
|
86
|
+
self.maxBytes = maxBytes
|
|
87
|
+
self.isCancelled = isCancelled
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func run(session: URLSession, request: URLRequest) async throws {
|
|
91
|
+
try await withTaskCancellationHandler {
|
|
92
|
+
try await withCheckedThrowingContinuation {
|
|
93
|
+
(continuation: CheckedContinuation<Void, Error>) in
|
|
94
|
+
let task = session.dataTask(with: request)
|
|
95
|
+
stateLock.lock()
|
|
96
|
+
if cancellationRequested || completed {
|
|
97
|
+
stateLock.unlock()
|
|
98
|
+
task.cancel()
|
|
99
|
+
continuation.resume(throwing: CancellationError())
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
self.continuation = continuation
|
|
103
|
+
dataTask = task
|
|
104
|
+
stateLock.unlock()
|
|
105
|
+
task.resume()
|
|
106
|
+
}
|
|
107
|
+
} onCancel: {
|
|
108
|
+
self.cancel()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private func cancel() {
|
|
113
|
+
stateLock.lock()
|
|
114
|
+
cancellationRequested = true
|
|
115
|
+
let task = dataTask
|
|
116
|
+
stateLock.unlock()
|
|
117
|
+
task?.cancel()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private func finish(_ result: Result<Void, Error>) {
|
|
121
|
+
stateLock.lock()
|
|
122
|
+
guard !completed else {
|
|
123
|
+
stateLock.unlock()
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
completed = true
|
|
127
|
+
let continuation = continuation
|
|
128
|
+
self.continuation = nil
|
|
129
|
+
dataTask = nil
|
|
130
|
+
let handle = handle
|
|
131
|
+
self.handle = nil
|
|
132
|
+
stateLock.unlock()
|
|
133
|
+
|
|
134
|
+
try? handle?.close()
|
|
135
|
+
continuation?.resume(with: result)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private func reject(
|
|
139
|
+
_ dataTask: URLSessionDataTask,
|
|
140
|
+
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void,
|
|
141
|
+
message: String
|
|
142
|
+
) {
|
|
143
|
+
completionHandler(.cancel)
|
|
144
|
+
fail(dataTask, message: message)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private func fail(_ task: URLSessionTask, message: String) {
|
|
148
|
+
task.cancel()
|
|
149
|
+
finish(.failure(FirmwareArtifactStoreError.downloadFailed(message)))
|
|
150
|
+
}
|
|
151
|
+
|
|
33
152
|
func urlSession(
|
|
34
153
|
_ session: URLSession,
|
|
35
154
|
task: URLSessionTask,
|
|
@@ -38,6 +157,193 @@ private final class FirmwareArtifactRedirectDelegate: NSObject, URLSessionTaskDe
|
|
|
38
157
|
completionHandler: @escaping (URLRequest?) -> Void
|
|
39
158
|
) {
|
|
40
159
|
completionHandler(nil)
|
|
160
|
+
fail(
|
|
161
|
+
task,
|
|
162
|
+
message:
|
|
163
|
+
"ARTIFACT_REDIRECT_REJECTED: firmware response changed canonical identity"
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
func urlSession(
|
|
168
|
+
_ session: URLSession,
|
|
169
|
+
dataTask: URLSessionDataTask,
|
|
170
|
+
didReceive response: URLResponse,
|
|
171
|
+
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
|
|
172
|
+
) {
|
|
173
|
+
guard !isCancelled() else {
|
|
174
|
+
reject(
|
|
175
|
+
dataTask,
|
|
176
|
+
completionHandler: completionHandler,
|
|
177
|
+
message: "ARTIFACT_CANCELLED: firmware artifact download was cancelled"
|
|
178
|
+
)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
guard let httpResponse = response as? HTTPURLResponse else {
|
|
182
|
+
reject(
|
|
183
|
+
dataTask,
|
|
184
|
+
completionHandler: completionHandler,
|
|
185
|
+
message: "Firmware response is not HTTP"
|
|
186
|
+
)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
guard
|
|
190
|
+
let responseURL = httpResponse.url,
|
|
191
|
+
responseURL.scheme?.lowercased() == "https",
|
|
192
|
+
responseURL.host?.lowercased() == hostname.lowercased(),
|
|
193
|
+
responseURL.port == nil || responseURL.port == 443
|
|
194
|
+
else {
|
|
195
|
+
reject(
|
|
196
|
+
dataTask,
|
|
197
|
+
completionHandler: completionHandler,
|
|
198
|
+
message:
|
|
199
|
+
"ARTIFACT_REDIRECT_REJECTED: firmware response changed canonical identity"
|
|
200
|
+
)
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
guard httpResponse.statusCode == 200 || httpResponse.statusCode == 206 else {
|
|
204
|
+
reject(
|
|
205
|
+
dataTask,
|
|
206
|
+
completionHandler: completionHandler,
|
|
207
|
+
message:
|
|
208
|
+
"ARTIFACT_HTTP_\(httpResponse.statusCode): firmware request failed"
|
|
209
|
+
)
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let append = resumeOffset > 0 && httpResponse.statusCode == 206
|
|
214
|
+
if httpResponse.statusCode == 206 {
|
|
215
|
+
guard
|
|
216
|
+
let contentRange = httpResponse.value(
|
|
217
|
+
forHTTPHeaderField: "Content-Range"
|
|
218
|
+
),
|
|
219
|
+
firmwareArtifactContentRangeIsValid(
|
|
220
|
+
contentRange,
|
|
221
|
+
expectedStart: append ? resumeOffset : 0,
|
|
222
|
+
expectedTotal: expectedSize,
|
|
223
|
+
maxBytes: maxBytes
|
|
224
|
+
)
|
|
225
|
+
else {
|
|
226
|
+
reject(
|
|
227
|
+
dataTask,
|
|
228
|
+
completionHandler: completionHandler,
|
|
229
|
+
message:
|
|
230
|
+
"ARTIFACT_PROTOCOL_INVALID: firmware resume Content-Range is invalid"
|
|
231
|
+
)
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let baseOffset = append ? resumeOffset : 0
|
|
237
|
+
guard
|
|
238
|
+
firmwareArtifactResponseFits(
|
|
239
|
+
expectedContentLength: httpResponse.expectedContentLength,
|
|
240
|
+
baseOffset: baseOffset,
|
|
241
|
+
maxBytes: maxBytes
|
|
242
|
+
)
|
|
243
|
+
else {
|
|
244
|
+
reject(
|
|
245
|
+
dataTask,
|
|
246
|
+
completionHandler: completionHandler,
|
|
247
|
+
message:
|
|
248
|
+
"ARTIFACT_PROTOCOL_INVALID: firmware artifact exceeds maxBytes"
|
|
249
|
+
)
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
do {
|
|
254
|
+
let handle = try FileHandle(forWritingTo: partialURL)
|
|
255
|
+
if append {
|
|
256
|
+
try handle.seekToEnd()
|
|
257
|
+
} else {
|
|
258
|
+
try handle.truncate(atOffset: 0)
|
|
259
|
+
}
|
|
260
|
+
self.handle = handle
|
|
261
|
+
written = baseOffset
|
|
262
|
+
responseAccepted = true
|
|
263
|
+
completionHandler(.allow)
|
|
264
|
+
} catch {
|
|
265
|
+
reject(
|
|
266
|
+
dataTask,
|
|
267
|
+
completionHandler: completionHandler,
|
|
268
|
+
message:
|
|
269
|
+
"ARTIFACT_NETWORK_FAILED: firmware partial file could not be opened"
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
func urlSession(
|
|
275
|
+
_ session: URLSession,
|
|
276
|
+
dataTask: URLSessionDataTask,
|
|
277
|
+
didReceive data: Data
|
|
278
|
+
) {
|
|
279
|
+
guard responseAccepted, let handle else {
|
|
280
|
+
fail(
|
|
281
|
+
dataTask,
|
|
282
|
+
message:
|
|
283
|
+
"ARTIFACT_PROTOCOL_INVALID: firmware response stream was not accepted"
|
|
284
|
+
)
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
guard !isCancelled() else {
|
|
288
|
+
fail(
|
|
289
|
+
dataTask,
|
|
290
|
+
message: "ARTIFACT_CANCELLED: firmware artifact download was cancelled"
|
|
291
|
+
)
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
guard
|
|
295
|
+
firmwareArtifactResponseFits(
|
|
296
|
+
expectedContentLength: Int64(data.count),
|
|
297
|
+
baseOffset: written,
|
|
298
|
+
maxBytes: maxBytes
|
|
299
|
+
)
|
|
300
|
+
else {
|
|
301
|
+
fail(
|
|
302
|
+
dataTask,
|
|
303
|
+
message:
|
|
304
|
+
"ARTIFACT_PROTOCOL_INVALID: firmware artifact exceeds maxBytes"
|
|
305
|
+
)
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
do {
|
|
309
|
+
try handle.write(contentsOf: data)
|
|
310
|
+
written += Int64(data.count)
|
|
311
|
+
} catch {
|
|
312
|
+
fail(
|
|
313
|
+
dataTask,
|
|
314
|
+
message:
|
|
315
|
+
"ARTIFACT_NETWORK_FAILED: firmware response stream could not be persisted"
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
func urlSession(
|
|
321
|
+
_ session: URLSession,
|
|
322
|
+
task: URLSessionTask,
|
|
323
|
+
didCompleteWithError error: Error?
|
|
324
|
+
) {
|
|
325
|
+
if let error {
|
|
326
|
+
finish(.failure(error))
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
guard responseAccepted, let handle else {
|
|
330
|
+
finish(
|
|
331
|
+
.failure(FirmwareArtifactStoreError.downloadFailed(
|
|
332
|
+
"ARTIFACT_PROTOCOL_INVALID: firmware response stream was not accepted"
|
|
333
|
+
))
|
|
334
|
+
)
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
do {
|
|
338
|
+
try handle.synchronize()
|
|
339
|
+
finish(.success(()))
|
|
340
|
+
} catch {
|
|
341
|
+
finish(
|
|
342
|
+
.failure(FirmwareArtifactStoreError.downloadFailed(
|
|
343
|
+
"ARTIFACT_NETWORK_FAILED: firmware partial file could not be synchronized"
|
|
344
|
+
))
|
|
345
|
+
)
|
|
346
|
+
}
|
|
41
347
|
}
|
|
42
348
|
}
|
|
43
349
|
|
|
@@ -79,10 +385,8 @@ private actor FirmwareArtifactDownloadCoordinator {
|
|
|
79
385
|
final class FirmwareArtifactStore {
|
|
80
386
|
static let shared = FirmwareArtifactStore()
|
|
81
387
|
static let maxReadBytes = 256 * 1024
|
|
82
|
-
private static let
|
|
83
|
-
private static let
|
|
84
|
-
private static let finalArtifactGrace: TimeInterval = 24 * 60 * 60
|
|
85
|
-
private static let partialArtifactGrace: TimeInterval = 7 * 24 * 60 * 60
|
|
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,26 +459,59 @@ final class FirmwareArtifactStore {
|
|
|
150
459
|
else {
|
|
151
460
|
throw FirmwareArtifactStoreError.invalidInput("Firmware URL must use HTTPS port 443")
|
|
152
461
|
}
|
|
462
|
+
let expectedSize: Int64?
|
|
463
|
+
if let value = params.expectedSize {
|
|
464
|
+
guard
|
|
465
|
+
value.isFinite,
|
|
466
|
+
value > 0,
|
|
467
|
+
value <= Double(Int64.max),
|
|
468
|
+
value.rounded() == value
|
|
469
|
+
else {
|
|
470
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
471
|
+
"Invalid firmware artifact expected size"
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
expectedSize = Int64(value)
|
|
475
|
+
} else {
|
|
476
|
+
expectedSize = nil
|
|
477
|
+
}
|
|
153
478
|
guard
|
|
154
|
-
params.expectedSize.isFinite,
|
|
155
|
-
params.expectedSize > 0,
|
|
156
|
-
params.expectedSize <= Double(Int64.max),
|
|
157
|
-
params.expectedSize.rounded() == params.expectedSize,
|
|
158
479
|
params.maxBytes.isFinite,
|
|
159
|
-
params.maxBytes
|
|
480
|
+
params.maxBytes > 0,
|
|
481
|
+
params.maxBytes <= Double(Int64.max),
|
|
482
|
+
params.maxBytes.rounded() == params.maxBytes,
|
|
160
483
|
params.maxBytes <= Double(512 * 1024 * 1024)
|
|
161
484
|
else {
|
|
162
485
|
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware artifact size")
|
|
163
486
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
487
|
+
let maxBytes = Int64(params.maxBytes)
|
|
488
|
+
guard expectedSize.map({ $0 <= maxBytes }) ?? true else {
|
|
489
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
490
|
+
"Firmware artifact expected size exceeds maxBytes"
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
let expectedSha256 = params.expectedSha256?.lowercased()
|
|
494
|
+
if let expectedSha256 {
|
|
495
|
+
guard expectedSha256.range(
|
|
496
|
+
of: "^[a-f0-9]{64}$",
|
|
497
|
+
options: .regularExpression
|
|
498
|
+
) != nil else {
|
|
499
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
500
|
+
"Invalid firmware artifact SHA-256"
|
|
501
|
+
)
|
|
502
|
+
}
|
|
169
503
|
}
|
|
170
504
|
guard params.routeType == "domain" || params.routeType == "pinnedIp" else {
|
|
171
505
|
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware route type")
|
|
172
506
|
}
|
|
507
|
+
let deadline = params.overallDeadlineSeconds ?? defaultDownloadDeadline
|
|
508
|
+
guard
|
|
509
|
+
deadline.isFinite,
|
|
510
|
+
deadline > 0,
|
|
511
|
+
deadline <= maxDownloadDeadline
|
|
512
|
+
else {
|
|
513
|
+
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware download deadline")
|
|
514
|
+
}
|
|
173
515
|
if params.routeType == "pinnedIp" {
|
|
174
516
|
guard let resolvedIp = params.resolvedIp, !resolvedIp.isEmpty else {
|
|
175
517
|
throw FirmwareArtifactStoreError.invalidInput("Pinned route requires resolvedIp")
|
|
@@ -177,20 +519,40 @@ final class FirmwareArtifactStore {
|
|
|
177
519
|
} else if params.resolvedIp != nil {
|
|
178
520
|
throw FirmwareArtifactStoreError.invalidInput("Domain route must not include resolvedIp")
|
|
179
521
|
}
|
|
522
|
+
return ValidatedDownload(
|
|
523
|
+
expectedSize: expectedSize,
|
|
524
|
+
expectedSha256: expectedSha256,
|
|
525
|
+
maxBytes: maxBytes,
|
|
526
|
+
downloadToken: firmwareArtifactDownloadToken(
|
|
527
|
+
expectedSha256: expectedSha256,
|
|
528
|
+
url: params.url
|
|
529
|
+
)
|
|
530
|
+
)
|
|
180
531
|
}
|
|
181
532
|
|
|
182
533
|
func download(_ params: FirmwareArtifactDownloadParams) async throws -> StoredFirmwareArtifact {
|
|
183
|
-
try Self.validateDownloadParams(params)
|
|
534
|
+
let validated = try Self.validateDownloadParams(params)
|
|
184
535
|
try rejectIfCancelled(transactionId: params.transactionId)
|
|
185
|
-
let expectedSha256 =
|
|
186
|
-
|
|
187
|
-
|
|
536
|
+
if let expectedSha256 = validated.expectedSha256 {
|
|
537
|
+
try retainExpectedArtifact(
|
|
538
|
+
leaseRef: params.leaseRef,
|
|
539
|
+
transactionId: params.transactionId,
|
|
540
|
+
artifactRef: "fw:\(expectedSha256)"
|
|
541
|
+
)
|
|
542
|
+
} else {
|
|
543
|
+
try requireLeaseTransaction(
|
|
544
|
+
leaseRef: params.leaseRef,
|
|
545
|
+
transactionId: params.transactionId
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
let key = firmwareArtifactDownloadKey(
|
|
188
549
|
transactionId: params.transactionId,
|
|
189
|
-
|
|
550
|
+
taskId: params.taskId,
|
|
551
|
+
expectedSize: validated.expectedSize,
|
|
552
|
+
expectedSha256: validated.expectedSha256,
|
|
553
|
+
downloadToken: validated.downloadToken
|
|
190
554
|
)
|
|
191
|
-
|
|
192
|
-
"\(params.transactionId):\(expectedSha256):\(Int64(params.expectedSize))"
|
|
193
|
-
markDownloadActive(expectedSha256, delta: 1)
|
|
555
|
+
markDownloadActive(validated.downloadToken, delta: 1)
|
|
194
556
|
do {
|
|
195
557
|
let artifact = try await downloadCoordinator.run(
|
|
196
558
|
key: key,
|
|
@@ -199,13 +561,23 @@ final class FirmwareArtifactStore {
|
|
|
199
561
|
try rejectIfCancelled(transactionId: params.transactionId)
|
|
200
562
|
return try await downloadLocked(
|
|
201
563
|
params,
|
|
202
|
-
|
|
564
|
+
validated: validated
|
|
203
565
|
)
|
|
204
566
|
}
|
|
205
|
-
|
|
567
|
+
try rejectIfCancelled(transactionId: params.transactionId)
|
|
568
|
+
try retainExpectedArtifact(
|
|
569
|
+
leaseRef: params.leaseRef,
|
|
570
|
+
transactionId: params.transactionId,
|
|
571
|
+
artifactRef: artifact.artifactRef
|
|
572
|
+
)
|
|
573
|
+
markDownloadActive(validated.downloadToken, delta: -1)
|
|
206
574
|
return artifact
|
|
207
575
|
} catch {
|
|
208
|
-
markDownloadActive(
|
|
576
|
+
markDownloadActive(validated.downloadToken, delta: -1)
|
|
577
|
+
OneKeyLog.error(
|
|
578
|
+
"FirmwareArtifact",
|
|
579
|
+
"event=download_failed transactionId=\(params.transactionId) artifactId=\(params.artifactId) route=\(params.routeType) errorType=\(String(describing: type(of: error)))"
|
|
580
|
+
)
|
|
209
581
|
if error is CancellationError ||
|
|
210
582
|
isTransactionCancelled(params.transactionId) {
|
|
211
583
|
throw FirmwareArtifactStoreError.downloadFailed(
|
|
@@ -216,6 +588,32 @@ final class FirmwareArtifactStore {
|
|
|
216
588
|
}
|
|
217
589
|
}
|
|
218
590
|
|
|
591
|
+
func cachedArtifact(
|
|
592
|
+
_ params: FirmwareArtifactDownloadParams
|
|
593
|
+
) throws -> StoredFirmwareArtifact? {
|
|
594
|
+
let validated = try Self.validateDownloadParams(params)
|
|
595
|
+
try rejectIfCancelled(transactionId: params.transactionId)
|
|
596
|
+
guard let expectedSha256 = validated.expectedSha256 else {
|
|
597
|
+
return nil
|
|
598
|
+
}
|
|
599
|
+
let finalURL = artifactURL(sha256: expectedSha256)
|
|
600
|
+
guard let artifact = try? validateDownloadedArtifact(
|
|
601
|
+
fileURL: finalURL,
|
|
602
|
+
expectedSize: validated.expectedSize,
|
|
603
|
+
expectedSha256: expectedSha256,
|
|
604
|
+
maxBytes: validated.maxBytes
|
|
605
|
+
) else {
|
|
606
|
+
return nil
|
|
607
|
+
}
|
|
608
|
+
try retainExpectedArtifact(
|
|
609
|
+
leaseRef: params.leaseRef,
|
|
610
|
+
transactionId: params.transactionId,
|
|
611
|
+
artifactRef: artifact.artifactRef
|
|
612
|
+
)
|
|
613
|
+
try rejectIfCancelled(transactionId: params.transactionId)
|
|
614
|
+
return artifact
|
|
615
|
+
}
|
|
616
|
+
|
|
219
617
|
func cancelDownloads(transactionId: String) async throws {
|
|
220
618
|
guard Self.isSafeIdentifier(transactionId) else {
|
|
221
619
|
throw FirmwareArtifactStoreError.invalidInput(
|
|
@@ -226,49 +624,55 @@ final class FirmwareArtifactStore {
|
|
|
226
624
|
cancelledTransactions.insert(transactionId)
|
|
227
625
|
}
|
|
228
626
|
await downloadCoordinator.cancel(transactionId: transactionId)
|
|
229
|
-
try await RangeDownloader.shared.cancelFirmwareArtifactDownloads(
|
|
230
|
-
transactionId: transactionId
|
|
231
|
-
)
|
|
232
627
|
}
|
|
233
628
|
|
|
234
629
|
private func downloadLocked(
|
|
235
630
|
_ params: FirmwareArtifactDownloadParams,
|
|
236
|
-
|
|
631
|
+
validated: ValidatedDownload
|
|
237
632
|
) 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
|
-
)
|
|
633
|
+
if let expectedSha256 = validated.expectedSha256 {
|
|
634
|
+
let finalURL = artifactURL(sha256: expectedSha256)
|
|
635
|
+
if let existing = try? validateDownloadedArtifact(
|
|
636
|
+
fileURL: finalURL,
|
|
637
|
+
expectedSize: validated.expectedSize,
|
|
638
|
+
expectedSha256: expectedSha256,
|
|
639
|
+
maxBytes: validated.maxBytes
|
|
640
|
+
) {
|
|
641
|
+
return existing
|
|
642
|
+
}
|
|
251
643
|
}
|
|
252
644
|
|
|
645
|
+
// Firmware preflight is foreground-bound, so both routes use the same
|
|
646
|
+
// cancellable stream instead of waiting on process-owned background tasks.
|
|
253
647
|
let partialURL = rootURL.appendingPathComponent(
|
|
254
|
-
|
|
648
|
+
firmwareArtifactPartialFileName(
|
|
649
|
+
transactionId: params.transactionId,
|
|
650
|
+
taskId: params.taskId,
|
|
651
|
+
downloadToken: validated.downloadToken
|
|
652
|
+
),
|
|
255
653
|
isDirectory: false
|
|
256
654
|
)
|
|
257
655
|
if !fileManager.fileExists(atPath: partialURL.path) {
|
|
258
656
|
fileManager.createFile(atPath: partialURL.path, contents: nil)
|
|
259
657
|
}
|
|
260
658
|
var currentSize = try fileSize(partialURL)
|
|
261
|
-
if currentSize >
|
|
659
|
+
if validated.expectedSha256 == nil && currentSize > 0 {
|
|
660
|
+
try fileManager.removeItem(at: partialURL)
|
|
661
|
+
fileManager.createFile(atPath: partialURL.path, contents: nil)
|
|
662
|
+
currentSize = 0
|
|
663
|
+
} else if currentSize > validated.maxBytes {
|
|
262
664
|
try fileManager.removeItem(at: partialURL)
|
|
263
665
|
fileManager.createFile(atPath: partialURL.path, contents: nil)
|
|
264
666
|
currentSize = 0
|
|
265
667
|
}
|
|
266
|
-
if currentSize ==
|
|
267
|
-
if let completed = try?
|
|
668
|
+
if let expectedSize = validated.expectedSize, currentSize == expectedSize {
|
|
669
|
+
if let completed = try? validateDownloadedArtifact(
|
|
268
670
|
fileURL: partialURL,
|
|
269
|
-
expectedSize:
|
|
270
|
-
expectedSha256: expectedSha256
|
|
671
|
+
expectedSize: expectedSize,
|
|
672
|
+
expectedSha256: validated.expectedSha256,
|
|
673
|
+
maxBytes: validated.maxBytes
|
|
271
674
|
) {
|
|
675
|
+
let finalURL = artifactURL(sha256: completed.sha256)
|
|
272
676
|
try promote(source: partialURL, destination: finalURL)
|
|
273
677
|
return StoredFirmwareArtifact(
|
|
274
678
|
artifactRef: completed.artifactRef,
|
|
@@ -282,22 +686,33 @@ final class FirmwareArtifactStore {
|
|
|
282
686
|
currentSize = 0
|
|
283
687
|
}
|
|
284
688
|
|
|
689
|
+
OneKeyLog.info(
|
|
690
|
+
"FirmwareArtifact",
|
|
691
|
+
"event=stream_start transactionId=\(params.transactionId) artifactId=\(params.artifactId) route=\(params.routeType) expectedBytes=\(validated.expectedSize ?? -1) resumeBytes=\(currentSize)"
|
|
692
|
+
)
|
|
285
693
|
try await streamDownload(
|
|
286
694
|
params,
|
|
287
695
|
partialURL: partialURL,
|
|
288
|
-
resumeOffset: min(currentSize,
|
|
696
|
+
resumeOffset: min(currentSize, validated.maxBytes),
|
|
697
|
+
validated: validated
|
|
698
|
+
)
|
|
699
|
+
OneKeyLog.info(
|
|
700
|
+
"FirmwareArtifact",
|
|
701
|
+
"event=stream_complete transactionId=\(params.transactionId) artifactId=\(params.artifactId) route=\(params.routeType) expectedBytes=\(validated.expectedSize ?? -1)"
|
|
289
702
|
)
|
|
290
703
|
let artifact: StoredFirmwareArtifact
|
|
291
704
|
do {
|
|
292
|
-
artifact = try
|
|
705
|
+
artifact = try validateDownloadedArtifact(
|
|
293
706
|
fileURL: partialURL,
|
|
294
|
-
expectedSize:
|
|
295
|
-
expectedSha256: expectedSha256
|
|
707
|
+
expectedSize: validated.expectedSize,
|
|
708
|
+
expectedSha256: validated.expectedSha256,
|
|
709
|
+
maxBytes: validated.maxBytes
|
|
296
710
|
)
|
|
297
711
|
} catch {
|
|
298
712
|
try? fileManager.removeItem(at: partialURL)
|
|
299
713
|
throw error
|
|
300
714
|
}
|
|
715
|
+
let finalURL = artifactURL(sha256: artifact.sha256)
|
|
301
716
|
try promote(source: partialURL, destination: finalURL)
|
|
302
717
|
return StoredFirmwareArtifact(
|
|
303
718
|
artifactRef: artifact.artifactRef,
|
|
@@ -307,67 +722,11 @@ final class FirmwareArtifactStore {
|
|
|
307
722
|
)
|
|
308
723
|
}
|
|
309
724
|
|
|
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
725
|
func discard(artifactRef: String) throws {
|
|
361
726
|
let fileURL = try resolveArtifactURL(artifactRef)
|
|
362
727
|
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
|
|
728
|
+
let isRetained = leases.values.contains {
|
|
729
|
+
$0.artifactRefs.contains(artifactRef)
|
|
371
730
|
}
|
|
372
731
|
leaseLock.unlock()
|
|
373
732
|
guard !isRetained else {
|
|
@@ -447,7 +806,7 @@ final class FirmwareArtifactStore {
|
|
|
447
806
|
func materializeArchive(
|
|
448
807
|
leaseRef: String,
|
|
449
808
|
artifactRef: String,
|
|
450
|
-
expectedEntries: [FirmwareArchiveExpectedEntry]
|
|
809
|
+
expectedEntries: [FirmwareArchiveExpectedEntry]?
|
|
451
810
|
) throws -> [StoredFirmwareArchiveEntry] {
|
|
452
811
|
try requireLease(leaseRef)
|
|
453
812
|
let archiveURL = try resolveArtifactURL(artifactRef)
|
|
@@ -458,10 +817,13 @@ final class FirmwareArtifactStore {
|
|
|
458
817
|
try fileManager.createDirectory(at: scratchURL, withIntermediateDirectories: true)
|
|
459
818
|
defer { try? fileManager.removeItem(at: scratchURL) }
|
|
460
819
|
|
|
461
|
-
let requirements = try validateArchiveRequirements(expectedEntries)
|
|
462
820
|
let archiveEntries = try FirmwareArchiveMinizipBridge.scanArchive(
|
|
463
821
|
atPath: archiveURL.path
|
|
464
822
|
)
|
|
823
|
+
let requirements = try resolveArchiveRequirements(
|
|
824
|
+
expectedEntries,
|
|
825
|
+
archiveEntries: archiveEntries
|
|
826
|
+
)
|
|
465
827
|
try validateArchiveEntries(
|
|
466
828
|
archiveEntries,
|
|
467
829
|
requirements: requirements
|
|
@@ -488,7 +850,7 @@ final class FirmwareArtifactStore {
|
|
|
488
850
|
}
|
|
489
851
|
do {
|
|
490
852
|
actualSize += Int64(chunk.count)
|
|
491
|
-
guard actualSize <=
|
|
853
|
+
guard actualSize <= requirement.expectedSize else {
|
|
492
854
|
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
493
855
|
"Firmware archive entry exceeds its expected size"
|
|
494
856
|
)
|
|
@@ -514,8 +876,8 @@ final class FirmwareArtifactStore {
|
|
|
514
876
|
String(format: "%02x", $0)
|
|
515
877
|
}.joined()
|
|
516
878
|
guard
|
|
517
|
-
actualSize ==
|
|
518
|
-
|
|
879
|
+
actualSize == requirement.expectedSize,
|
|
880
|
+
requirement.expectedSha256.map({ sha256 == $0 }) ?? true
|
|
519
881
|
else {
|
|
520
882
|
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
521
883
|
"Firmware archive entry integrity mismatch"
|
|
@@ -566,18 +928,16 @@ final class FirmwareArtifactStore {
|
|
|
566
928
|
}
|
|
567
929
|
leaseLock.lock()
|
|
568
930
|
defer { leaseLock.unlock() }
|
|
569
|
-
|
|
570
|
-
guard envelope.leases.count < 32 else {
|
|
931
|
+
guard leases.count < 32 else {
|
|
571
932
|
throw FirmwareArtifactStoreError.invalidInput(
|
|
572
933
|
"Too many firmware artifact leases"
|
|
573
934
|
)
|
|
574
935
|
}
|
|
575
936
|
let leaseRef = "fwlease:\(UUID().uuidString.lowercased())"
|
|
576
|
-
|
|
937
|
+
leases[leaseRef] = LeaseState(
|
|
577
938
|
transactionId: transactionId,
|
|
578
939
|
artifactRefs: []
|
|
579
940
|
)
|
|
580
|
-
try saveLeasesLocked(envelope)
|
|
581
941
|
return leaseRef
|
|
582
942
|
}
|
|
583
943
|
|
|
@@ -603,9 +963,8 @@ final class FirmwareArtifactStore {
|
|
|
603
963
|
let transactionId: String = try {
|
|
604
964
|
leaseLock.lock()
|
|
605
965
|
defer { leaseLock.unlock() }
|
|
606
|
-
var envelope = try loadLeasesLocked()
|
|
607
966
|
guard
|
|
608
|
-
let lease =
|
|
967
|
+
let lease = leases.removeValue(
|
|
609
968
|
forKey: try validateLeaseRef(leaseRef)
|
|
610
969
|
)
|
|
611
970
|
else {
|
|
@@ -613,7 +972,6 @@ final class FirmwareArtifactStore {
|
|
|
613
972
|
"Firmware artifact lease is unavailable"
|
|
614
973
|
)
|
|
615
974
|
}
|
|
616
|
-
try saveLeasesLocked(envelope)
|
|
617
975
|
return lease.transactionId
|
|
618
976
|
}()
|
|
619
977
|
cancellationLock.withFirmwareArtifactLock {
|
|
@@ -621,43 +979,13 @@ final class FirmwareArtifactStore {
|
|
|
621
979
|
}
|
|
622
980
|
}
|
|
623
981
|
|
|
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
982
|
func sweepOrphans() throws -> (deletedFiles: Int, deletedBytes: Int64) {
|
|
649
983
|
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
|
-
}
|
|
984
|
+
let retained = Set(
|
|
985
|
+
leases.values
|
|
986
|
+
.flatMap(\.artifactRefs)
|
|
987
|
+
.map { String($0.dropFirst(3)) }
|
|
988
|
+
)
|
|
661
989
|
leaseLock.unlock()
|
|
662
990
|
activeDownloadLock.lock()
|
|
663
991
|
let active = Set(activeDownloadCounts.filter { $0.value > 0 }.keys)
|
|
@@ -666,61 +994,41 @@ final class FirmwareArtifactStore {
|
|
|
666
994
|
let openPaths = Set(readers.values.map(\.fileURL.path))
|
|
667
995
|
readerLock.unlock()
|
|
668
996
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
options: [.skipsHiddenFiles]
|
|
997
|
+
return try sweepFirmwareArtifactOrphansAtRoot(
|
|
998
|
+
rootURL,
|
|
999
|
+
retainedSha256: retained,
|
|
1000
|
+
activeSha256: active,
|
|
1001
|
+
openPaths: openPaths,
|
|
1002
|
+
fileManager: fileManager
|
|
676
1003
|
)
|
|
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
1004
|
}
|
|
713
1005
|
|
|
714
1006
|
private func requireLease(_ leaseRef: String) throws {
|
|
715
1007
|
leaseLock.lock()
|
|
716
1008
|
defer { leaseLock.unlock() }
|
|
717
|
-
guard try
|
|
1009
|
+
guard leases[try validateLeaseRef(leaseRef)] != nil else {
|
|
718
1010
|
throw FirmwareArtifactStoreError.invalidInput(
|
|
719
1011
|
"Firmware artifact lease is unavailable"
|
|
720
1012
|
)
|
|
721
1013
|
}
|
|
722
1014
|
}
|
|
723
1015
|
|
|
1016
|
+
private func requireLeaseTransaction(
|
|
1017
|
+
leaseRef: String,
|
|
1018
|
+
transactionId: String
|
|
1019
|
+
) throws {
|
|
1020
|
+
leaseLock.lock()
|
|
1021
|
+
defer { leaseLock.unlock() }
|
|
1022
|
+
guard
|
|
1023
|
+
let lease = leases[try validateLeaseRef(leaseRef)],
|
|
1024
|
+
lease.transactionId == transactionId
|
|
1025
|
+
else {
|
|
1026
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
1027
|
+
"Firmware artifact lease transaction mismatch"
|
|
1028
|
+
)
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
724
1032
|
private func retainExpectedArtifact(
|
|
725
1033
|
leaseRef: String,
|
|
726
1034
|
transactionId: String?,
|
|
@@ -736,9 +1044,8 @@ final class FirmwareArtifactStore {
|
|
|
736
1044
|
}
|
|
737
1045
|
leaseLock.lock()
|
|
738
1046
|
defer { leaseLock.unlock() }
|
|
739
|
-
var envelope = try loadLeasesLocked()
|
|
740
1047
|
let validatedLeaseRef = try validateLeaseRef(leaseRef)
|
|
741
|
-
guard var lease =
|
|
1048
|
+
guard var lease = leases[validatedLeaseRef] else {
|
|
742
1049
|
throw FirmwareArtifactStoreError.invalidInput(
|
|
743
1050
|
"Firmware artifact lease is unavailable"
|
|
744
1051
|
)
|
|
@@ -749,8 +1056,7 @@ final class FirmwareArtifactStore {
|
|
|
749
1056
|
)
|
|
750
1057
|
}
|
|
751
1058
|
if lease.artifactRefs.insert(artifactRef).inserted {
|
|
752
|
-
|
|
753
|
-
try saveLeasesLocked(envelope)
|
|
1059
|
+
leases[validatedLeaseRef] = lease
|
|
754
1060
|
}
|
|
755
1061
|
}
|
|
756
1062
|
|
|
@@ -770,84 +1076,8 @@ final class FirmwareArtifactStore {
|
|
|
770
1076
|
) != nil
|
|
771
1077
|
}
|
|
772
1078
|
|
|
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])
|
|
1079
|
+
static func isSafeIdentifier(_ value: String) -> Bool {
|
|
1080
|
+
firmwareArtifactIdentifierIsSafe(value)
|
|
851
1081
|
}
|
|
852
1082
|
|
|
853
1083
|
private func markDownloadActive(_ sha256: String, delta: Int) {
|
|
@@ -863,7 +1093,7 @@ final class FirmwareArtifactStore {
|
|
|
863
1093
|
|
|
864
1094
|
private func validateArchiveRequirements(
|
|
865
1095
|
_ expectedEntries: [FirmwareArchiveExpectedEntry]
|
|
866
|
-
) throws -> [
|
|
1096
|
+
) throws -> [FirmwareArchiveRequirement] {
|
|
867
1097
|
guard !expectedEntries.isEmpty, expectedEntries.count <= 4096 else {
|
|
868
1098
|
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
869
1099
|
"Firmware archive expected entry count is invalid"
|
|
@@ -904,12 +1134,48 @@ final class FirmwareArtifactStore {
|
|
|
904
1134
|
)
|
|
905
1135
|
}
|
|
906
1136
|
}
|
|
907
|
-
return expectedEntries
|
|
1137
|
+
return expectedEntries.map {
|
|
1138
|
+
FirmwareArchiveRequirement(
|
|
1139
|
+
entryName: $0.entryName,
|
|
1140
|
+
expectedSize: Int64($0.expectedSize),
|
|
1141
|
+
expectedSha256: $0.expectedSha256.lowercased()
|
|
1142
|
+
)
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
private func resolveArchiveRequirements(
|
|
1147
|
+
_ expectedEntries: [FirmwareArchiveExpectedEntry]?,
|
|
1148
|
+
archiveEntries: [FirmwareArchiveEntryInfo]
|
|
1149
|
+
) throws -> [FirmwareArchiveRequirement] {
|
|
1150
|
+
if let expectedEntries {
|
|
1151
|
+
return try validateArchiveRequirements(expectedEntries)
|
|
1152
|
+
}
|
|
1153
|
+
if let issue = firmwareArchiveDiscoveredEntriesIssue(
|
|
1154
|
+
archiveEntries.map(\.name)
|
|
1155
|
+
) {
|
|
1156
|
+
switch issue {
|
|
1157
|
+
case .empty:
|
|
1158
|
+
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
1159
|
+
"Firmware archive must contain at least one entry"
|
|
1160
|
+
)
|
|
1161
|
+
case .duplicateName:
|
|
1162
|
+
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
1163
|
+
"Firmware archive contains duplicate entry names"
|
|
1164
|
+
)
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return archiveEntries.map {
|
|
1168
|
+
FirmwareArchiveRequirement(
|
|
1169
|
+
entryName: $0.name,
|
|
1170
|
+
expectedSize: $0.uncompressedSize,
|
|
1171
|
+
expectedSha256: nil
|
|
1172
|
+
)
|
|
1173
|
+
}
|
|
908
1174
|
}
|
|
909
1175
|
|
|
910
1176
|
private func validateArchiveEntries(
|
|
911
1177
|
_ entries: [FirmwareArchiveEntryInfo],
|
|
912
|
-
requirements: [
|
|
1178
|
+
requirements: [FirmwareArchiveRequirement]
|
|
913
1179
|
) throws {
|
|
914
1180
|
guard entries.count == requirements.count else {
|
|
915
1181
|
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
@@ -921,16 +1187,32 @@ final class FirmwareArtifactStore {
|
|
|
921
1187
|
)
|
|
922
1188
|
var names = Set<String>()
|
|
923
1189
|
var canonicalNames = Set<String>()
|
|
1190
|
+
var totalSize: Int64 = 0
|
|
924
1191
|
for entry in entries {
|
|
1192
|
+
guard let requirement = requirementsByName[entry.name] else {
|
|
1193
|
+
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
1194
|
+
"Firmware archive contains an unexpected entry"
|
|
1195
|
+
)
|
|
1196
|
+
}
|
|
1197
|
+
let (nextTotalSize, overflowed) = totalSize.addingReportingOverflow(
|
|
1198
|
+
entry.uncompressedSize
|
|
1199
|
+
)
|
|
1200
|
+
guard !overflowed else {
|
|
1201
|
+
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
1202
|
+
"Firmware archive expanded size is invalid"
|
|
1203
|
+
)
|
|
1204
|
+
}
|
|
1205
|
+
totalSize = nextTotalSize
|
|
925
1206
|
guard
|
|
926
1207
|
names.insert(entry.name).inserted,
|
|
927
1208
|
isPortableArchiveEntryName(
|
|
928
1209
|
entry.name,
|
|
929
1210
|
canonicalNames: &canonicalNames
|
|
930
1211
|
),
|
|
931
|
-
|
|
932
|
-
entry.uncompressedSize == Int64(requirement.expectedSize),
|
|
1212
|
+
entry.uncompressedSize == requirement.expectedSize,
|
|
933
1213
|
entry.uncompressedSize > 0,
|
|
1214
|
+
entry.uncompressedSize <= 128 * 1024 * 1024,
|
|
1215
|
+
totalSize <= 512 * 1024 * 1024,
|
|
934
1216
|
entry.compressedSize >= 0,
|
|
935
1217
|
entry.compressedSize <= 512 * 1024 * 1024,
|
|
936
1218
|
entry.uncompressedSize <= max(entry.compressedSize, 1) * 1000,
|
|
@@ -1001,32 +1283,70 @@ final class FirmwareArtifactStore {
|
|
|
1001
1283
|
private func streamDownload(
|
|
1002
1284
|
_ params: FirmwareArtifactDownloadParams,
|
|
1003
1285
|
partialURL: URL,
|
|
1004
|
-
resumeOffset: Int64
|
|
1286
|
+
resumeOffset: Int64,
|
|
1287
|
+
validated: ValidatedDownload
|
|
1288
|
+
) async throws {
|
|
1289
|
+
do {
|
|
1290
|
+
try await FirmwareArtifactWallClockDeadline.run(
|
|
1291
|
+
timeoutSeconds:
|
|
1292
|
+
params.overallDeadlineSeconds ?? Self.defaultDownloadDeadline
|
|
1293
|
+
) { [self] in
|
|
1294
|
+
try await streamDownloadWithinDeadline(
|
|
1295
|
+
params,
|
|
1296
|
+
partialURL: partialURL,
|
|
1297
|
+
resumeOffset: resumeOffset,
|
|
1298
|
+
validated: validated
|
|
1299
|
+
)
|
|
1300
|
+
}
|
|
1301
|
+
} catch FirmwareArtifactDeadlineError.exceeded {
|
|
1302
|
+
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1303
|
+
"ARTIFACT_DEADLINE_EXCEEDED: firmware download exceeded its deadline"
|
|
1304
|
+
)
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
private func streamDownloadWithinDeadline(
|
|
1309
|
+
_ params: FirmwareArtifactDownloadParams,
|
|
1310
|
+
partialURL: URL,
|
|
1311
|
+
resumeOffset: Int64,
|
|
1312
|
+
validated: ValidatedDownload
|
|
1005
1313
|
) async throws {
|
|
1006
1314
|
guard let url = URL(string: params.url), let hostname = url.host else {
|
|
1007
1315
|
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware URL")
|
|
1008
1316
|
}
|
|
1009
1317
|
var request = URLRequest(url: url)
|
|
1010
1318
|
request.cachePolicy = .reloadIgnoringLocalCacheData
|
|
1011
|
-
request.timeoutInterval =
|
|
1319
|
+
request.timeoutInterval =
|
|
1320
|
+
params.overallDeadlineSeconds ?? Self.defaultDownloadDeadline
|
|
1012
1321
|
request.setValue("identity", forHTTPHeaderField: "Accept-Encoding")
|
|
1013
1322
|
if resumeOffset > 0 {
|
|
1014
1323
|
request.setValue("bytes=\(resumeOffset)-", forHTTPHeaderField: "Range")
|
|
1015
1324
|
}
|
|
1016
1325
|
|
|
1326
|
+
let streamDelegate = FirmwareArtifactStreamDelegate(
|
|
1327
|
+
partialURL: partialURL,
|
|
1328
|
+
hostname: hostname,
|
|
1329
|
+
resumeOffset: resumeOffset,
|
|
1330
|
+
expectedSize: validated.expectedSize,
|
|
1331
|
+
maxBytes: validated.maxBytes,
|
|
1332
|
+
isCancelled: { [weak self] in
|
|
1333
|
+
Task.isCancelled ||
|
|
1334
|
+
self?.isTransactionCancelled(params.transactionId) == true
|
|
1335
|
+
}
|
|
1336
|
+
)
|
|
1017
1337
|
let pinnedSession: SniConnectPinnedSession?
|
|
1018
1338
|
if params.routeType == "pinnedIp" {
|
|
1019
1339
|
pinnedSession = try SniConnectPinnedTransport.makeSession(
|
|
1020
1340
|
hostname: hostname,
|
|
1021
|
-
ip: params.resolvedIp
|
|
1341
|
+
ip: params.resolvedIp!,
|
|
1342
|
+
dataDelegate: streamDelegate
|
|
1022
1343
|
)
|
|
1023
1344
|
} else {
|
|
1024
1345
|
pinnedSession = nil
|
|
1025
1346
|
}
|
|
1026
|
-
let domainDelegate = FirmwareArtifactRedirectDelegate()
|
|
1027
1347
|
let session = pinnedSession?.session ?? URLSession(
|
|
1028
1348
|
configuration: .ephemeral,
|
|
1029
|
-
delegate:
|
|
1349
|
+
delegate: streamDelegate,
|
|
1030
1350
|
delegateQueue: nil
|
|
1031
1351
|
)
|
|
1032
1352
|
defer {
|
|
@@ -1037,71 +1357,8 @@ final class FirmwareArtifactStore {
|
|
|
1037
1357
|
}
|
|
1038
1358
|
}
|
|
1039
1359
|
|
|
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)
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
var written = append ? resumeOffset : 0
|
|
1089
|
-
var buffer = Data()
|
|
1090
|
-
buffer.reserveCapacity(64 * 1024)
|
|
1091
1360
|
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
|
-
}
|
|
1361
|
+
try await streamDelegate.run(session: session, request: request)
|
|
1105
1362
|
} catch let error as FirmwareArtifactStoreError {
|
|
1106
1363
|
throw error
|
|
1107
1364
|
} catch {
|
|
@@ -1113,20 +1370,15 @@ final class FirmwareArtifactStore {
|
|
|
1113
1370
|
"ARTIFACT_CANCELLED: firmware artifact download was cancelled"
|
|
1114
1371
|
)
|
|
1115
1372
|
}
|
|
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 {
|
|
1373
|
+
if isFirmwareArtifactTLSError(error) {
|
|
1123
1374
|
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1124
|
-
"
|
|
1375
|
+
"ARTIFACT_TLS_FAILED: firmware TLS validation failed"
|
|
1125
1376
|
)
|
|
1126
1377
|
}
|
|
1127
|
-
|
|
1378
|
+
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1379
|
+
"ARTIFACT_NETWORK_FAILED: firmware request failed"
|
|
1380
|
+
)
|
|
1128
1381
|
}
|
|
1129
|
-
try handle.synchronize()
|
|
1130
1382
|
try rejectIfCancelled(transactionId: params.transactionId)
|
|
1131
1383
|
}
|
|
1132
1384
|
|
|
@@ -1157,34 +1409,6 @@ final class FirmwareArtifactStore {
|
|
|
1157
1409
|
nsError.code == NSURLErrorCancelled
|
|
1158
1410
|
}
|
|
1159
1411
|
|
|
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
1412
|
private func validateStoredArtifact(
|
|
1189
1413
|
fileURL: URL,
|
|
1190
1414
|
expectedSize: Int64,
|
|
@@ -1210,6 +1434,36 @@ final class FirmwareArtifactStore {
|
|
|
1210
1434
|
)
|
|
1211
1435
|
}
|
|
1212
1436
|
|
|
1437
|
+
private func validateDownloadedArtifact(
|
|
1438
|
+
fileURL: URL,
|
|
1439
|
+
expectedSize: Int64?,
|
|
1440
|
+
expectedSha256: String?,
|
|
1441
|
+
maxBytes: Int64
|
|
1442
|
+
) throws -> StoredFirmwareArtifact {
|
|
1443
|
+
let size = try fileSize(fileURL)
|
|
1444
|
+
guard
|
|
1445
|
+
size > 0,
|
|
1446
|
+
size <= maxBytes,
|
|
1447
|
+
expectedSize.map({ size == $0 }) ?? true
|
|
1448
|
+
else {
|
|
1449
|
+
throw FirmwareArtifactStoreError.integrityMismatch(
|
|
1450
|
+
"ARTIFACT_INTEGRITY_FAILED: firmware artifact size mismatch"
|
|
1451
|
+
)
|
|
1452
|
+
}
|
|
1453
|
+
let sha256 = try hashFile(fileURL)
|
|
1454
|
+
guard expectedSha256.map({ sha256 == $0 }) ?? true else {
|
|
1455
|
+
throw FirmwareArtifactStoreError.integrityMismatch(
|
|
1456
|
+
"ARTIFACT_INTEGRITY_FAILED: firmware artifact SHA-256 mismatch"
|
|
1457
|
+
)
|
|
1458
|
+
}
|
|
1459
|
+
return StoredFirmwareArtifact(
|
|
1460
|
+
artifactRef: "fw:\(sha256)",
|
|
1461
|
+
size: size,
|
|
1462
|
+
sha256: sha256,
|
|
1463
|
+
fileURL: fileURL
|
|
1464
|
+
)
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1213
1467
|
private func resolveArtifactURL(_ artifactRef: String) throws -> URL {
|
|
1214
1468
|
guard artifactRef.hasPrefix("fw:") else {
|
|
1215
1469
|
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware artifactRef")
|