@onekeyfe/react-native-range-downloader 3.0.81 → 3.0.82
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 +7 -0
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRules.kt +291 -0
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactOrphanSweep.kt +111 -0
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactStore.kt +822 -0
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ReactNativeRangeDownloader.kt +148 -0
- package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRulesTest.kt +114 -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/FirmwareArchiveMinizipBridge.h +33 -0
- package/ios/FirmwareArchiveMinizipBridge.mm +213 -0
- package/ios/FirmwareArtifactStore.swift +1508 -0
- package/ios/RangeDownloadLogic.swift +317 -0
- package/ios/ReactNativeRangeDownloader.swift +214 -6
- package/lib/module/index.js +2 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts +92 -1
- package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JDownloadChannel.hpp +8 -6
- package/nitrogen/generated/android/c++/JFirmwareArchiveExpectedEntry.hpp +69 -0
- package/nitrogen/generated/android/c++/JFirmwareArchiveMaterializeParams.hpp +87 -0
- package/nitrogen/generated/android/c++/JFirmwareArchiveMaterializeResult.hpp +80 -0
- package/nitrogen/generated/android/c++/JFirmwareArchiveMaterializedArtifact.hpp +63 -0
- package/nitrogen/generated/android/c++/JFirmwareArtifactCancelParams.hpp +57 -0
- package/nitrogen/generated/android/c++/JFirmwareArtifactCapabilities.hpp +88 -0
- package/nitrogen/generated/android/c++/JFirmwareArtifactDownloadParams.hpp +98 -0
- package/nitrogen/generated/android/c++/JFirmwareArtifactLease.hpp +57 -0
- package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseCreateParams.hpp +57 -0
- package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseReleaseParams.hpp +61 -0
- package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseRetainParams.hpp +61 -0
- package/nitrogen/generated/android/c++/JFirmwareArtifactReaderCloseParams.hpp +57 -0
- package/nitrogen/generated/android/c++/JFirmwareArtifactReaderInfo.hpp +61 -0
- package/nitrogen/generated/android/c++/JFirmwareArtifactReaderReadParams.hpp +65 -0
- package/nitrogen/generated/android/c++/JFirmwareArtifactReceipt.hpp +69 -0
- package/nitrogen/generated/android/c++/JFirmwareArtifactRefParams.hpp +57 -0
- package/nitrogen/generated/android/c++/JFirmwareArtifactSweepResult.hpp +61 -0
- package/nitrogen/generated/android/c++/JFunc_void_RangeDownloadEvent.hpp +3 -3
- package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.cpp +274 -25
- package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.hpp +32 -22
- package/nitrogen/generated/android/c++/JRangeDownloadEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JRangeDownloadOutcome.hpp +6 -7
- package/nitrogen/generated/android/c++/JRangeDownloadParams.hpp +2 -2
- package/nitrogen/generated/android/c++/JRangeDownloadResult.hpp +2 -2
- package/nitrogen/generated/android/c++/JRangeFallbackKind.hpp +12 -13
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/DownloadChannel.kt +5 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveExpectedEntry.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveMaterializeParams.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveMaterializeResult.kt +38 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveMaterializedArtifact.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactCancelParams.kt +38 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactCapabilities.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactDownloadParams.kt +68 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLease.kt +38 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseCreateParams.kt +38 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseReleaseParams.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseRetainParams.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactReaderCloseParams.kt +38 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactReaderInfo.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactReaderReadParams.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactReceipt.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactRefParams.kt +38 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactSweepResult.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/Func_void_RangeDownloadEvent.kt +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/HybridReactNativeRangeDownloaderSpec.kt +65 -19
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/RangeDownloadEvent.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/RangeDownloadOutcome.kt +3 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/RangeDownloadParams.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/RangeDownloadResult.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/RangeFallbackKind.kt +3 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/reactnativerangedownloaderOnLoad.kt +1 -1
- package/nitrogen/generated/android/reactnativerangedownloader+autolinking.cmake +1 -1
- package/nitrogen/generated/android/reactnativerangedownloader+autolinking.gradle +1 -1
- package/nitrogen/generated/android/reactnativerangedownloaderOnLoad.cpp +28 -18
- package/nitrogen/generated/android/reactnativerangedownloaderOnLoad.hpp +14 -5
- package/nitrogen/generated/ios/ReactNativeRangeDownloader+autolinking.rb +2 -2
- package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Bridge.cpp +49 -1
- package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Bridge.hpp +348 -4
- package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Umbrella.hpp +54 -1
- package/nitrogen/generated/ios/ReactNativeRangeDownloaderAutolinking.mm +1 -1
- package/nitrogen/generated/ios/ReactNativeRangeDownloaderAutolinking.swift +9 -8
- package/nitrogen/generated/ios/c++/HybridReactNativeRangeDownloaderSpecSwift.cpp +1 -1
- package/nitrogen/generated/ios/c++/HybridReactNativeRangeDownloaderSpecSwift.hpp +159 -1
- package/nitrogen/generated/ios/swift/DownloadChannel.swift +5 -1
- package/nitrogen/generated/ios/swift/FirmwareArchiveExpectedEntry.swift +44 -0
- package/nitrogen/generated/ios/swift/FirmwareArchiveMaterializeParams.swift +58 -0
- package/nitrogen/generated/ios/swift/FirmwareArchiveMaterializeResult.swift +35 -0
- package/nitrogen/generated/ios/swift/FirmwareArchiveMaterializedArtifact.swift +34 -0
- package/nitrogen/generated/ios/swift/FirmwareArtifactCancelParams.swift +29 -0
- package/nitrogen/generated/ios/swift/FirmwareArtifactCapabilities.swift +50 -0
- package/nitrogen/generated/ios/swift/FirmwareArtifactDownloadParams.swift +131 -0
- package/nitrogen/generated/ios/swift/FirmwareArtifactLease.swift +29 -0
- package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseCreateParams.swift +29 -0
- package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseReleaseParams.swift +34 -0
- package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseRetainParams.swift +34 -0
- package/nitrogen/generated/ios/swift/FirmwareArtifactReaderCloseParams.swift +29 -0
- package/nitrogen/generated/ios/swift/FirmwareArtifactReaderInfo.swift +34 -0
- package/nitrogen/generated/ios/swift/FirmwareArtifactReaderReadParams.swift +39 -0
- package/nitrogen/generated/ios/swift/FirmwareArtifactReceipt.swift +44 -0
- package/nitrogen/generated/ios/swift/FirmwareArtifactRefParams.swift +29 -0
- package/nitrogen/generated/ios/swift/FirmwareArtifactSweepResult.swift +34 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +1 -2
- package/nitrogen/generated/ios/swift/Func_void_FirmwareArchiveMaterializeResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_FirmwareArtifactLease.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_FirmwareArtifactReaderInfo.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_FirmwareArtifactReceipt.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_FirmwareArtifactSweepResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_RangeDownloadEvent.swift +1 -2
- package/nitrogen/generated/ios/swift/Func_void_RangeDownloadResult.swift +1 -2
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +1 -2
- package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec.swift +15 -4
- package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec_cxx.swift +230 -2
- package/nitrogen/generated/ios/swift/RangeDownloadEvent.swift +11 -42
- package/nitrogen/generated/ios/swift/RangeDownloadOutcome.swift +1 -1
- package/nitrogen/generated/ios/swift/RangeDownloadParams.swift +72 -139
- package/nitrogen/generated/ios/swift/RangeDownloadResult.swift +16 -53
- package/nitrogen/generated/ios/swift/RangeFallbackKind.swift +1 -1
- package/nitrogen/generated/shared/c++/DownloadChannel.hpp +5 -1
- package/nitrogen/generated/shared/c++/FirmwareArchiveExpectedEntry.hpp +95 -0
- package/nitrogen/generated/shared/c++/FirmwareArchiveMaterializeParams.hpp +95 -0
- package/nitrogen/generated/shared/c++/FirmwareArchiveMaterializeResult.hpp +85 -0
- package/nitrogen/generated/shared/c++/FirmwareArchiveMaterializedArtifact.hpp +89 -0
- package/nitrogen/generated/shared/c++/FirmwareArtifactCancelParams.hpp +83 -0
- package/nitrogen/generated/shared/c++/FirmwareArtifactCapabilities.hpp +96 -0
- package/nitrogen/generated/shared/c++/FirmwareArtifactDownloadParams.hpp +124 -0
- package/nitrogen/generated/shared/c++/FirmwareArtifactLease.hpp +83 -0
- package/nitrogen/generated/shared/c++/FirmwareArtifactLeaseCreateParams.hpp +83 -0
- package/nitrogen/generated/shared/c++/FirmwareArtifactLeaseReleaseParams.hpp +87 -0
- package/nitrogen/generated/shared/c++/FirmwareArtifactLeaseRetainParams.hpp +87 -0
- package/nitrogen/generated/shared/c++/FirmwareArtifactReaderCloseParams.hpp +83 -0
- package/nitrogen/generated/shared/c++/FirmwareArtifactReaderInfo.hpp +87 -0
- package/nitrogen/generated/shared/c++/FirmwareArtifactReaderReadParams.hpp +91 -0
- package/nitrogen/generated/shared/c++/FirmwareArtifactReceipt.hpp +95 -0
- package/nitrogen/generated/shared/c++/FirmwareArtifactRefParams.hpp +83 -0
- package/nitrogen/generated/shared/c++/FirmwareArtifactSweepResult.hpp +87 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.cpp +13 -1
- package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.hpp +59 -1
- package/nitrogen/generated/shared/c++/RangeDownloadEvent.hpp +25 -17
- package/nitrogen/generated/shared/c++/RangeDownloadOutcome.hpp +1 -1
- package/nitrogen/generated/shared/c++/RangeDownloadParams.hpp +43 -35
- package/nitrogen/generated/shared/c++/RangeDownloadResult.hpp +22 -14
- package/nitrogen/generated/shared/c++/RangeFallbackKind.hpp +1 -1
- package/package.json +4 -4
- package/src/ReactNativeRangeDownloader.nitro.ts +130 -1
- package/src/index.tsx +3 -1
|
@@ -0,0 +1,1508 @@
|
|
|
1
|
+
import CryptoKit
|
|
2
|
+
import Foundation
|
|
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
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
struct StoredFirmwareArtifact: Sendable {
|
|
33
|
+
let artifactRef: String
|
|
34
|
+
let size: Int64
|
|
35
|
+
let sha256: String
|
|
36
|
+
let fileURL: URL
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
struct StoredFirmwareArchiveEntry {
|
|
40
|
+
let entryName: String
|
|
41
|
+
let artifact: StoredFirmwareArtifact
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private struct StagedFirmwareArchiveEntry {
|
|
45
|
+
let entryName: String
|
|
46
|
+
let size: Int64
|
|
47
|
+
let sha256: String
|
|
48
|
+
let stagingURL: URL
|
|
49
|
+
}
|
|
50
|
+
|
|
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
|
+
|
|
151
|
+
func urlSession(
|
|
152
|
+
_ session: URLSession,
|
|
153
|
+
task: URLSessionTask,
|
|
154
|
+
willPerformHTTPRedirection response: HTTPURLResponse,
|
|
155
|
+
newRequest request: URLRequest,
|
|
156
|
+
completionHandler: @escaping (URLRequest?) -> Void
|
|
157
|
+
) {
|
|
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
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private actor FirmwareArtifactDownloadCoordinator {
|
|
350
|
+
private struct ActiveDownload {
|
|
351
|
+
let transactionId: String
|
|
352
|
+
let task: Task<StoredFirmwareArtifact, Error>
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private var downloads: [String: ActiveDownload] = [:]
|
|
356
|
+
|
|
357
|
+
func run(
|
|
358
|
+
key: String,
|
|
359
|
+
transactionId: String,
|
|
360
|
+
operation: @escaping () async throws -> StoredFirmwareArtifact
|
|
361
|
+
) async throws -> StoredFirmwareArtifact {
|
|
362
|
+
if let download = downloads[key] {
|
|
363
|
+
return try await download.task.value
|
|
364
|
+
}
|
|
365
|
+
let task = Task {
|
|
366
|
+
try await operation()
|
|
367
|
+
}
|
|
368
|
+
downloads[key] = ActiveDownload(
|
|
369
|
+
transactionId: transactionId,
|
|
370
|
+
task: task
|
|
371
|
+
)
|
|
372
|
+
defer { downloads[key] = nil }
|
|
373
|
+
return try await task.value
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
func cancel(transactionId: String) {
|
|
377
|
+
for download in downloads.values
|
|
378
|
+
where download.transactionId == transactionId {
|
|
379
|
+
download.task.cancel()
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
final class FirmwareArtifactStore {
|
|
385
|
+
static let shared = FirmwareArtifactStore()
|
|
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
|
|
390
|
+
|
|
391
|
+
private struct OpenReader {
|
|
392
|
+
let handle: FileHandle
|
|
393
|
+
let fileURL: URL
|
|
394
|
+
let size: Int64
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private struct LeaseState {
|
|
398
|
+
let transactionId: String
|
|
399
|
+
var artifactRefs: Set<String>
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private let fileManager = FileManager.default
|
|
403
|
+
private let downloadCoordinator = FirmwareArtifactDownloadCoordinator()
|
|
404
|
+
private let leaseLock = NSLock()
|
|
405
|
+
private let activeDownloadLock = NSLock()
|
|
406
|
+
private var activeDownloadCounts: [String: Int] = [:]
|
|
407
|
+
private let cancellationLock = NSLock()
|
|
408
|
+
private var cancelledTransactions: Set<String> = []
|
|
409
|
+
private let readerLock = NSLock()
|
|
410
|
+
private var readers: [String: OpenReader] = [:]
|
|
411
|
+
private var leases: [String: LeaseState] = [:]
|
|
412
|
+
|
|
413
|
+
private lazy var rootURL: URL = {
|
|
414
|
+
let base = fileManager.urls(
|
|
415
|
+
for: .applicationSupportDirectory,
|
|
416
|
+
in: .userDomainMask
|
|
417
|
+
).first!
|
|
418
|
+
let root = base.appendingPathComponent("onekey-firmware-artifacts", isDirectory: true)
|
|
419
|
+
try? fileManager.createDirectory(at: root, withIntermediateDirectories: true)
|
|
420
|
+
return root
|
|
421
|
+
}()
|
|
422
|
+
|
|
423
|
+
private init() {}
|
|
424
|
+
|
|
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 {
|
|
435
|
+
guard
|
|
436
|
+
!params.taskId.isEmpty,
|
|
437
|
+
params.taskId.count <= 100,
|
|
438
|
+
params.taskId.allSatisfy({ $0.isLetter || $0.isNumber || "._-".contains($0) })
|
|
439
|
+
else {
|
|
440
|
+
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware taskId")
|
|
441
|
+
}
|
|
442
|
+
guard
|
|
443
|
+
isSafeIdentifier(params.transactionId),
|
|
444
|
+
isSafeIdentifier(params.artifactId),
|
|
445
|
+
isValidLeaseRef(params.leaseRef)
|
|
446
|
+
else {
|
|
447
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
448
|
+
"Invalid firmware transaction or lease identity"
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
guard
|
|
452
|
+
let url = URL(string: params.url),
|
|
453
|
+
url.scheme?.lowercased() == "https",
|
|
454
|
+
url.host?.isEmpty == false,
|
|
455
|
+
url.user == nil,
|
|
456
|
+
url.password == nil,
|
|
457
|
+
url.fragment == nil,
|
|
458
|
+
url.port == nil || url.port == 443
|
|
459
|
+
else {
|
|
460
|
+
throw FirmwareArtifactStoreError.invalidInput("Firmware URL must use HTTPS port 443")
|
|
461
|
+
}
|
|
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 {
|
|
482
|
+
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware artifact size")
|
|
483
|
+
}
|
|
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
|
+
}
|
|
499
|
+
}
|
|
500
|
+
guard params.routeType == "domain" else {
|
|
501
|
+
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware route type")
|
|
502
|
+
}
|
|
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 {
|
|
512
|
+
throw FirmwareArtifactStoreError.invalidInput("Domain route must not include resolvedIp")
|
|
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
|
+
)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
func download(_ params: FirmwareArtifactDownloadParams) async throws -> StoredFirmwareArtifact {
|
|
526
|
+
let validated = try Self.validateDownloadParams(params)
|
|
527
|
+
try rejectIfCancelled(transactionId: params.transactionId)
|
|
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(
|
|
541
|
+
transactionId: params.transactionId,
|
|
542
|
+
taskId: params.taskId,
|
|
543
|
+
expectedSize: validated.expectedSize,
|
|
544
|
+
expectedSha256: validated.expectedSha256,
|
|
545
|
+
downloadToken: validated.downloadToken
|
|
546
|
+
)
|
|
547
|
+
markDownloadActive(validated.downloadToken, delta: 1)
|
|
548
|
+
do {
|
|
549
|
+
let artifact = try await downloadCoordinator.run(
|
|
550
|
+
key: key,
|
|
551
|
+
transactionId: params.transactionId
|
|
552
|
+
) { [self] in
|
|
553
|
+
try rejectIfCancelled(transactionId: params.transactionId)
|
|
554
|
+
return try await downloadLocked(
|
|
555
|
+
params,
|
|
556
|
+
validated: validated
|
|
557
|
+
)
|
|
558
|
+
}
|
|
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)
|
|
566
|
+
return artifact
|
|
567
|
+
} catch {
|
|
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
|
+
)
|
|
573
|
+
if error is CancellationError ||
|
|
574
|
+
isTransactionCancelled(params.transactionId) {
|
|
575
|
+
throw FirmwareArtifactStoreError.downloadFailed(
|
|
576
|
+
"ARTIFACT_CANCELLED: firmware artifact download was cancelled"
|
|
577
|
+
)
|
|
578
|
+
}
|
|
579
|
+
throw error
|
|
580
|
+
}
|
|
581
|
+
}
|
|
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
|
+
|
|
609
|
+
func cancelDownloads(transactionId: String) async throws {
|
|
610
|
+
guard Self.isSafeIdentifier(transactionId) else {
|
|
611
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
612
|
+
"Invalid firmware transactionId"
|
|
613
|
+
)
|
|
614
|
+
}
|
|
615
|
+
cancellationLock.withFirmwareArtifactLock {
|
|
616
|
+
cancelledTransactions.insert(transactionId)
|
|
617
|
+
}
|
|
618
|
+
await downloadCoordinator.cancel(transactionId: transactionId)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
private func downloadLocked(
|
|
622
|
+
_ params: FirmwareArtifactDownloadParams,
|
|
623
|
+
validated: ValidatedDownload
|
|
624
|
+
) async throws -> StoredFirmwareArtifact {
|
|
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
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Firmware preflight is foreground-bound, so both routes use the same
|
|
638
|
+
// cancellable stream instead of waiting on process-owned background tasks.
|
|
639
|
+
let partialURL = rootURL.appendingPathComponent(
|
|
640
|
+
firmwareArtifactPartialFileName(
|
|
641
|
+
transactionId: params.transactionId,
|
|
642
|
+
taskId: params.taskId,
|
|
643
|
+
downloadToken: validated.downloadToken
|
|
644
|
+
),
|
|
645
|
+
isDirectory: false
|
|
646
|
+
)
|
|
647
|
+
if !fileManager.fileExists(atPath: partialURL.path) {
|
|
648
|
+
fileManager.createFile(atPath: partialURL.path, contents: nil)
|
|
649
|
+
}
|
|
650
|
+
var currentSize = try fileSize(partialURL)
|
|
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 {
|
|
656
|
+
try fileManager.removeItem(at: partialURL)
|
|
657
|
+
fileManager.createFile(atPath: partialURL.path, contents: nil)
|
|
658
|
+
currentSize = 0
|
|
659
|
+
}
|
|
660
|
+
if let expectedSize = validated.expectedSize, currentSize == expectedSize {
|
|
661
|
+
if let completed = try? validateDownloadedArtifact(
|
|
662
|
+
fileURL: partialURL,
|
|
663
|
+
expectedSize: expectedSize,
|
|
664
|
+
expectedSha256: validated.expectedSha256,
|
|
665
|
+
maxBytes: validated.maxBytes
|
|
666
|
+
) {
|
|
667
|
+
let finalURL = artifactURL(sha256: completed.sha256)
|
|
668
|
+
try promote(source: partialURL, destination: finalURL)
|
|
669
|
+
return StoredFirmwareArtifact(
|
|
670
|
+
artifactRef: completed.artifactRef,
|
|
671
|
+
size: completed.size,
|
|
672
|
+
sha256: completed.sha256,
|
|
673
|
+
fileURL: finalURL
|
|
674
|
+
)
|
|
675
|
+
}
|
|
676
|
+
try fileManager.removeItem(at: partialURL)
|
|
677
|
+
fileManager.createFile(atPath: partialURL.path, contents: nil)
|
|
678
|
+
currentSize = 0
|
|
679
|
+
}
|
|
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
|
+
)
|
|
685
|
+
try await streamDownload(
|
|
686
|
+
params,
|
|
687
|
+
partialURL: partialURL,
|
|
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)"
|
|
694
|
+
)
|
|
695
|
+
let artifact: StoredFirmwareArtifact
|
|
696
|
+
do {
|
|
697
|
+
artifact = try validateDownloadedArtifact(
|
|
698
|
+
fileURL: partialURL,
|
|
699
|
+
expectedSize: validated.expectedSize,
|
|
700
|
+
expectedSha256: validated.expectedSha256,
|
|
701
|
+
maxBytes: validated.maxBytes
|
|
702
|
+
)
|
|
703
|
+
} catch {
|
|
704
|
+
try? fileManager.removeItem(at: partialURL)
|
|
705
|
+
throw error
|
|
706
|
+
}
|
|
707
|
+
let finalURL = artifactURL(sha256: artifact.sha256)
|
|
708
|
+
try promote(source: partialURL, destination: finalURL)
|
|
709
|
+
return StoredFirmwareArtifact(
|
|
710
|
+
artifactRef: artifact.artifactRef,
|
|
711
|
+
size: artifact.size,
|
|
712
|
+
sha256: artifact.sha256,
|
|
713
|
+
fileURL: finalURL
|
|
714
|
+
)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
func discard(artifactRef: String) throws {
|
|
718
|
+
let fileURL = try resolveArtifactURL(artifactRef)
|
|
719
|
+
leaseLock.lock()
|
|
720
|
+
let isRetained = leases.values.contains {
|
|
721
|
+
$0.artifactRefs.contains(artifactRef)
|
|
722
|
+
}
|
|
723
|
+
leaseLock.unlock()
|
|
724
|
+
guard !isRetained else {
|
|
725
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
726
|
+
"ARTIFACT_LEASED: firmware artifact is retained"
|
|
727
|
+
)
|
|
728
|
+
}
|
|
729
|
+
readerLock.lock()
|
|
730
|
+
let hasMatchingReader = readers.values.contains {
|
|
731
|
+
$0.fileURL == fileURL
|
|
732
|
+
}
|
|
733
|
+
readerLock.unlock()
|
|
734
|
+
guard !hasMatchingReader else {
|
|
735
|
+
throw FirmwareArtifactStoreError.readerInvalid(
|
|
736
|
+
"ARTIFACT_BUSY: firmware artifact has an open reader"
|
|
737
|
+
)
|
|
738
|
+
}
|
|
739
|
+
if fileManager.fileExists(atPath: fileURL.path) {
|
|
740
|
+
try fileManager.removeItem(at: fileURL)
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
func open(artifactRef: String) throws -> (readerId: String, size: Int64) {
|
|
745
|
+
let fileURL = try resolveArtifactURL(artifactRef)
|
|
746
|
+
let size = try fileSize(fileURL)
|
|
747
|
+
guard size > 0 else {
|
|
748
|
+
throw FirmwareArtifactStoreError.readerInvalid("Firmware artifact is empty")
|
|
749
|
+
}
|
|
750
|
+
let expectedSha256 = String(artifactRef.dropFirst(3))
|
|
751
|
+
guard try hashFile(fileURL) == expectedSha256 else {
|
|
752
|
+
throw FirmwareArtifactStoreError.integrityMismatch(
|
|
753
|
+
"Firmware artifact SHA-256 mismatch"
|
|
754
|
+
)
|
|
755
|
+
}
|
|
756
|
+
let readerId = UUID().uuidString
|
|
757
|
+
let reader = OpenReader(
|
|
758
|
+
handle: try FileHandle(forReadingFrom: fileURL),
|
|
759
|
+
fileURL: fileURL,
|
|
760
|
+
size: size
|
|
761
|
+
)
|
|
762
|
+
readerLock.lock()
|
|
763
|
+
readers[readerId] = reader
|
|
764
|
+
readerLock.unlock()
|
|
765
|
+
return (readerId, size)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
func read(readerId: String, offset: Int64, length: Int) throws -> Data {
|
|
769
|
+
guard length > 0, length <= Self.maxReadBytes, offset >= 0 else {
|
|
770
|
+
throw FirmwareArtifactStoreError.readerInvalid("Invalid firmware artifact read")
|
|
771
|
+
}
|
|
772
|
+
readerLock.lock()
|
|
773
|
+
defer { readerLock.unlock() }
|
|
774
|
+
guard
|
|
775
|
+
let reader = readers[readerId],
|
|
776
|
+
offset <= reader.size - Int64(length)
|
|
777
|
+
else {
|
|
778
|
+
throw FirmwareArtifactStoreError.readerInvalid("Firmware artifact read is out of bounds")
|
|
779
|
+
}
|
|
780
|
+
try reader.handle.seek(toOffset: UInt64(offset))
|
|
781
|
+
let data = try reader.handle.read(upToCount: length) ?? Data()
|
|
782
|
+
guard data.count == length else {
|
|
783
|
+
throw FirmwareArtifactStoreError.readerInvalid("Firmware artifact returned a short read")
|
|
784
|
+
}
|
|
785
|
+
return data
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
func close(readerId: String) throws {
|
|
789
|
+
readerLock.lock()
|
|
790
|
+
let reader = readers.removeValue(forKey: readerId)
|
|
791
|
+
readerLock.unlock()
|
|
792
|
+
guard let reader else {
|
|
793
|
+
return
|
|
794
|
+
}
|
|
795
|
+
try reader.handle.close()
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
func materializeArchive(
|
|
799
|
+
leaseRef: String,
|
|
800
|
+
artifactRef: String,
|
|
801
|
+
expectedEntries: [FirmwareArchiveExpectedEntry]?
|
|
802
|
+
) throws -> [StoredFirmwareArchiveEntry] {
|
|
803
|
+
try requireLease(leaseRef)
|
|
804
|
+
let archiveURL = try resolveArtifactURL(artifactRef)
|
|
805
|
+
let scratchURL = rootURL.appendingPathComponent(
|
|
806
|
+
"archive-\(UUID().uuidString)",
|
|
807
|
+
isDirectory: true
|
|
808
|
+
)
|
|
809
|
+
try fileManager.createDirectory(at: scratchURL, withIntermediateDirectories: true)
|
|
810
|
+
defer { try? fileManager.removeItem(at: scratchURL) }
|
|
811
|
+
|
|
812
|
+
let archiveEntries = try FirmwareArchiveMinizipBridge.scanArchive(
|
|
813
|
+
atPath: archiveURL.path
|
|
814
|
+
)
|
|
815
|
+
let requirements = try resolveArchiveRequirements(
|
|
816
|
+
expectedEntries,
|
|
817
|
+
archiveEntries: archiveEntries
|
|
818
|
+
)
|
|
819
|
+
try validateArchiveEntries(
|
|
820
|
+
archiveEntries,
|
|
821
|
+
requirements: requirements
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
var staged: [StagedFirmwareArchiveEntry] = []
|
|
825
|
+
for (index, requirement) in requirements.enumerated() {
|
|
826
|
+
let stagingURL = scratchURL.appendingPathComponent(
|
|
827
|
+
"\(index).entry",
|
|
828
|
+
isDirectory: false
|
|
829
|
+
)
|
|
830
|
+
fileManager.createFile(atPath: stagingURL.path, contents: nil)
|
|
831
|
+
let handle = try FileHandle(forWritingTo: stagingURL)
|
|
832
|
+
var hasher = SHA256()
|
|
833
|
+
var actualSize: Int64 = 0
|
|
834
|
+
var consumerError: Error?
|
|
835
|
+
do {
|
|
836
|
+
try FirmwareArchiveMinizipBridge.extractEntryNamed(
|
|
837
|
+
requirement.entryName,
|
|
838
|
+
archivePath: archiveURL.path
|
|
839
|
+
) { chunk in
|
|
840
|
+
guard consumerError == nil else {
|
|
841
|
+
return false
|
|
842
|
+
}
|
|
843
|
+
do {
|
|
844
|
+
actualSize += Int64(chunk.count)
|
|
845
|
+
guard actualSize <= requirement.expectedSize else {
|
|
846
|
+
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
847
|
+
"Firmware archive entry exceeds its expected size"
|
|
848
|
+
)
|
|
849
|
+
}
|
|
850
|
+
hasher.update(data: chunk)
|
|
851
|
+
try handle.write(contentsOf: chunk)
|
|
852
|
+
return true
|
|
853
|
+
} catch {
|
|
854
|
+
consumerError = error
|
|
855
|
+
return false
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if let consumerError {
|
|
859
|
+
throw consumerError
|
|
860
|
+
}
|
|
861
|
+
try handle.synchronize()
|
|
862
|
+
try handle.close()
|
|
863
|
+
} catch {
|
|
864
|
+
try? handle.close()
|
|
865
|
+
throw error
|
|
866
|
+
}
|
|
867
|
+
let sha256 = hasher.finalize().map {
|
|
868
|
+
String(format: "%02x", $0)
|
|
869
|
+
}.joined()
|
|
870
|
+
guard
|
|
871
|
+
actualSize == requirement.expectedSize,
|
|
872
|
+
requirement.expectedSha256.map({ sha256 == $0 }) ?? true
|
|
873
|
+
else {
|
|
874
|
+
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
875
|
+
"Firmware archive entry integrity mismatch"
|
|
876
|
+
)
|
|
877
|
+
}
|
|
878
|
+
staged.append(
|
|
879
|
+
StagedFirmwareArchiveEntry(
|
|
880
|
+
entryName: requirement.entryName,
|
|
881
|
+
size: actualSize,
|
|
882
|
+
sha256: sha256,
|
|
883
|
+
stagingURL: stagingURL
|
|
884
|
+
)
|
|
885
|
+
)
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return try staged.map { entry in
|
|
889
|
+
let destination = artifactURL(sha256: entry.sha256)
|
|
890
|
+
if (try? validateStoredArtifact(
|
|
891
|
+
fileURL: destination,
|
|
892
|
+
expectedSize: entry.size,
|
|
893
|
+
expectedSha256: entry.sha256
|
|
894
|
+
)) == nil {
|
|
895
|
+
try promote(source: entry.stagingURL, destination: destination)
|
|
896
|
+
}
|
|
897
|
+
let stored = StoredFirmwareArtifact(
|
|
898
|
+
artifactRef: "fw:\(entry.sha256)",
|
|
899
|
+
size: entry.size,
|
|
900
|
+
sha256: entry.sha256,
|
|
901
|
+
fileURL: destination
|
|
902
|
+
)
|
|
903
|
+
try retainExpectedArtifact(
|
|
904
|
+
leaseRef: leaseRef,
|
|
905
|
+
transactionId: nil,
|
|
906
|
+
artifactRef: stored.artifactRef
|
|
907
|
+
)
|
|
908
|
+
return StoredFirmwareArchiveEntry(
|
|
909
|
+
entryName: entry.entryName,
|
|
910
|
+
artifact: stored
|
|
911
|
+
)
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
func createLease(transactionId: String) throws -> String {
|
|
916
|
+
guard Self.isSafeIdentifier(transactionId) else {
|
|
917
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
918
|
+
"Invalid firmware transactionId"
|
|
919
|
+
)
|
|
920
|
+
}
|
|
921
|
+
leaseLock.lock()
|
|
922
|
+
defer { leaseLock.unlock() }
|
|
923
|
+
guard leases.count < 32 else {
|
|
924
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
925
|
+
"Too many firmware artifact leases"
|
|
926
|
+
)
|
|
927
|
+
}
|
|
928
|
+
let leaseRef = "fwlease:\(UUID().uuidString.lowercased())"
|
|
929
|
+
leases[leaseRef] = LeaseState(
|
|
930
|
+
transactionId: transactionId,
|
|
931
|
+
artifactRefs: []
|
|
932
|
+
)
|
|
933
|
+
return leaseRef
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
func retain(leaseRef: String, artifactRef: String) throws {
|
|
937
|
+
_ = try resolveArtifactURL(artifactRef)
|
|
938
|
+
try retainExpectedArtifact(
|
|
939
|
+
leaseRef: leaseRef,
|
|
940
|
+
transactionId: nil,
|
|
941
|
+
artifactRef: artifactRef
|
|
942
|
+
)
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
func releaseLease(leaseRef: String, disposition: String) throws {
|
|
946
|
+
guard
|
|
947
|
+
disposition == "completed" ||
|
|
948
|
+
disposition == "safeCancelled" ||
|
|
949
|
+
disposition == "safeAbandoned"
|
|
950
|
+
else {
|
|
951
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
952
|
+
"Invalid firmware lease disposition"
|
|
953
|
+
)
|
|
954
|
+
}
|
|
955
|
+
let transactionId: String = try {
|
|
956
|
+
leaseLock.lock()
|
|
957
|
+
defer { leaseLock.unlock() }
|
|
958
|
+
guard
|
|
959
|
+
let lease = leases.removeValue(
|
|
960
|
+
forKey: try validateLeaseRef(leaseRef)
|
|
961
|
+
)
|
|
962
|
+
else {
|
|
963
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
964
|
+
"Firmware artifact lease is unavailable"
|
|
965
|
+
)
|
|
966
|
+
}
|
|
967
|
+
return lease.transactionId
|
|
968
|
+
}()
|
|
969
|
+
cancellationLock.withFirmwareArtifactLock {
|
|
970
|
+
cancelledTransactions.remove(transactionId)
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
func sweepOrphans() throws -> (deletedFiles: Int, deletedBytes: Int64) {
|
|
975
|
+
leaseLock.lock()
|
|
976
|
+
let retained = Set(
|
|
977
|
+
leases.values
|
|
978
|
+
.flatMap(\.artifactRefs)
|
|
979
|
+
.map { String($0.dropFirst(3)) }
|
|
980
|
+
)
|
|
981
|
+
leaseLock.unlock()
|
|
982
|
+
activeDownloadLock.lock()
|
|
983
|
+
let active = Set(activeDownloadCounts.filter { $0.value > 0 }.keys)
|
|
984
|
+
activeDownloadLock.unlock()
|
|
985
|
+
readerLock.lock()
|
|
986
|
+
let openPaths = Set(readers.values.map(\.fileURL.path))
|
|
987
|
+
readerLock.unlock()
|
|
988
|
+
|
|
989
|
+
return try sweepFirmwareArtifactOrphansAtRoot(
|
|
990
|
+
rootURL,
|
|
991
|
+
retainedSha256: retained,
|
|
992
|
+
activeSha256: active,
|
|
993
|
+
openPaths: openPaths,
|
|
994
|
+
fileManager: fileManager
|
|
995
|
+
)
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
private func requireLease(_ leaseRef: String) throws {
|
|
999
|
+
leaseLock.lock()
|
|
1000
|
+
defer { leaseLock.unlock() }
|
|
1001
|
+
guard leases[try validateLeaseRef(leaseRef)] != nil else {
|
|
1002
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
1003
|
+
"Firmware artifact lease is unavailable"
|
|
1004
|
+
)
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
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
|
+
|
|
1024
|
+
private func retainExpectedArtifact(
|
|
1025
|
+
leaseRef: String,
|
|
1026
|
+
transactionId: String?,
|
|
1027
|
+
artifactRef: String
|
|
1028
|
+
) throws {
|
|
1029
|
+
guard artifactRef.range(
|
|
1030
|
+
of: "^fw:[a-f0-9]{64}$",
|
|
1031
|
+
options: .regularExpression
|
|
1032
|
+
) != nil else {
|
|
1033
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
1034
|
+
"Invalid firmware artifactRef"
|
|
1035
|
+
)
|
|
1036
|
+
}
|
|
1037
|
+
leaseLock.lock()
|
|
1038
|
+
defer { leaseLock.unlock() }
|
|
1039
|
+
let validatedLeaseRef = try validateLeaseRef(leaseRef)
|
|
1040
|
+
guard var lease = leases[validatedLeaseRef] else {
|
|
1041
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
1042
|
+
"Firmware artifact lease is unavailable"
|
|
1043
|
+
)
|
|
1044
|
+
}
|
|
1045
|
+
if let transactionId, lease.transactionId != transactionId {
|
|
1046
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
1047
|
+
"Firmware artifact lease transaction mismatch"
|
|
1048
|
+
)
|
|
1049
|
+
}
|
|
1050
|
+
if lease.artifactRefs.insert(artifactRef).inserted {
|
|
1051
|
+
leases[validatedLeaseRef] = lease
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
private func validateLeaseRef(_ leaseRef: String) throws -> String {
|
|
1056
|
+
guard Self.isValidLeaseRef(leaseRef) else {
|
|
1057
|
+
throw FirmwareArtifactStoreError.invalidInput(
|
|
1058
|
+
"Invalid firmware leaseRef"
|
|
1059
|
+
)
|
|
1060
|
+
}
|
|
1061
|
+
return leaseRef
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
private static func isValidLeaseRef(_ value: String) -> Bool {
|
|
1065
|
+
value.range(
|
|
1066
|
+
of: "^fwlease:[a-f0-9-]{36}$",
|
|
1067
|
+
options: .regularExpression
|
|
1068
|
+
) != nil
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
static func isSafeIdentifier(_ value: String) -> Bool {
|
|
1072
|
+
firmwareArtifactIdentifierIsSafe(value)
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
private func markDownloadActive(_ sha256: String, delta: Int) {
|
|
1076
|
+
activeDownloadLock.lock()
|
|
1077
|
+
let next = (activeDownloadCounts[sha256] ?? 0) + delta
|
|
1078
|
+
if next <= 0 {
|
|
1079
|
+
activeDownloadCounts.removeValue(forKey: sha256)
|
|
1080
|
+
} else {
|
|
1081
|
+
activeDownloadCounts[sha256] = next
|
|
1082
|
+
}
|
|
1083
|
+
activeDownloadLock.unlock()
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
private func validateArchiveRequirements(
|
|
1087
|
+
_ expectedEntries: [FirmwareArchiveExpectedEntry]
|
|
1088
|
+
) throws -> [FirmwareArchiveRequirement] {
|
|
1089
|
+
guard !expectedEntries.isEmpty, expectedEntries.count <= 4096 else {
|
|
1090
|
+
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
1091
|
+
"Firmware archive expected entry count is invalid"
|
|
1092
|
+
)
|
|
1093
|
+
}
|
|
1094
|
+
var names = Set<String>()
|
|
1095
|
+
var canonicalNames = Set<String>()
|
|
1096
|
+
var totalSize: Int64 = 0
|
|
1097
|
+
for entry in expectedEntries {
|
|
1098
|
+
guard
|
|
1099
|
+
!entry.artifactId.isEmpty,
|
|
1100
|
+
entry.artifactId.count <= 160,
|
|
1101
|
+
entry.artifactId.allSatisfy({
|
|
1102
|
+
$0.isLetter || $0.isNumber || "._-".contains($0)
|
|
1103
|
+
}),
|
|
1104
|
+
isPortableArchiveEntryName(
|
|
1105
|
+
entry.entryName,
|
|
1106
|
+
canonicalNames: &canonicalNames
|
|
1107
|
+
),
|
|
1108
|
+
names.insert(entry.entryName).inserted,
|
|
1109
|
+
entry.expectedSize.isFinite,
|
|
1110
|
+
entry.expectedSize > 0,
|
|
1111
|
+
entry.expectedSize <= Double(128 * 1024 * 1024),
|
|
1112
|
+
entry.expectedSize.rounded() == entry.expectedSize,
|
|
1113
|
+
entry.expectedSha256.range(
|
|
1114
|
+
of: "^[a-fA-F0-9]{64}$",
|
|
1115
|
+
options: .regularExpression
|
|
1116
|
+
) != nil
|
|
1117
|
+
else {
|
|
1118
|
+
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
1119
|
+
"Firmware archive expected entry is invalid"
|
|
1120
|
+
)
|
|
1121
|
+
}
|
|
1122
|
+
totalSize += Int64(entry.expectedSize)
|
|
1123
|
+
guard totalSize <= 512 * 1024 * 1024 else {
|
|
1124
|
+
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
1125
|
+
"Firmware archive expected entries exceed limits"
|
|
1126
|
+
)
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
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
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
private func validateArchiveEntries(
|
|
1169
|
+
_ entries: [FirmwareArchiveEntryInfo],
|
|
1170
|
+
requirements: [FirmwareArchiveRequirement]
|
|
1171
|
+
) throws {
|
|
1172
|
+
guard entries.count == requirements.count else {
|
|
1173
|
+
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
1174
|
+
"Firmware archive has missing or extra entries"
|
|
1175
|
+
)
|
|
1176
|
+
}
|
|
1177
|
+
let requirementsByName = Dictionary(
|
|
1178
|
+
uniqueKeysWithValues: requirements.map { ($0.entryName, $0) }
|
|
1179
|
+
)
|
|
1180
|
+
var names = Set<String>()
|
|
1181
|
+
var canonicalNames = Set<String>()
|
|
1182
|
+
var totalSize: Int64 = 0
|
|
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
|
|
1198
|
+
guard
|
|
1199
|
+
names.insert(entry.name).inserted,
|
|
1200
|
+
isPortableArchiveEntryName(
|
|
1201
|
+
entry.name,
|
|
1202
|
+
canonicalNames: &canonicalNames
|
|
1203
|
+
),
|
|
1204
|
+
entry.uncompressedSize == requirement.expectedSize,
|
|
1205
|
+
entry.uncompressedSize > 0,
|
|
1206
|
+
entry.uncompressedSize <= 128 * 1024 * 1024,
|
|
1207
|
+
totalSize <= 512 * 1024 * 1024,
|
|
1208
|
+
entry.compressedSize >= 0,
|
|
1209
|
+
entry.compressedSize <= 512 * 1024 * 1024,
|
|
1210
|
+
entry.uncompressedSize <= max(entry.compressedSize, 1) * 1000,
|
|
1211
|
+
entry.diskNumber == 0,
|
|
1212
|
+
entry.flags & 1 == 0,
|
|
1213
|
+
entry.compressionMethod == 0 || entry.compressionMethod == 8,
|
|
1214
|
+
entry.linkName?.isEmpty != false,
|
|
1215
|
+
isRegularArchiveEntry(entry)
|
|
1216
|
+
else {
|
|
1217
|
+
throw FirmwareArtifactStoreError.archiveInvalid(
|
|
1218
|
+
"Firmware archive entry metadata is invalid"
|
|
1219
|
+
)
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
private func isPortableArchiveEntryName(
|
|
1225
|
+
_ name: String,
|
|
1226
|
+
canonicalNames: inout Set<String>
|
|
1227
|
+
) -> Bool {
|
|
1228
|
+
let lowered = name.precomposedStringWithCanonicalMapping.lowercased()
|
|
1229
|
+
let components = name.split(
|
|
1230
|
+
separator: "/",
|
|
1231
|
+
omittingEmptySubsequences: false
|
|
1232
|
+
)
|
|
1233
|
+
let nestedArchiveExtensions = [
|
|
1234
|
+
".zip", ".7z", ".rar", ".tar", ".gz", ".tgz",
|
|
1235
|
+
]
|
|
1236
|
+
guard
|
|
1237
|
+
!name.isEmpty,
|
|
1238
|
+
name.count <= 512,
|
|
1239
|
+
name == name.precomposedStringWithCanonicalMapping,
|
|
1240
|
+
!name.hasPrefix("/"),
|
|
1241
|
+
!name.hasPrefix("\\"),
|
|
1242
|
+
!name.contains("\\"),
|
|
1243
|
+
!name.contains(":"),
|
|
1244
|
+
name.unicodeScalars.allSatisfy({
|
|
1245
|
+
!CharacterSet.controlCharacters.contains($0)
|
|
1246
|
+
}),
|
|
1247
|
+
components.allSatisfy({
|
|
1248
|
+
!$0.isEmpty &&
|
|
1249
|
+
$0 != "." &&
|
|
1250
|
+
$0 != ".." &&
|
|
1251
|
+
!$0.hasSuffix(".") &&
|
|
1252
|
+
!$0.hasSuffix(" ")
|
|
1253
|
+
}),
|
|
1254
|
+
!nestedArchiveExtensions.contains(where: {
|
|
1255
|
+
lowered.hasSuffix($0)
|
|
1256
|
+
}),
|
|
1257
|
+
canonicalNames.insert(lowered).inserted
|
|
1258
|
+
else {
|
|
1259
|
+
return false
|
|
1260
|
+
}
|
|
1261
|
+
return true
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
private func isRegularArchiveEntry(
|
|
1265
|
+
_ entry: FirmwareArchiveEntryInfo
|
|
1266
|
+
) -> Bool {
|
|
1267
|
+
let hostSystem = UInt8(entry.versionMadeBy >> 8)
|
|
1268
|
+
if hostSystem == 3 || hostSystem == 19 {
|
|
1269
|
+
let fileType = (entry.externalAttributes >> 16) & 0xF000
|
|
1270
|
+
return fileType == 0 || fileType == 0x8000
|
|
1271
|
+
}
|
|
1272
|
+
return entry.externalAttributes & 0x10 == 0
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
private func streamDownload(
|
|
1276
|
+
_ params: FirmwareArtifactDownloadParams,
|
|
1277
|
+
partialURL: URL,
|
|
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
|
|
1305
|
+
) async throws {
|
|
1306
|
+
guard let url = URL(string: params.url), let hostname = url.host else {
|
|
1307
|
+
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware URL")
|
|
1308
|
+
}
|
|
1309
|
+
var request = URLRequest(url: url)
|
|
1310
|
+
request.cachePolicy = .reloadIgnoringLocalCacheData
|
|
1311
|
+
request.timeoutInterval =
|
|
1312
|
+
params.overallDeadlineSeconds ?? Self.defaultDownloadDeadline
|
|
1313
|
+
request.setValue("identity", forHTTPHeaderField: "Accept-Encoding")
|
|
1314
|
+
if resumeOffset > 0 {
|
|
1315
|
+
request.setValue("bytes=\(resumeOffset)-", forHTTPHeaderField: "Range")
|
|
1316
|
+
}
|
|
1317
|
+
|
|
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(
|
|
1330
|
+
configuration: .ephemeral,
|
|
1331
|
+
delegate: streamDelegate,
|
|
1332
|
+
delegateQueue: nil
|
|
1333
|
+
)
|
|
1334
|
+
defer {
|
|
1335
|
+
session.finishTasksAndInvalidate()
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
do {
|
|
1339
|
+
try await streamDelegate.run(session: session, request: request)
|
|
1340
|
+
} catch let error as FirmwareArtifactStoreError {
|
|
1341
|
+
throw error
|
|
1342
|
+
} catch {
|
|
1343
|
+
if isCancellationError(
|
|
1344
|
+
error,
|
|
1345
|
+
transactionId: params.transactionId
|
|
1346
|
+
) {
|
|
1347
|
+
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1348
|
+
"ARTIFACT_CANCELLED: firmware artifact download was cancelled"
|
|
1349
|
+
)
|
|
1350
|
+
}
|
|
1351
|
+
if isFirmwareArtifactTLSError(error) {
|
|
1352
|
+
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1353
|
+
"ARTIFACT_TLS_FAILED: firmware TLS validation failed"
|
|
1354
|
+
)
|
|
1355
|
+
}
|
|
1356
|
+
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1357
|
+
"ARTIFACT_NETWORK_FAILED: firmware request failed"
|
|
1358
|
+
)
|
|
1359
|
+
}
|
|
1360
|
+
try rejectIfCancelled(transactionId: params.transactionId)
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
private func rejectIfCancelled(transactionId: String) throws {
|
|
1364
|
+
if isTransactionCancelled(transactionId) {
|
|
1365
|
+
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1366
|
+
"ARTIFACT_CANCELLED: firmware artifact download was cancelled"
|
|
1367
|
+
)
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
func isTransactionCancelled(_ transactionId: String) -> Bool {
|
|
1372
|
+
cancellationLock.withFirmwareArtifactLock {
|
|
1373
|
+
cancelledTransactions.contains(transactionId)
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
private func isCancellationError(
|
|
1378
|
+
_ error: Error,
|
|
1379
|
+
transactionId: String
|
|
1380
|
+
) -> Bool {
|
|
1381
|
+
if error is CancellationError ||
|
|
1382
|
+
isTransactionCancelled(transactionId) {
|
|
1383
|
+
return true
|
|
1384
|
+
}
|
|
1385
|
+
let nsError = error as NSError
|
|
1386
|
+
return nsError.domain == NSURLErrorDomain &&
|
|
1387
|
+
nsError.code == NSURLErrorCancelled
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
private func validateStoredArtifact(
|
|
1391
|
+
fileURL: URL,
|
|
1392
|
+
expectedSize: Int64,
|
|
1393
|
+
expectedSha256: String
|
|
1394
|
+
) throws -> StoredFirmwareArtifact {
|
|
1395
|
+
let size = try fileSize(fileURL)
|
|
1396
|
+
guard size == expectedSize else {
|
|
1397
|
+
throw FirmwareArtifactStoreError.integrityMismatch(
|
|
1398
|
+
"ARTIFACT_INTEGRITY_FAILED: firmware artifact size mismatch"
|
|
1399
|
+
)
|
|
1400
|
+
}
|
|
1401
|
+
let sha256 = try hashFile(fileURL)
|
|
1402
|
+
guard sha256 == expectedSha256 else {
|
|
1403
|
+
throw FirmwareArtifactStoreError.integrityMismatch(
|
|
1404
|
+
"ARTIFACT_INTEGRITY_FAILED: firmware artifact SHA-256 mismatch"
|
|
1405
|
+
)
|
|
1406
|
+
}
|
|
1407
|
+
return StoredFirmwareArtifact(
|
|
1408
|
+
artifactRef: "fw:\(sha256)",
|
|
1409
|
+
size: size,
|
|
1410
|
+
sha256: sha256,
|
|
1411
|
+
fileURL: fileURL
|
|
1412
|
+
)
|
|
1413
|
+
}
|
|
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
|
+
|
|
1445
|
+
private func resolveArtifactURL(_ artifactRef: String) throws -> URL {
|
|
1446
|
+
guard artifactRef.hasPrefix("fw:") else {
|
|
1447
|
+
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware artifactRef")
|
|
1448
|
+
}
|
|
1449
|
+
let sha256 = String(artifactRef.dropFirst(3))
|
|
1450
|
+
guard sha256.range(of: "^[a-f0-9]{64}$", options: .regularExpression) != nil else {
|
|
1451
|
+
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware artifactRef")
|
|
1452
|
+
}
|
|
1453
|
+
let url = artifactURL(sha256: sha256)
|
|
1454
|
+
guard fileManager.fileExists(atPath: url.path) else {
|
|
1455
|
+
throw FirmwareArtifactStoreError.invalidInput("Firmware artifact not found")
|
|
1456
|
+
}
|
|
1457
|
+
return url
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
private func artifactURL(sha256: String) -> URL {
|
|
1461
|
+
rootURL.appendingPathComponent("\(sha256).bin", isDirectory: false)
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
private func fileSize(_ url: URL) throws -> Int64 {
|
|
1465
|
+
let attributes = try fileManager.attributesOfItem(atPath: url.path)
|
|
1466
|
+
return (attributes[.size] as? NSNumber)?.int64Value ?? 0
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
private func hashFile(_ url: URL) throws -> String {
|
|
1470
|
+
let handle = try FileHandle(forReadingFrom: url)
|
|
1471
|
+
defer { try? handle.close() }
|
|
1472
|
+
var hasher = SHA256()
|
|
1473
|
+
while let data = try handle.read(upToCount: 256 * 1024), !data.isEmpty {
|
|
1474
|
+
hasher.update(data: data)
|
|
1475
|
+
}
|
|
1476
|
+
return hasher.finalize().map { String(format: "%02x", $0) }.joined()
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
private func promote(source: URL, destination: URL) throws {
|
|
1480
|
+
let stagingURL = destination
|
|
1481
|
+
.deletingLastPathComponent()
|
|
1482
|
+
.appendingPathComponent(".promote-\(UUID().uuidString)", isDirectory: false)
|
|
1483
|
+
try fileManager.moveItem(at: source, to: stagingURL)
|
|
1484
|
+
defer {
|
|
1485
|
+
if fileManager.fileExists(atPath: stagingURL.path) {
|
|
1486
|
+
try? fileManager.removeItem(at: stagingURL)
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if fileManager.fileExists(atPath: destination.path) {
|
|
1490
|
+
_ = try fileManager.replaceItemAt(
|
|
1491
|
+
destination,
|
|
1492
|
+
withItemAt: stagingURL,
|
|
1493
|
+
backupItemName: nil,
|
|
1494
|
+
options: []
|
|
1495
|
+
)
|
|
1496
|
+
} else {
|
|
1497
|
+
try fileManager.moveItem(at: stagingURL, to: destination)
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
private extension NSLock {
|
|
1503
|
+
func withFirmwareArtifactLock<T>(_ body: () -> T) -> T {
|
|
1504
|
+
lock()
|
|
1505
|
+
defer { unlock() }
|
|
1506
|
+
return body()
|
|
1507
|
+
}
|
|
1508
|
+
}
|