@onekeyfe/react-native-range-downloader 3.0.81-alpha.7 → 3.0.81

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.
Files changed (111) hide show
  1. package/ReactNativeRangeDownloader.podspec +0 -8
  2. package/android/build.gradle +0 -1
  3. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ReactNativeRangeDownloader.kt +0 -144
  4. package/ios/RangeDownloadLogic.swift +0 -120
  5. package/ios/ReactNativeRangeDownloader.swift +6 -623
  6. package/lib/module/index.js +1 -2
  7. package/lib/module/index.js.map +1 -1
  8. package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts +1 -91
  9. package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts.map +1 -1
  10. package/lib/typescript/src/index.d.ts +0 -1
  11. package/lib/typescript/src/index.d.ts.map +1 -1
  12. package/nitrogen/generated/android/c++/JDownloadChannel.hpp +0 -3
  13. package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.cpp +0 -248
  14. package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.hpp +0 -12
  15. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/DownloadChannel.kt +1 -2
  16. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/HybridReactNativeRangeDownloaderSpec.kt +0 -49
  17. package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Bridge.cpp +0 -48
  18. package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Bridge.hpp +0 -329
  19. package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Umbrella.hpp +0 -53
  20. package/nitrogen/generated/ios/c++/HybridReactNativeRangeDownloaderSpecSwift.hpp +0 -152
  21. package/nitrogen/generated/ios/swift/DownloadChannel.swift +0 -4
  22. package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec.swift +0 -12
  23. package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec_cxx.swift +0 -221
  24. package/nitrogen/generated/shared/c++/DownloadChannel.hpp +0 -4
  25. package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.cpp +0 -12
  26. package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.hpp +0 -58
  27. package/package.json +2 -4
  28. package/src/ReactNativeRangeDownloader.nitro.ts +1 -127
  29. package/src/index.tsx +1 -3
  30. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRules.kt +0 -284
  31. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactStore.kt +0 -774
  32. package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRulesTest.kt +0 -75
  33. package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactDeadlineTest.kt +0 -26
  34. package/ios/FirmwareArchiveMinizipBridge.h +0 -33
  35. package/ios/FirmwareArchiveMinizipBridge.mm +0 -213
  36. package/ios/FirmwareArtifactStore.swift +0 -1445
  37. package/ios/FirmwareBackgroundSessionEventRouter.swift +0 -17
  38. package/nitrogen/generated/android/c++/JFirmwareArchiveExpectedEntry.hpp +0 -69
  39. package/nitrogen/generated/android/c++/JFirmwareArchiveMaterializeParams.hpp +0 -86
  40. package/nitrogen/generated/android/c++/JFirmwareArchiveMaterializeResult.hpp +0 -80
  41. package/nitrogen/generated/android/c++/JFirmwareArchiveMaterializedArtifact.hpp +0 -63
  42. package/nitrogen/generated/android/c++/JFirmwareArtifactCancelParams.hpp +0 -57
  43. package/nitrogen/generated/android/c++/JFirmwareArtifactCapabilities.hpp +0 -88
  44. package/nitrogen/generated/android/c++/JFirmwareArtifactDownloadParams.hpp +0 -98
  45. package/nitrogen/generated/android/c++/JFirmwareArtifactLease.hpp +0 -57
  46. package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseCreateParams.hpp +0 -57
  47. package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseReleaseParams.hpp +0 -61
  48. package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseRetainParams.hpp +0 -61
  49. package/nitrogen/generated/android/c++/JFirmwareArtifactReaderCloseParams.hpp +0 -57
  50. package/nitrogen/generated/android/c++/JFirmwareArtifactReaderInfo.hpp +0 -61
  51. package/nitrogen/generated/android/c++/JFirmwareArtifactReaderReadParams.hpp +0 -65
  52. package/nitrogen/generated/android/c++/JFirmwareArtifactReceipt.hpp +0 -65
  53. package/nitrogen/generated/android/c++/JFirmwareArtifactRefParams.hpp +0 -57
  54. package/nitrogen/generated/android/c++/JFirmwareArtifactSweepResult.hpp +0 -61
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveExpectedEntry.kt +0 -47
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveMaterializeParams.kt +0 -44
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveMaterializeResult.kt +0 -38
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveMaterializedArtifact.kt +0 -41
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactCancelParams.kt +0 -38
  60. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactCapabilities.kt +0 -47
  61. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactDownloadParams.kt +0 -68
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLease.kt +0 -38
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseCreateParams.kt +0 -38
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseReleaseParams.kt +0 -41
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseRetainParams.kt +0 -41
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactReaderCloseParams.kt +0 -38
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactReaderInfo.kt +0 -41
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactReaderReadParams.kt +0 -44
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactReceipt.kt +0 -44
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactRefParams.kt +0 -38
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactSweepResult.kt +0 -41
  72. package/nitrogen/generated/ios/swift/FirmwareArchiveExpectedEntry.swift +0 -69
  73. package/nitrogen/generated/ios/swift/FirmwareArchiveMaterializeParams.swift +0 -70
  74. package/nitrogen/generated/ios/swift/FirmwareArchiveMaterializeResult.swift +0 -48
  75. package/nitrogen/generated/ios/swift/FirmwareArchiveMaterializedArtifact.swift +0 -47
  76. package/nitrogen/generated/ios/swift/FirmwareArtifactCancelParams.swift +0 -36
  77. package/nitrogen/generated/ios/swift/FirmwareArtifactCapabilities.swift +0 -81
  78. package/nitrogen/generated/ios/swift/FirmwareArtifactDownloadParams.swift +0 -177
  79. package/nitrogen/generated/ios/swift/FirmwareArtifactLease.swift +0 -36
  80. package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseCreateParams.swift +0 -36
  81. package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseReleaseParams.swift +0 -47
  82. package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseRetainParams.swift +0 -47
  83. package/nitrogen/generated/ios/swift/FirmwareArtifactReaderCloseParams.swift +0 -36
  84. package/nitrogen/generated/ios/swift/FirmwareArtifactReaderInfo.swift +0 -47
  85. package/nitrogen/generated/ios/swift/FirmwareArtifactReaderReadParams.swift +0 -58
  86. package/nitrogen/generated/ios/swift/FirmwareArtifactReceipt.swift +0 -58
  87. package/nitrogen/generated/ios/swift/FirmwareArtifactRefParams.swift +0 -36
  88. package/nitrogen/generated/ios/swift/FirmwareArtifactSweepResult.swift +0 -47
  89. package/nitrogen/generated/ios/swift/Func_void_FirmwareArchiveMaterializeResult.swift +0 -47
  90. package/nitrogen/generated/ios/swift/Func_void_FirmwareArtifactLease.swift +0 -47
  91. package/nitrogen/generated/ios/swift/Func_void_FirmwareArtifactReaderInfo.swift +0 -47
  92. package/nitrogen/generated/ios/swift/Func_void_FirmwareArtifactReceipt.swift +0 -47
  93. package/nitrogen/generated/ios/swift/Func_void_FirmwareArtifactSweepResult.swift +0 -47
  94. package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift +0 -47
  95. package/nitrogen/generated/shared/c++/FirmwareArchiveExpectedEntry.hpp +0 -87
  96. package/nitrogen/generated/shared/c++/FirmwareArchiveMaterializeParams.hpp +0 -86
  97. package/nitrogen/generated/shared/c++/FirmwareArchiveMaterializeResult.hpp +0 -77
  98. package/nitrogen/generated/shared/c++/FirmwareArchiveMaterializedArtifact.hpp +0 -81
  99. package/nitrogen/generated/shared/c++/FirmwareArtifactCancelParams.hpp +0 -75
  100. package/nitrogen/generated/shared/c++/FirmwareArtifactCapabilities.hpp +0 -88
  101. package/nitrogen/generated/shared/c++/FirmwareArtifactDownloadParams.hpp +0 -116
  102. package/nitrogen/generated/shared/c++/FirmwareArtifactLease.hpp +0 -75
  103. package/nitrogen/generated/shared/c++/FirmwareArtifactLeaseCreateParams.hpp +0 -75
  104. package/nitrogen/generated/shared/c++/FirmwareArtifactLeaseReleaseParams.hpp +0 -79
  105. package/nitrogen/generated/shared/c++/FirmwareArtifactLeaseRetainParams.hpp +0 -79
  106. package/nitrogen/generated/shared/c++/FirmwareArtifactReaderCloseParams.hpp +0 -75
  107. package/nitrogen/generated/shared/c++/FirmwareArtifactReaderInfo.hpp +0 -79
  108. package/nitrogen/generated/shared/c++/FirmwareArtifactReaderReadParams.hpp +0 -83
  109. package/nitrogen/generated/shared/c++/FirmwareArtifactReceipt.hpp +0 -83
  110. package/nitrogen/generated/shared/c++/FirmwareArtifactRefParams.hpp +0 -75
  111. package/nitrogen/generated/shared/c++/FirmwareArtifactSweepResult.hpp +0 -79
