@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,6 +1,303 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import CommonCrypto
|
|
3
3
|
|
|
4
|
+
enum FirmwareArtifactStoreError: Error, CustomStringConvertible {
|
|
5
|
+
case invalidInput(String)
|
|
6
|
+
case downloadFailed(String)
|
|
7
|
+
case integrityMismatch(String)
|
|
8
|
+
case readerInvalid(String)
|
|
9
|
+
case archiveInvalid(String)
|
|
10
|
+
|
|
11
|
+
var description: String {
|
|
12
|
+
switch self {
|
|
13
|
+
case let .invalidInput(message), let .downloadFailed(message),
|
|
14
|
+
let .integrityMismatch(message), let .readerInvalid(message),
|
|
15
|
+
let .archiveInvalid(message):
|
|
16
|
+
return message
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
enum FirmwareArtifactDeadlineError: Error {
|
|
22
|
+
case exceeded
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
enum FirmwareArtifactWallClockDeadline {
|
|
26
|
+
static func run<T>(
|
|
27
|
+
timeoutSeconds: TimeInterval,
|
|
28
|
+
operation: @escaping @Sendable () async throws -> T
|
|
29
|
+
) async throws -> T {
|
|
30
|
+
let timeoutNanoseconds = UInt64(max(0.001, timeoutSeconds) * 1_000_000_000.0)
|
|
31
|
+
return try await withThrowingTaskGroup(of: T.self) { group in
|
|
32
|
+
group.addTask {
|
|
33
|
+
try await operation()
|
|
34
|
+
}
|
|
35
|
+
group.addTask {
|
|
36
|
+
try await Task.sleep(nanoseconds: timeoutNanoseconds)
|
|
37
|
+
throw FirmwareArtifactDeadlineError.exceeded
|
|
38
|
+
}
|
|
39
|
+
defer { group.cancelAll() }
|
|
40
|
+
guard let result = try await group.next() else {
|
|
41
|
+
throw FirmwareArtifactDeadlineError.exceeded
|
|
42
|
+
}
|
|
43
|
+
return result
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func firmwareArtifactDownloadKey(
|
|
49
|
+
transactionId: String,
|
|
50
|
+
taskId: String,
|
|
51
|
+
expectedSize: Int64?,
|
|
52
|
+
expectedSha256: String?,
|
|
53
|
+
downloadToken: String
|
|
54
|
+
) -> String {
|
|
55
|
+
"\(transactionId)|\(taskId)|\(expectedSize.map(String.init) ?? "unknown")|\(expectedSha256 ?? "unknown")|\(downloadToken)"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
enum FirmwareArchiveDiscoveredEntriesIssue: Equatable {
|
|
59
|
+
case empty
|
|
60
|
+
case duplicateName
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func firmwareArchiveDiscoveredEntriesIssue(
|
|
64
|
+
_ entryNames: [String]
|
|
65
|
+
) -> FirmwareArchiveDiscoveredEntriesIssue? {
|
|
66
|
+
guard !entryNames.isEmpty else {
|
|
67
|
+
return .empty
|
|
68
|
+
}
|
|
69
|
+
var names = Set<String>()
|
|
70
|
+
guard entryNames.allSatisfy({ names.insert($0).inserted }) else {
|
|
71
|
+
return .duplicateName
|
|
72
|
+
}
|
|
73
|
+
return nil
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func firmwareArtifactDownloadToken(
|
|
77
|
+
expectedSha256: String?,
|
|
78
|
+
url: String
|
|
79
|
+
) -> String {
|
|
80
|
+
if let expectedSha256 {
|
|
81
|
+
return expectedSha256.lowercased()
|
|
82
|
+
}
|
|
83
|
+
let urlData = Data(url.utf8)
|
|
84
|
+
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
|
85
|
+
urlData.withUnsafeBytes {
|
|
86
|
+
_ = CC_SHA256($0.baseAddress, CC_LONG(urlData.count), &hash)
|
|
87
|
+
}
|
|
88
|
+
return hash.map { String(format: "%02x", $0) }.joined()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func firmwareArtifactPartialFileName(
|
|
92
|
+
transactionId: String,
|
|
93
|
+
taskId: String,
|
|
94
|
+
downloadToken: String
|
|
95
|
+
) -> String {
|
|
96
|
+
let transactionData = Data(transactionId.utf8)
|
|
97
|
+
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
|
98
|
+
transactionData.withUnsafeBytes {
|
|
99
|
+
_ = CC_SHA256($0.baseAddress, CC_LONG(transactionData.count), &hash)
|
|
100
|
+
}
|
|
101
|
+
let transactionToken = hash
|
|
102
|
+
.prefix(8)
|
|
103
|
+
.map { String(format: "%02x", $0) }
|
|
104
|
+
.joined()
|
|
105
|
+
return "\(downloadToken).\(taskId).\(transactionToken).partial"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func firmwareArtifactContentRangeIsValid(
|
|
109
|
+
_ value: String,
|
|
110
|
+
expectedStart: Int64,
|
|
111
|
+
expectedTotal: Int64?,
|
|
112
|
+
maxBytes: Int64
|
|
113
|
+
) -> Bool {
|
|
114
|
+
guard value.lowercased().hasPrefix("bytes ") else {
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
guard
|
|
118
|
+
let bounds = RangeDownloadLogic.parseContentRangeBounds(value),
|
|
119
|
+
let total = RangeDownloadLogic.parseContentRangeTotal(value)
|
|
120
|
+
else {
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
let totalMatches = expectedTotal.map { total == $0 } ?? (total > 0 && total <= maxBytes)
|
|
124
|
+
return bounds.start == expectedStart &&
|
|
125
|
+
bounds.end >= bounds.start &&
|
|
126
|
+
bounds.end < total &&
|
|
127
|
+
totalMatches
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func firmwareArtifactResponseFits(
|
|
131
|
+
expectedContentLength: Int64,
|
|
132
|
+
baseOffset: Int64,
|
|
133
|
+
maxBytes: Int64
|
|
134
|
+
) -> Bool {
|
|
135
|
+
guard baseOffset >= 0, baseOffset <= maxBytes else {
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
return expectedContentLength < 0 ||
|
|
139
|
+
expectedContentLength <= maxBytes - baseOffset
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let firmwareArtifactFinalGrace: TimeInterval = 24 * 60 * 60
|
|
143
|
+
let firmwareArtifactPartialGrace: TimeInterval = 7 * 24 * 60 * 60
|
|
144
|
+
let firmwareArtifactScratchGrace: TimeInterval = firmwareArtifactPartialGrace
|
|
145
|
+
|
|
146
|
+
func firmwareArtifactIdentifierIsSafe(_ value: String) -> Bool {
|
|
147
|
+
value.range(
|
|
148
|
+
of: "^[A-Za-z0-9._:-]{1,160}$",
|
|
149
|
+
options: .regularExpression
|
|
150
|
+
) != nil
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private func firmwareArtifactScratchNameIsValid(
|
|
154
|
+
_ name: String,
|
|
155
|
+
prefix: String
|
|
156
|
+
) -> Bool {
|
|
157
|
+
guard name.hasPrefix(prefix) else {
|
|
158
|
+
return false
|
|
159
|
+
}
|
|
160
|
+
let rawUUID = String(name.dropFirst(prefix.count))
|
|
161
|
+
guard
|
|
162
|
+
rawUUID.count == 36,
|
|
163
|
+
let uuid = UUID(uuidString: rawUUID)
|
|
164
|
+
else {
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
return uuid.uuidString.caseInsensitiveCompare(rawUUID) == .orderedSame
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private func firmwareArtifactEntrySize(
|
|
171
|
+
_ entryURL: URL,
|
|
172
|
+
values: URLResourceValues,
|
|
173
|
+
fileManager: FileManager
|
|
174
|
+
) -> Int64 {
|
|
175
|
+
if values.isRegularFile == true {
|
|
176
|
+
return Int64(values.fileSize ?? 0)
|
|
177
|
+
}
|
|
178
|
+
guard
|
|
179
|
+
values.isDirectory == true,
|
|
180
|
+
let enumerator = fileManager.enumerator(
|
|
181
|
+
at: entryURL,
|
|
182
|
+
includingPropertiesForKeys: [
|
|
183
|
+
.isRegularFileKey,
|
|
184
|
+
.isSymbolicLinkKey,
|
|
185
|
+
.fileSizeKey,
|
|
186
|
+
],
|
|
187
|
+
options: [],
|
|
188
|
+
errorHandler: nil
|
|
189
|
+
)
|
|
190
|
+
else {
|
|
191
|
+
return 0
|
|
192
|
+
}
|
|
193
|
+
var size: Int64 = 0
|
|
194
|
+
for case let childURL as URL in enumerator {
|
|
195
|
+
guard
|
|
196
|
+
let childValues = try? childURL.resourceValues(
|
|
197
|
+
forKeys: [.isRegularFileKey, .isSymbolicLinkKey, .fileSizeKey]
|
|
198
|
+
),
|
|
199
|
+
childValues.isRegularFile == true,
|
|
200
|
+
childValues.isSymbolicLink != true
|
|
201
|
+
else {
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
size += Int64(childValues.fileSize ?? 0)
|
|
205
|
+
}
|
|
206
|
+
return size
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
func sweepFirmwareArtifactOrphansAtRoot(
|
|
210
|
+
_ rootURL: URL,
|
|
211
|
+
retainedSha256: Set<String>,
|
|
212
|
+
activeSha256: Set<String>,
|
|
213
|
+
openPaths: Set<String>,
|
|
214
|
+
now: Date = Date(),
|
|
215
|
+
fileManager: FileManager = .default
|
|
216
|
+
) throws -> (deletedFiles: Int, deletedBytes: Int64) {
|
|
217
|
+
var deletedFiles = 0
|
|
218
|
+
var deletedBytes: Int64 = 0
|
|
219
|
+
let resourceKeys: Set<URLResourceKey> = [
|
|
220
|
+
.isRegularFileKey,
|
|
221
|
+
.isDirectoryKey,
|
|
222
|
+
.isSymbolicLinkKey,
|
|
223
|
+
.contentModificationDateKey,
|
|
224
|
+
.fileSizeKey,
|
|
225
|
+
]
|
|
226
|
+
let entries = try fileManager.contentsOfDirectory(
|
|
227
|
+
at: rootURL,
|
|
228
|
+
includingPropertiesForKeys: Array(resourceKeys),
|
|
229
|
+
options: []
|
|
230
|
+
)
|
|
231
|
+
for entryURL in entries {
|
|
232
|
+
let name = entryURL.lastPathComponent
|
|
233
|
+
let values = try entryURL.resourceValues(forKeys: resourceKeys)
|
|
234
|
+
let isArchiveScratch = firmwareArtifactScratchNameIsValid(
|
|
235
|
+
name,
|
|
236
|
+
prefix: "archive-"
|
|
237
|
+
)
|
|
238
|
+
let isPromoteScratch = firmwareArtifactScratchNameIsValid(
|
|
239
|
+
name,
|
|
240
|
+
prefix: ".promote-"
|
|
241
|
+
)
|
|
242
|
+
let isScratchCandidate = values.isSymbolicLink != true &&
|
|
243
|
+
((isArchiveScratch && values.isDirectory == true) ||
|
|
244
|
+
(isPromoteScratch && values.isRegularFile == true))
|
|
245
|
+
if isScratchCandidate {
|
|
246
|
+
guard
|
|
247
|
+
let modifiedAt = values.contentModificationDate,
|
|
248
|
+
now.timeIntervalSince(modifiedAt) >= firmwareArtifactScratchGrace
|
|
249
|
+
else {
|
|
250
|
+
continue
|
|
251
|
+
}
|
|
252
|
+
let size = firmwareArtifactEntrySize(
|
|
253
|
+
entryURL,
|
|
254
|
+
values: values,
|
|
255
|
+
fileManager: fileManager
|
|
256
|
+
)
|
|
257
|
+
try fileManager.removeItem(at: entryURL)
|
|
258
|
+
deletedFiles += 1
|
|
259
|
+
deletedBytes += size
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
guard
|
|
264
|
+
values.isRegularFile == true,
|
|
265
|
+
values.isSymbolicLink != true,
|
|
266
|
+
name.count >= 64
|
|
267
|
+
else {
|
|
268
|
+
continue
|
|
269
|
+
}
|
|
270
|
+
let sha256 = String(name.prefix(64))
|
|
271
|
+
guard
|
|
272
|
+
sha256.range(of: "^[a-f0-9]{64}$", options: .regularExpression) != nil,
|
|
273
|
+
!retainedSha256.contains(sha256),
|
|
274
|
+
!activeSha256.contains(sha256),
|
|
275
|
+
!openPaths.contains(entryURL.path)
|
|
276
|
+
else {
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
let grace: TimeInterval
|
|
280
|
+
if name.hasSuffix(".bin") {
|
|
281
|
+
grace = firmwareArtifactFinalGrace
|
|
282
|
+
} else if name.hasSuffix(".partial") {
|
|
283
|
+
grace = firmwareArtifactPartialGrace
|
|
284
|
+
} else {
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
guard
|
|
288
|
+
let modifiedAt = values.contentModificationDate,
|
|
289
|
+
now.timeIntervalSince(modifiedAt) >= grace
|
|
290
|
+
else {
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
let size = Int64(values.fileSize ?? 0)
|
|
294
|
+
try fileManager.removeItem(at: entryURL)
|
|
295
|
+
deletedFiles += 1
|
|
296
|
+
deletedBytes += size
|
|
297
|
+
}
|
|
298
|
+
return (deletedFiles, deletedBytes)
|
|
299
|
+
}
|
|
300
|
+
|
|
4
301
|
// MARK: - Dependency-free RangeDownloader logic (OCDS §4 / §5)
|
|
5
302
|
//
|
|
6
303
|
// This file holds the DETERMINISTIC, dependency-light pieces of the range
|