@@ -1,774 +0,0 @@
1
- package com.margelo.nitro.reactnativerangedownloader
2
-
3
- import android.system.Os
4
- import com.margelo.nitro.NitroModules
5
- import com.sniconnect.SniPinnedTransport
6
- import java.io.BufferedInputStream
7
- import java.io.File
8
- import java.io.FileInputStream
9
- import java.io.IOException
10
- import java.io.RandomAccessFile
11
- import java.security.MessageDigest
12
- import java.util.UUID
13
- import java.util.concurrent.ConcurrentHashMap
14
- import java.util.concurrent.TimeUnit
15
- import java.util.zip.ZipInputStream
16
- import javax.net.ssl.SSLException
17
- import kotlin.math.ceil
18
- import okhttp3.Call
19
- import okhttp3.OkHttpClient
20
- import okhttp3.Protocol
21
- import okhttp3.Request
22
- import okhttp3.Response
23
- import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
24
-
25
- private const val DEFAULT_FIRMWARE_DOWNLOAD_DEADLINE_SECONDS = 180.0
26
- private const val MAX_FIRMWARE_DOWNLOAD_DEADLINE_SECONDS = 24.0 * 60 * 60
27
-
28
- internal fun validateFirmwareDownloadDeadlineSeconds(value: Double?): Double {
29
- val deadline = value ?: DEFAULT_FIRMWARE_DOWNLOAD_DEADLINE_SECONDS
30
- require(
31
- deadline.isFinite() &&
32
- deadline > 0 &&
33
- deadline <= MAX_FIRMWARE_DOWNLOAD_DEADLINE_SECONDS
34
- ) {
35
- "Invalid firmware download deadline"
36
- }
37
- return deadline
38
- }
39
-
40
- internal data class StoredFirmwareArtifact(
41
- val artifactRef: String,
42
- val size: Long,
43
- val sha256: String,
44
- val file: File,
45
- )
46
-
47
- internal data class StoredFirmwareArchiveEntry(
48
- val entryName: String,
49
- val artifact: StoredFirmwareArtifact,
50
- )
51
-
52
- private data class StagedFirmwareArchiveEntry(
53
- val entryName: String,
54
- val size: Long,
55
- val sha256: String,
56
- val file: File,
57
- )
58
-
59
- private data class FirmwareDownloadKey(
60
- val expectedSha256: String,
61
- )
62
-
63
- private class FirmwareDownloadLock {
64
- val monitor = Any()
65
- var references = 0
66
- }
67
-
68
- internal object FirmwareArtifactStore {
69
- const val MAX_READ_BYTES = 256 * 1024
70
-
71
- private const val MAX_ARTIFACT_BYTES = 512L * 1024 * 1024
72
- private const val FINAL_ARTIFACT_GRACE_MS = 24L * 60 * 60 * 1000
73
- private const val PARTIAL_ARTIFACT_GRACE_MS = 7L * 24 * 60 * 60 * 1000
74
- private val sha256Pattern = Regex("^[a-fA-F0-9]{64}$")
75
- private val artifactRefPattern = Regex("^fw:[a-f0-9]{64}$")
76
- private val leaseRefPattern = Regex("^fwlease:[a-f0-9-]{36}$")
77
- private val identifierPattern = Regex("^[A-Za-z0-9._:-]{1,160}$")
78
- private val downloadLocks =
79
- ConcurrentHashMap<FirmwareDownloadKey, FirmwareDownloadLock>()
80
- private val activeCalls =
81
- ConcurrentHashMap<String, MutableSet<Call>>()
82
- private val cancelledTransactions =
83
- ConcurrentHashMap.newKeySet<String>()
84
- private val activeDownloadLock = Any()
85
- private val activeDownloadCounts = mutableMapOf<String, Int>()
86
- private val leaseLock = Any()
87
- private val readerLock = Any()
88
- private val readers = mutableMapOf<String, OpenReader>()
89
- private val leases = mutableMapOf<String, LeaseState>()
90
-
91
- private data class LeaseState(
92
- val transactionId: String,
93
- val artifactRefs: MutableSet<String>,
94
- )
95
-
96
- private data class OpenReader(
97
- val file: File,
98
- val handle: RandomAccessFile,
99
- val size: Long,
100
- )
101
-
102
- private val root: File
103
- get() {
104
- val context = NitroModules.applicationContext
105
- ?: error("Application context is not available")
106
- return File(context.filesDir, "onekey-firmware-artifacts").also {
107
- check(it.exists() || it.mkdirs()) {
108
- "Firmware artifact directory cannot be created"
109
- }
110
- }
111
- }
112
-
113
- fun download(params: FirmwareArtifactDownloadParams): StoredFirmwareArtifact {
114
- val validated = validateDownloadParams(params)
115
- check(!cancelledTransactions.contains(params.transactionId)) {
116
- "ARTIFACT_CANCELLED: firmware artifact download was cancelled"
117
- }
118
- retainExpectedArtifact(
119
- leaseRef = params.leaseRef,
120
- transactionId = params.transactionId,
121
- artifactRef = "fw:${validated.expectedSha256}",
122
- )
123
- val lockKey = FirmwareDownloadKey(validated.expectedSha256)
124
- val downloadLock = downloadLocks.compute(lockKey) { _, current ->
125
- (current ?: FirmwareDownloadLock()).also {
126
- it.references += 1
127
- }
128
- } ?: error("Firmware artifact lock is unavailable")
129
- markDownloadActive(validated.expectedSha256, 1)
130
- try {
131
- return synchronized(downloadLock.monitor) {
132
- check(!cancelledTransactions.contains(params.transactionId)) {
133
- "ARTIFACT_CANCELLED: firmware artifact download was cancelled"
134
- }
135
- downloadLocked(params, validated)
136
- }
137
- } finally {
138
- markDownloadActive(validated.expectedSha256, -1)
139
- downloadLocks.compute(lockKey) { _, current ->
140
- if (current !== downloadLock) {
141
- current
142
- } else {
143
- current.references -= 1
144
- current.takeIf { it.references > 0 }
145
- }
146
- }
147
- }
148
- }
149
-
150
- fun cancelDownloads(transactionId: String) {
151
- require(identifierPattern.matches(transactionId)) {
152
- "Invalid firmware transactionId"
153
- }
154
- cancelledTransactions.add(transactionId)
155
- activeCalls[transactionId]
156
- ?.toList()
157
- ?.forEach { it.cancel() }
158
- }
159
-
160
- fun discard(artifactRef: String) {
161
- val file = resolveArtifactFile(artifactRef)
162
- synchronized(leaseLock) {
163
- require(leases.values.none { artifactRef in it.artifactRefs }) {
164
- "ARTIFACT_LEASED: firmware artifact is retained"
165
- }
166
- }
167
- synchronized(readerLock) {
168
- require(readers.none { it.value.file == file }) {
169
- "ARTIFACT_BUSY: firmware artifact has an open reader"
170
- }
171
- }
172
- if (file.exists() && !file.delete()) {
173
- error("Firmware artifact cannot be discarded")
174
- }
175
- }
176
-
177
- fun open(artifactRef: String): Pair<String, Long> {
178
- val file = resolveArtifactFile(artifactRef)
179
- val size = file.length()
180
- require(size > 0) { "Firmware artifact is empty" }
181
- require(hashFile(file) == artifactRef.removePrefix("fw:")) {
182
- "Firmware artifact SHA-256 mismatch"
183
- }
184
- val readerId = UUID.randomUUID().toString()
185
- synchronized(readerLock) {
186
- readers[readerId] = OpenReader(file, RandomAccessFile(file, "r"), size)
187
- }
188
- return readerId to size
189
- }
190
-
191
- fun read(readerId: String, offset: Long, length: Int): ByteArray {
192
- require(offset >= 0 && length in 1..MAX_READ_BYTES) {
193
- "Invalid firmware artifact read"
194
- }
195
- return synchronized(readerLock) {
196
- val reader = readers[readerId] ?: error("Firmware artifact reader is invalid")
197
- require(offset <= reader.size - length) {
198
- "Firmware artifact read is out of bounds"
199
- }
200
- val data = ByteArray(length)
201
- reader.handle.seek(offset)
202
- reader.handle.readFully(data)
203
- data
204
- }
205
- }
206
-
207
- fun close(readerId: String) {
208
- synchronized(readerLock) {
209
- readers.remove(readerId)?.handle?.close()
210
- }
211
- }
212
-
213
- fun materializeArchive(
214
- leaseRef: String,
215
- artifactRef: String,
216
- expectedEntries: Array<FirmwareArchiveExpectedEntry>,
217
- ): List<StoredFirmwareArchiveEntry> {
218
- requireLease(leaseRef)
219
- val archiveFile = resolveArtifactFile(artifactRef)
220
- val requirements = FirmwareArchiveRules.validateRequirements(expectedEntries)
221
- val centralEntries = FirmwareArchiveRules.validateCentralDirectory(
222
- archiveFile,
223
- requirements,
224
- )
225
- val requirementsByName = requirements.associateBy { it.entryName }
226
- val centralNames = centralEntries.mapTo(mutableSetOf()) { it.name }
227
- val scratchDir = File(root, "archive-${UUID.randomUUID()}")
228
- check(scratchDir.mkdirs()) { "Firmware archive scratch directory cannot be created" }
229
- try {
230
- val staged = mutableListOf<StagedFirmwareArchiveEntry>()
231
- val entryNames = mutableSetOf<String>()
232
- ZipInputStream(BufferedInputStream(FileInputStream(archiveFile))).use { zip ->
233
- while (true) {
234
- val zipEntry = zip.nextEntry ?: break
235
- require(!zipEntry.isDirectory) {
236
- "Firmware archive contains an unexpected directory"
237
- }
238
- val requirement = requirementsByName[zipEntry.name]
239
- ?: error("Firmware archive contains an unexpected entry")
240
- require(
241
- centralNames.contains(zipEntry.name) &&
242
- entryNames.add(zipEntry.name)
243
- ) {
244
- "Firmware archive contains a duplicate or mismatched entry"
245
- }
246
- val expectedSize = requirement.expectedSize.toLong()
247
- val expectedSha256 = requirement.expectedSha256.lowercase()
248
- val scratchFile = File(scratchDir, "${staged.size}.entry")
249
- val digest = MessageDigest.getInstance("SHA-256")
250
- var entrySize = 0L
251
- RandomAccessFile(scratchFile, "rw").use { output ->
252
- val buffer = ByteArray(64 * 1024)
253
- while (true) {
254
- val count = zip.read(buffer)
255
- if (count < 0) break
256
- if (count == 0) continue
257
- entrySize = Math.addExact(entrySize, count.toLong())
258
- require(entrySize <= expectedSize) {
259
- "Firmware archive entry exceeds its expected size"
260
- }
261
- output.write(buffer, 0, count)
262
- digest.update(buffer, 0, count)
263
- }
264
- output.fd.sync()
265
- }
266
- val sha256 = digest.digest().toHex()
267
- require(entrySize == expectedSize && sha256 == expectedSha256) {
268
- "Firmware archive entry integrity mismatch"
269
- }
270
- staged += StagedFirmwareArchiveEntry(
271
- entryName = requirement.entryName,
272
- size = entrySize,
273
- sha256 = sha256,
274
- file = scratchFile,
275
- )
276
- zip.closeEntry()
277
- }
278
- }
279
- require(entryNames == centralNames) {
280
- "Firmware archive has missing or extra entries"
281
- }
282
- return staged.map { entry ->
283
- val destination = artifactFile(entry.sha256)
284
- val stored = validateStoredArtifactOrNull(
285
- destination,
286
- entry.size,
287
- entry.sha256,
288
- ) ?: run {
289
- promoteAtomically(entry.file, destination)
290
- StoredFirmwareArtifact(
291
- "fw:${entry.sha256}",
292
- entry.size,
293
- entry.sha256,
294
- destination,
295
- )
296
- }
297
- retainExpectedArtifact(
298
- leaseRef = leaseRef,
299
- transactionId = null,
300
- artifactRef = stored.artifactRef,
301
- )
302
- StoredFirmwareArchiveEntry(entry.entryName, stored)
303
- }
304
- } finally {
305
- scratchDir.deleteRecursively()
306
- }
307
- }
308
-
309
- private data class ValidatedDownload(
310
- val expectedSize: Long,
311
- val maxBytes: Long,
312
- val expectedSha256: String,
313
- val hostname: String,
314
- val overallDeadlineSeconds: Double,
315
- )
316
-
317
- private fun validateDownloadParams(
318
- params: FirmwareArtifactDownloadParams,
319
- ): ValidatedDownload {
320
- require(
321
- params.taskId.isNotEmpty() &&
322
- params.taskId.length <= 100 &&
323
- params.taskId.all { it.isLetterOrDigit() || it in "._-" }
324
- ) {
325
- "Invalid firmware taskId"
326
- }
327
- require(identifierPattern.matches(params.transactionId)) {
328
- "Invalid firmware transactionId"
329
- }
330
- require(identifierPattern.matches(params.artifactId)) {
331
- "Invalid firmware artifactId"
332
- }
333
- require(leaseRefPattern.matches(params.leaseRef)) {
334
- "Invalid firmware leaseRef"
335
- }
336
- val url = params.url.toHttpUrlOrNull()
337
- ?: throw IllegalArgumentException("Invalid firmware URL")
338
- require(
339
- url.isHttps &&
340
- url.port == 443 &&
341
- url.username.isEmpty() &&
342
- url.password.isEmpty() &&
343
- url.fragment == null
344
- ) {
345
- "Firmware URL must use HTTPS port 443"
346
- }
347
- val expectedSize = params.expectedSize.toExactPositiveLong("expectedSize")
348
- val maxBytes = params.maxBytes.toExactPositiveLong("maxBytes")
349
- require(maxBytes == expectedSize && maxBytes <= MAX_ARTIFACT_BYTES) {
350
- "Invalid firmware maxBytes"
351
- }
352
- require(sha256Pattern.matches(params.expectedSha256)) {
353
- "Invalid firmware artifact SHA-256"
354
- }
355
- val overallDeadlineSeconds =
356
- validateFirmwareDownloadDeadlineSeconds(params.overallDeadlineSeconds)
357
- require(params.routeType == "domain" || params.routeType == "pinnedIp") {
358
- "Invalid firmware route type"
359
- }
360
- if (params.routeType == "pinnedIp") {
361
- require(!params.resolvedIp.isNullOrEmpty()) {
362
- "Pinned route requires resolvedIp"
363
- }
364
- } else {
365
- require(params.resolvedIp == null) {
366
- "Domain route must not include resolvedIp"
367
- }
368
- }
369
- return ValidatedDownload(
370
- expectedSize = expectedSize,
371
- maxBytes = maxBytes,
372
- expectedSha256 = params.expectedSha256.lowercase(),
373
- hostname = url.host,
374
- overallDeadlineSeconds = overallDeadlineSeconds,
375
- )
376
- }
377
-
378
- private fun downloadLocked(
379
- params: FirmwareArtifactDownloadParams,
380
- validated: ValidatedDownload,
381
- ): StoredFirmwareArtifact {
382
- val finalFile = artifactFile(validated.expectedSha256)
383
- validateStoredArtifactOrNull(
384
- finalFile,
385
- validated.expectedSize,
386
- validated.expectedSha256,
387
- )?.let { return it }
388
-
389
- val partialFile = File(
390
- root,
391
- "${validated.expectedSha256}.${params.taskId}.partial",
392
- )
393
- if (partialFile.length() > validated.expectedSize) {
394
- check(partialFile.delete()) { "Invalid firmware partial cannot be removed" }
395
- }
396
- if (partialFile.length() == validated.expectedSize) {
397
- validateStoredArtifactOrNull(
398
- partialFile,
399
- validated.expectedSize,
400
- validated.expectedSha256,
401
- )?.let {
402
- promoteAtomically(partialFile, finalFile)
403
- return StoredFirmwareArtifact(
404
- it.artifactRef,
405
- it.size,
406
- it.sha256,
407
- finalFile,
408
- )
409
- }
410
- check(partialFile.delete()) { "Invalid firmware partial cannot be removed" }
411
- }
412
-
413
- val resumeOffset = partialFile.length()
414
- val requestBuilder = Request.Builder()
415
- .url(params.url)
416
- .header("Accept-Encoding", "identity")
417
- if (resumeOffset > 0) {
418
- requestBuilder.header("Range", "bytes=$resumeOffset-")
419
- }
420
-
421
- val client = if (params.routeType == "pinnedIp") {
422
- SniPinnedTransport.createClient(
423
- ip = checkNotNull(params.resolvedIp),
424
- hostname = validated.hostname,
425
- )
426
- } else {
427
- OkHttpClient.Builder()
428
- .protocols(listOf(Protocol.HTTP_1_1))
429
- .followRedirects(false)
430
- .followSslRedirects(false)
431
- .build()
432
- }
433
- val call = client.newCall(requestBuilder.build())
434
- call.timeout().timeout(
435
- ceil(validated.overallDeadlineSeconds * 1000).toLong(),
436
- TimeUnit.MILLISECONDS,
437
- )
438
-
439
- registerCall(params.transactionId, call)
440
- try {
441
- call.execute().use {
442
- writeResponseToPartial(
443
- it,
444
- partialFile,
445
- resumeOffset,
446
- validated.expectedSize,
447
- validated.maxBytes,
448
- )
449
- }
450
- } catch (error: IOException) {
451
- if (
452
- call.isCanceled() ||
453
- cancelledTransactions.contains(params.transactionId)
454
- ) {
455
- throw IllegalStateException(
456
- "ARTIFACT_CANCELLED: firmware artifact download was cancelled",
457
- error,
458
- )
459
- }
460
- if (generateSequence<Throwable>(error) { it.cause }.any { it is SSLException }) {
461
- throw IllegalStateException(
462
- "ARTIFACT_TLS_FAILED: firmware TLS validation failed",
463
- error,
464
- )
465
- }
466
- throw IllegalStateException(
467
- "ARTIFACT_NETWORK_FAILED: firmware request failed",
468
- error,
469
- )
470
- } finally {
471
- unregisterCall(params.transactionId, call)
472
- }
473
- check(!cancelledTransactions.contains(params.transactionId)) {
474
- "ARTIFACT_CANCELLED: firmware artifact download was cancelled"
475
- }
476
-
477
- val artifact = try {
478
- validateStoredArtifact(
479
- partialFile,
480
- validated.expectedSize,
481
- validated.expectedSha256,
482
- )
483
- } catch (error: Throwable) {
484
- partialFile.delete()
485
- throw error
486
- }
487
- promoteAtomically(partialFile, finalFile)
488
- return StoredFirmwareArtifact(
489
- artifact.artifactRef,
490
- artifact.size,
491
- artifact.sha256,
492
- finalFile,
493
- )
494
- }
495
-
496
- private fun writeResponseToPartial(
497
- response: Response,
498
- partialFile: File,
499
- resumeOffset: Long,
500
- expectedSize: Long,
501
- maxBytes: Long,
502
- ) {
503
- require(response.code == 200 || response.code == 206) {
504
- "ARTIFACT_HTTP_${response.code}: firmware request failed"
505
- }
506
- val append = resumeOffset > 0 && response.code == 206
507
- if (response.code == 206) {
508
- require(
509
- validateContentRange(
510
- response.header("Content-Range"),
511
- if (append) resumeOffset else 0,
512
- expectedSize,
513
- )
514
- ) {
515
- "ARTIFACT_PROTOCOL_INVALID: firmware resume Content-Range is invalid"
516
- }
517
- }
518
- val body = response.body ?: error("Firmware response has no body")
519
- RandomAccessFile(partialFile, "rw").use { output ->
520
- if (append) {
521
- output.seek(resumeOffset)
522
- } else {
523
- output.setLength(0)
524
- }
525
- var written = if (append) resumeOffset else 0L
526
- val source = body.source()
527
- val buffer = ByteArray(64 * 1024)
528
- while (true) {
529
- val count = source.read(buffer)
530
- if (count < 0) break
531
- if (count == 0) continue
532
- written += count
533
- require(written <= maxBytes) {
534
- "ARTIFACT_PROTOCOL_INVALID: firmware artifact exceeds maxBytes"
535
- }
536
- output.write(buffer, 0, count)
537
- }
538
- output.fd.sync()
539
- }
540
- }
541
-
542
- private fun validateContentRange(
543
- value: String?,
544
- expectedStart: Long,
545
- expectedTotal: Long,
546
- ): Boolean {
547
- val match = value
548
- ?.lowercase()
549
- ?.let { Regex("^bytes ([0-9]+)-([0-9]+)/([0-9]+)$").matchEntire(it) }
550
- ?: return false
551
- val start = match.groupValues[1].toLongOrNull() ?: return false
552
- val end = match.groupValues[2].toLongOrNull() ?: return false
553
- val total = match.groupValues[3].toLongOrNull() ?: return false
554
- return start == expectedStart &&
555
- end >= start &&
556
- end < total &&
557
- total == expectedTotal
558
- }
559
-
560
- private fun validateStoredArtifactOrNull(
561
- file: File,
562
- expectedSize: Long,
563
- expectedSha256: String,
564
- ): StoredFirmwareArtifact? = try {
565
- validateStoredArtifact(file, expectedSize, expectedSha256)
566
- } catch (_: Throwable) {
567
- null
568
- }
569
-
570
- private fun validateStoredArtifact(
571
- file: File,
572
- expectedSize: Long,
573
- expectedSha256: String,
574
- ): StoredFirmwareArtifact {
575
- require(file.isFile && file.length() == expectedSize) {
576
- "ARTIFACT_INTEGRITY_FAILED: firmware artifact size mismatch"
577
- }
578
- val sha256 = hashFile(file)
579
- require(sha256 == expectedSha256) {
580
- "ARTIFACT_INTEGRITY_FAILED: firmware artifact SHA-256 mismatch"
581
- }
582
- return StoredFirmwareArtifact("fw:$sha256", expectedSize, sha256, file)
583
- }
584
-
585
- private fun resolveArtifactFile(artifactRef: String): File {
586
- require(artifactRefPattern.matches(artifactRef)) {
587
- "Invalid firmware artifactRef"
588
- }
589
- val file = artifactFile(artifactRef.removePrefix("fw:"))
590
- require(file.isFile) { "Firmware artifact not found" }
591
- return file
592
- }
593
-
594
- private fun artifactFile(sha256: String): File = File(root, "$sha256.bin")
595
-
596
- fun createLease(transactionId: String): String {
597
- require(identifierPattern.matches(transactionId)) {
598
- "Invalid firmware transactionId"
599
- }
600
- return synchronized(leaseLock) {
601
- require(leases.size < 32) {
602
- "Too many firmware artifact leases"
603
- }
604
- val leaseRef = "fwlease:${UUID.randomUUID()}"
605
- leases[leaseRef] = LeaseState(transactionId, mutableSetOf())
606
- leaseRef
607
- }
608
- }
609
-
610
- fun retain(leaseRef: String, artifactRef: String) {
611
- resolveArtifactFile(artifactRef)
612
- retainExpectedArtifact(
613
- leaseRef = leaseRef,
614
- transactionId = null,
615
- artifactRef = artifactRef,
616
- )
617
- }
618
-
619
- fun releaseLease(leaseRef: String, disposition: String) {
620
- require(
621
- disposition == "completed" ||
622
- disposition == "safeCancelled" ||
623
- disposition == "safeAbandoned"
624
- ) {
625
- "Invalid firmware lease disposition"
626
- }
627
- val transactionId = synchronized(leaseLock) {
628
- val removed = leases.remove(validateLeaseRef(leaseRef))
629
- require(removed != null) {
630
- "Firmware artifact lease is unavailable"
631
- }
632
- removed.transactionId
633
- }
634
- cancelledTransactions.remove(transactionId)
635
- }
636
-
637
- fun sweepOrphans(): Pair<Int, Long> {
638
- val retainedSha256 = synchronized(leaseLock) {
639
- leases.values
640
- .flatMap { it.artifactRefs }
641
- .mapTo(mutableSetOf()) { it.removePrefix("fw:") }
642
- }
643
- val activeSha256 = synchronized(activeDownloadLock) {
644
- activeDownloadCounts.filterValues { it > 0 }.keys.toSet()
645
- }
646
- val openFiles = synchronized(readerLock) {
647
- readers.values.mapTo(mutableSetOf()) { it.file.absolutePath }
648
- }
649
- val now = System.currentTimeMillis()
650
- var deletedFiles = 0
651
- var deletedBytes = 0L
652
- root.listFiles()?.forEach { file ->
653
- if (!file.isFile) return@forEach
654
- val sha256 = file.name.take(64)
655
- if (
656
- !sha256Pattern.matches(sha256) ||
657
- sha256 in retainedSha256 ||
658
- sha256 in activeSha256 ||
659
- file.absolutePath in openFiles
660
- ) {
661
- return@forEach
662
- }
663
- val grace = if (file.name.endsWith(".bin")) {
664
- FINAL_ARTIFACT_GRACE_MS
665
- } else if (file.name.endsWith(".partial")) {
666
- PARTIAL_ARTIFACT_GRACE_MS
667
- } else {
668
- return@forEach
669
- }
670
- if (now - file.lastModified() < grace) return@forEach
671
- val size = file.length()
672
- if (file.delete()) {
673
- deletedFiles += 1
674
- deletedBytes += size
675
- }
676
- }
677
- return deletedFiles to deletedBytes
678
- }
679
-
680
- private fun requireLease(leaseRef: String) {
681
- synchronized(leaseLock) {
682
- require(leases.containsKey(validateLeaseRef(leaseRef))) {
683
- "Firmware artifact lease is unavailable"
684
- }
685
- }
686
- }
687
-
688
- private fun retainExpectedArtifact(
689
- leaseRef: String,
690
- transactionId: String?,
691
- artifactRef: String,
692
- ) {
693
- require(artifactRefPattern.matches(artifactRef)) {
694
- "Invalid firmware artifactRef"
695
- }
696
- synchronized(leaseLock) {
697
- val lease = leases[validateLeaseRef(leaseRef)]
698
- ?: error("Firmware artifact lease is unavailable")
699
- if (transactionId != null) {
700
- require(lease.transactionId == transactionId) {
701
- "Firmware artifact lease transaction mismatch"
702
- }
703
- }
704
- lease.artifactRefs.add(artifactRef)
705
- }
706
- }
707
-
708
- private fun validateLeaseRef(leaseRef: String): String {
709
- require(leaseRefPattern.matches(leaseRef)) {
710
- "Invalid firmware leaseRef"
711
- }
712
- return leaseRef
713
- }
714
-
715
- private fun markDownloadActive(sha256: String, delta: Int) {
716
- synchronized(activeDownloadLock) {
717
- val count = (activeDownloadCounts[sha256] ?: 0) + delta
718
- if (count <= 0) {
719
- activeDownloadCounts.remove(sha256)
720
- } else {
721
- activeDownloadCounts[sha256] = count
722
- }
723
- }
724
- }
725
-
726
- private fun hashFile(file: File): String {
727
- val digest = MessageDigest.getInstance("SHA-256")
728
- BufferedInputStream(FileInputStream(file)).use { input ->
729
- val buffer = ByteArray(MAX_READ_BYTES)
730
- while (true) {
731
- val count = input.read(buffer)
732
- if (count < 0) break
733
- if (count > 0) digest.update(buffer, 0, count)
734
- }
735
- }
736
- return digest.digest().toHex()
737
- }
738
-
739
- private fun promoteAtomically(source: File, destination: File) {
740
- destination.parentFile?.mkdirs()
741
- Os.rename(source.absolutePath, destination.absolutePath)
742
- }
743
-
744
- private fun Double.toExactPositiveLong(label: String): Long {
745
- require(isFinite() && this > 0 && this <= Long.MAX_VALUE.toDouble()) {
746
- "Invalid firmware $label"
747
- }
748
- val converted = toLong()
749
- require(converted.toDouble() == this) { "Invalid firmware $label" }
750
- return converted
751
- }
752
-
753
- private fun ByteArray.toHex(): String = joinToString("") {
754
- "%02x".format(it)
755
- }
756
-
757
- private fun registerCall(transactionId: String, call: Call) {
758
- val calls = activeCalls.computeIfAbsent(transactionId) {
759
- ConcurrentHashMap.newKeySet()
760
- }
761
- calls.add(call)
762
- if (cancelledTransactions.contains(transactionId)) {
763
- call.cancel()
764
- }
765
- }
766
-
767
- private fun unregisterCall(transactionId: String, call: Call) {
768
- val calls = activeCalls[transactionId] ?: return
769
- calls.remove(call)
770
- if (calls.isEmpty()) {
771
- activeCalls.remove(transactionId, calls)
772
- }
773
- }
774
- }