@onekeyfe/react-native-range-downloader 3.0.81-alpha.1 → 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
package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactStore.kt
CHANGED
|
@@ -6,7 +6,6 @@ import com.sniconnect.SniPinnedTransport
|
|
|
6
6
|
import java.io.BufferedInputStream
|
|
7
7
|
import java.io.File
|
|
8
8
|
import java.io.FileInputStream
|
|
9
|
-
import java.io.FileOutputStream
|
|
10
9
|
import java.io.IOException
|
|
11
10
|
import java.io.RandomAccessFile
|
|
12
11
|
import java.security.MessageDigest
|
|
@@ -14,14 +13,29 @@ import java.util.UUID
|
|
|
14
13
|
import java.util.concurrent.ConcurrentHashMap
|
|
15
14
|
import java.util.concurrent.TimeUnit
|
|
16
15
|
import java.util.zip.ZipInputStream
|
|
16
|
+
import javax.net.ssl.SSLException
|
|
17
|
+
import kotlin.math.ceil
|
|
17
18
|
import okhttp3.Call
|
|
18
19
|
import okhttp3.OkHttpClient
|
|
19
20
|
import okhttp3.Protocol
|
|
20
21
|
import okhttp3.Request
|
|
21
22
|
import okhttp3.Response
|
|
22
23
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
}
|
|
25
39
|
|
|
26
40
|
internal data class StoredFirmwareArtifact(
|
|
27
41
|
val artifactRef: String,
|
|
@@ -44,22 +58,25 @@ private data class StagedFirmwareArchiveEntry(
|
|
|
44
58
|
|
|
45
59
|
private data class FirmwareDownloadKey(
|
|
46
60
|
val transactionId: String,
|
|
47
|
-
val
|
|
61
|
+
val taskId: String,
|
|
62
|
+
val downloadToken: String,
|
|
48
63
|
)
|
|
49
64
|
|
|
65
|
+
private class FirmwareDownloadLock {
|
|
66
|
+
val monitor = Any()
|
|
67
|
+
var references = 0
|
|
68
|
+
}
|
|
69
|
+
|
|
50
70
|
internal object FirmwareArtifactStore {
|
|
51
71
|
const val MAX_READ_BYTES = 256 * 1024
|
|
52
72
|
|
|
53
73
|
private const val MAX_ARTIFACT_BYTES = 512L * 1024 * 1024
|
|
54
|
-
private const val MAX_LEASE_METADATA_BYTES = 1024L * 1024
|
|
55
|
-
private const val MAX_TOTAL_LEASE_REFS = 8192
|
|
56
|
-
private const val FINAL_ARTIFACT_GRACE_MS = 24L * 60 * 60 * 1000
|
|
57
|
-
private const val PARTIAL_ARTIFACT_GRACE_MS = 7L * 24 * 60 * 60 * 1000
|
|
58
74
|
private val sha256Pattern = Regex("^[a-fA-F0-9]{64}$")
|
|
59
75
|
private val artifactRefPattern = Regex("^fw:[a-f0-9]{64}$")
|
|
60
76
|
private val leaseRefPattern = Regex("^fwlease:[a-f0-9-]{36}$")
|
|
61
77
|
private val identifierPattern = Regex("^[A-Za-z0-9._:-]{1,160}$")
|
|
62
|
-
private val downloadLocks =
|
|
78
|
+
private val downloadLocks =
|
|
79
|
+
ConcurrentHashMap<FirmwareDownloadKey, FirmwareDownloadLock>()
|
|
63
80
|
private val activeCalls =
|
|
64
81
|
ConcurrentHashMap<String, MutableSet<Call>>()
|
|
65
82
|
private val cancelledTransactions =
|
|
@@ -69,6 +86,7 @@ internal object FirmwareArtifactStore {
|
|
|
69
86
|
private val leaseLock = Any()
|
|
70
87
|
private val readerLock = Any()
|
|
71
88
|
private val readers = mutableMapOf<String, OpenReader>()
|
|
89
|
+
private val leases = mutableMapOf<String, LeaseState>()
|
|
72
90
|
|
|
73
91
|
private data class LeaseState(
|
|
74
92
|
val transactionId: String,
|
|
@@ -97,26 +115,47 @@ internal object FirmwareArtifactStore {
|
|
|
97
115
|
check(!cancelledTransactions.contains(params.transactionId)) {
|
|
98
116
|
"ARTIFACT_CANCELLED: firmware artifact download was cancelled"
|
|
99
117
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
validated.expectedSha256?.let {
|
|
119
|
+
retainExpectedArtifact(
|
|
120
|
+
leaseRef = params.leaseRef,
|
|
121
|
+
transactionId = params.transactionId,
|
|
122
|
+
artifactRef = "fw:$it",
|
|
123
|
+
)
|
|
124
|
+
} ?: requireLeaseTransaction(params.leaseRef, params.transactionId)
|
|
105
125
|
val lockKey = FirmwareDownloadKey(
|
|
106
126
|
params.transactionId,
|
|
107
|
-
|
|
127
|
+
params.taskId,
|
|
128
|
+
validated.downloadToken,
|
|
108
129
|
)
|
|
109
|
-
val
|
|
110
|
-
|
|
130
|
+
val downloadLock = downloadLocks.compute(lockKey) { _, current ->
|
|
131
|
+
(current ?: FirmwareDownloadLock()).also {
|
|
132
|
+
it.references += 1
|
|
133
|
+
}
|
|
134
|
+
} ?: error("Firmware artifact lock is unavailable")
|
|
135
|
+
markDownloadActive(validated.downloadToken, 1)
|
|
111
136
|
try {
|
|
112
|
-
|
|
137
|
+
val artifact = synchronized(downloadLock.monitor) {
|
|
113
138
|
check(!cancelledTransactions.contains(params.transactionId)) {
|
|
114
139
|
"ARTIFACT_CANCELLED: firmware artifact download was cancelled"
|
|
115
140
|
}
|
|
116
141
|
downloadLocked(params, validated)
|
|
117
142
|
}
|
|
143
|
+
retainExpectedArtifact(
|
|
144
|
+
leaseRef = params.leaseRef,
|
|
145
|
+
transactionId = params.transactionId,
|
|
146
|
+
artifactRef = artifact.artifactRef,
|
|
147
|
+
)
|
|
148
|
+
return artifact
|
|
118
149
|
} finally {
|
|
119
|
-
markDownloadActive(validated.
|
|
150
|
+
markDownloadActive(validated.downloadToken, -1)
|
|
151
|
+
downloadLocks.compute(lockKey) { _, current ->
|
|
152
|
+
if (current !== downloadLock) {
|
|
153
|
+
current
|
|
154
|
+
} else {
|
|
155
|
+
current.references -= 1
|
|
156
|
+
current.takeIf { it.references > 0 }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
120
159
|
}
|
|
121
160
|
}
|
|
122
161
|
|
|
@@ -133,7 +172,7 @@ internal object FirmwareArtifactStore {
|
|
|
133
172
|
fun discard(artifactRef: String) {
|
|
134
173
|
val file = resolveArtifactFile(artifactRef)
|
|
135
174
|
synchronized(leaseLock) {
|
|
136
|
-
require(
|
|
175
|
+
require(leases.values.none { artifactRef in it.artifactRefs }) {
|
|
137
176
|
"ARTIFACT_LEASED: firmware artifact is retained"
|
|
138
177
|
}
|
|
139
178
|
}
|
|
@@ -186,16 +225,19 @@ internal object FirmwareArtifactStore {
|
|
|
186
225
|
fun materializeArchive(
|
|
187
226
|
leaseRef: String,
|
|
188
227
|
artifactRef: String,
|
|
189
|
-
expectedEntries: Array<FirmwareArchiveExpectedEntry
|
|
228
|
+
expectedEntries: Array<FirmwareArchiveExpectedEntry>?,
|
|
190
229
|
): List<StoredFirmwareArchiveEntry> {
|
|
191
230
|
requireLease(leaseRef)
|
|
192
231
|
val archiveFile = resolveArtifactFile(artifactRef)
|
|
193
|
-
val requirements =
|
|
232
|
+
val requirements = expectedEntries?.let {
|
|
233
|
+
FirmwareArchiveRules.validateRequirements(it)
|
|
234
|
+
}
|
|
194
235
|
val centralEntries = FirmwareArchiveRules.validateCentralDirectory(
|
|
195
236
|
archiveFile,
|
|
196
237
|
requirements,
|
|
197
238
|
)
|
|
198
|
-
val requirementsByName = requirements
|
|
239
|
+
val requirementsByName = requirements?.associateBy { it.entryName }.orEmpty()
|
|
240
|
+
val centralEntriesByName = centralEntries.associateBy { it.name }
|
|
199
241
|
val centralNames = centralEntries.mapTo(mutableSetOf()) { it.name }
|
|
200
242
|
val scratchDir = File(root, "archive-${UUID.randomUUID()}")
|
|
201
243
|
check(scratchDir.mkdirs()) { "Firmware archive scratch directory cannot be created" }
|
|
@@ -208,16 +250,18 @@ internal object FirmwareArtifactStore {
|
|
|
208
250
|
require(!zipEntry.isDirectory) {
|
|
209
251
|
"Firmware archive contains an unexpected directory"
|
|
210
252
|
}
|
|
211
|
-
val
|
|
253
|
+
val centralEntry = centralEntriesByName[zipEntry.name]
|
|
212
254
|
?: error("Firmware archive contains an unexpected entry")
|
|
255
|
+
val requirement = requirementsByName[zipEntry.name]
|
|
213
256
|
require(
|
|
214
257
|
centralNames.contains(zipEntry.name) &&
|
|
215
258
|
entryNames.add(zipEntry.name)
|
|
216
259
|
) {
|
|
217
260
|
"Firmware archive contains a duplicate or mismatched entry"
|
|
218
261
|
}
|
|
219
|
-
val expectedSize = requirement
|
|
220
|
-
|
|
262
|
+
val expectedSize = requirement?.expectedSize?.toLong()
|
|
263
|
+
?: centralEntry.uncompressedSize
|
|
264
|
+
val expectedSha256 = requirement?.expectedSha256?.lowercase()
|
|
221
265
|
val scratchFile = File(scratchDir, "${staged.size}.entry")
|
|
222
266
|
val digest = MessageDigest.getInstance("SHA-256")
|
|
223
267
|
var entrySize = 0L
|
|
@@ -237,11 +281,14 @@ internal object FirmwareArtifactStore {
|
|
|
237
281
|
output.fd.sync()
|
|
238
282
|
}
|
|
239
283
|
val sha256 = digest.digest().toHex()
|
|
240
|
-
require(
|
|
284
|
+
require(
|
|
285
|
+
entrySize == expectedSize &&
|
|
286
|
+
(expectedSha256 == null || sha256 == expectedSha256)
|
|
287
|
+
) {
|
|
241
288
|
"Firmware archive entry integrity mismatch"
|
|
242
289
|
}
|
|
243
290
|
staged += StagedFirmwareArchiveEntry(
|
|
244
|
-
entryName =
|
|
291
|
+
entryName = zipEntry.name,
|
|
245
292
|
size = entrySize,
|
|
246
293
|
sha256 = sha256,
|
|
247
294
|
file = scratchFile,
|
|
@@ -280,10 +327,12 @@ internal object FirmwareArtifactStore {
|
|
|
280
327
|
}
|
|
281
328
|
|
|
282
329
|
private data class ValidatedDownload(
|
|
283
|
-
val expectedSize: Long
|
|
330
|
+
val expectedSize: Long?,
|
|
284
331
|
val maxBytes: Long,
|
|
285
|
-
val expectedSha256: String
|
|
332
|
+
val expectedSha256: String?,
|
|
333
|
+
val downloadToken: String,
|
|
286
334
|
val hostname: String,
|
|
335
|
+
val overallDeadlineSeconds: Double,
|
|
287
336
|
)
|
|
288
337
|
|
|
289
338
|
private fun validateDownloadParams(
|
|
@@ -316,14 +365,20 @@ internal object FirmwareArtifactStore {
|
|
|
316
365
|
) {
|
|
317
366
|
"Firmware URL must use HTTPS port 443"
|
|
318
367
|
}
|
|
319
|
-
val expectedSize = params.expectedSize
|
|
368
|
+
val expectedSize = params.expectedSize?.toExactPositiveLong("expectedSize")
|
|
320
369
|
val maxBytes = params.maxBytes.toExactPositiveLong("maxBytes")
|
|
321
|
-
require(
|
|
370
|
+
require(
|
|
371
|
+
maxBytes <= MAX_ARTIFACT_BYTES &&
|
|
372
|
+
(expectedSize == null || expectedSize <= maxBytes)
|
|
373
|
+
) {
|
|
322
374
|
"Invalid firmware maxBytes"
|
|
323
375
|
}
|
|
324
|
-
|
|
376
|
+
val expectedSha256 = params.expectedSha256?.lowercase()
|
|
377
|
+
require(expectedSha256 == null || sha256Pattern.matches(expectedSha256)) {
|
|
325
378
|
"Invalid firmware artifact SHA-256"
|
|
326
379
|
}
|
|
380
|
+
val overallDeadlineSeconds =
|
|
381
|
+
validateFirmwareDownloadDeadlineSeconds(params.overallDeadlineSeconds)
|
|
327
382
|
require(params.routeType == "domain" || params.routeType == "pinnedIp") {
|
|
328
383
|
"Invalid firmware route type"
|
|
329
384
|
}
|
|
@@ -339,8 +394,10 @@ internal object FirmwareArtifactStore {
|
|
|
339
394
|
return ValidatedDownload(
|
|
340
395
|
expectedSize = expectedSize,
|
|
341
396
|
maxBytes = maxBytes,
|
|
342
|
-
expectedSha256 =
|
|
397
|
+
expectedSha256 = expectedSha256,
|
|
398
|
+
downloadToken = expectedSha256 ?: sha256(params.url),
|
|
343
399
|
hostname = url.host,
|
|
400
|
+
overallDeadlineSeconds = overallDeadlineSeconds,
|
|
344
401
|
)
|
|
345
402
|
}
|
|
346
403
|
|
|
@@ -348,26 +405,37 @@ internal object FirmwareArtifactStore {
|
|
|
348
405
|
params: FirmwareArtifactDownloadParams,
|
|
349
406
|
validated: ValidatedDownload,
|
|
350
407
|
): StoredFirmwareArtifact {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
408
|
+
validated.expectedSha256?.let { expectedSha256 ->
|
|
409
|
+
val finalFile = artifactFile(expectedSha256)
|
|
410
|
+
validateDownloadedArtifactOrNull(
|
|
411
|
+
finalFile,
|
|
412
|
+
validated.expectedSize,
|
|
413
|
+
expectedSha256,
|
|
414
|
+
validated.maxBytes,
|
|
415
|
+
)?.let { return it }
|
|
416
|
+
}
|
|
357
417
|
|
|
418
|
+
val transactionToken = sha256(params.transactionId).take(16)
|
|
358
419
|
val partialFile = File(
|
|
359
420
|
root,
|
|
360
|
-
"${validated.
|
|
421
|
+
"${validated.downloadToken}.${params.taskId}.$transactionToken.partial",
|
|
361
422
|
)
|
|
362
|
-
if (partialFile.length() >
|
|
423
|
+
if (validated.expectedSha256 == null && partialFile.length() > 0) {
|
|
424
|
+
check(partialFile.delete()) { "Unverified firmware partial cannot be removed" }
|
|
425
|
+
} else if (partialFile.length() > validated.maxBytes) {
|
|
363
426
|
check(partialFile.delete()) { "Invalid firmware partial cannot be removed" }
|
|
364
427
|
}
|
|
365
|
-
if (
|
|
366
|
-
|
|
428
|
+
if (
|
|
429
|
+
validated.expectedSize != null &&
|
|
430
|
+
partialFile.length() == validated.expectedSize
|
|
431
|
+
) {
|
|
432
|
+
validateDownloadedArtifactOrNull(
|
|
367
433
|
partialFile,
|
|
368
434
|
validated.expectedSize,
|
|
369
435
|
validated.expectedSha256,
|
|
436
|
+
validated.maxBytes,
|
|
370
437
|
)?.let {
|
|
438
|
+
val finalFile = artifactFile(it.sha256)
|
|
371
439
|
promoteAtomically(partialFile, finalFile)
|
|
372
440
|
return StoredFirmwareArtifact(
|
|
373
441
|
it.artifactRef,
|
|
@@ -400,12 +468,10 @@ internal object FirmwareArtifactStore {
|
|
|
400
468
|
.build()
|
|
401
469
|
}
|
|
402
470
|
val call = client.newCall(requestBuilder.build())
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
call.timeout().timeout(deadline.toLong().coerceAtLeast(1), TimeUnit.SECONDS)
|
|
408
|
-
}
|
|
471
|
+
call.timeout().timeout(
|
|
472
|
+
ceil(validated.overallDeadlineSeconds * 1000).toLong(),
|
|
473
|
+
TimeUnit.MILLISECONDS,
|
|
474
|
+
)
|
|
409
475
|
|
|
410
476
|
registerCall(params.transactionId, call)
|
|
411
477
|
try {
|
|
@@ -428,6 +494,12 @@ internal object FirmwareArtifactStore {
|
|
|
428
494
|
error,
|
|
429
495
|
)
|
|
430
496
|
}
|
|
497
|
+
if (generateSequence<Throwable>(error) { it.cause }.any { it is SSLException }) {
|
|
498
|
+
throw IllegalStateException(
|
|
499
|
+
"ARTIFACT_TLS_FAILED: firmware TLS validation failed",
|
|
500
|
+
error,
|
|
501
|
+
)
|
|
502
|
+
}
|
|
431
503
|
throw IllegalStateException(
|
|
432
504
|
"ARTIFACT_NETWORK_FAILED: firmware request failed",
|
|
433
505
|
error,
|
|
@@ -440,15 +512,17 @@ internal object FirmwareArtifactStore {
|
|
|
440
512
|
}
|
|
441
513
|
|
|
442
514
|
val artifact = try {
|
|
443
|
-
|
|
515
|
+
validateDownloadedArtifact(
|
|
444
516
|
partialFile,
|
|
445
517
|
validated.expectedSize,
|
|
446
518
|
validated.expectedSha256,
|
|
519
|
+
validated.maxBytes,
|
|
447
520
|
)
|
|
448
521
|
} catch (error: Throwable) {
|
|
449
522
|
partialFile.delete()
|
|
450
523
|
throw error
|
|
451
524
|
}
|
|
525
|
+
val finalFile = artifactFile(artifact.sha256)
|
|
452
526
|
promoteAtomically(partialFile, finalFile)
|
|
453
527
|
return StoredFirmwareArtifact(
|
|
454
528
|
artifact.artifactRef,
|
|
@@ -462,7 +536,7 @@ internal object FirmwareArtifactStore {
|
|
|
462
536
|
response: Response,
|
|
463
537
|
partialFile: File,
|
|
464
538
|
resumeOffset: Long,
|
|
465
|
-
expectedSize: Long
|
|
539
|
+
expectedSize: Long?,
|
|
466
540
|
maxBytes: Long,
|
|
467
541
|
) {
|
|
468
542
|
require(response.code == 200 || response.code == 206) {
|
|
@@ -475,6 +549,7 @@ internal object FirmwareArtifactStore {
|
|
|
475
549
|
response.header("Content-Range"),
|
|
476
550
|
if (append) resumeOffset else 0,
|
|
477
551
|
expectedSize,
|
|
552
|
+
maxBytes,
|
|
478
553
|
)
|
|
479
554
|
) {
|
|
480
555
|
"ARTIFACT_PROTOCOL_INVALID: firmware resume Content-Range is invalid"
|
|
@@ -507,7 +582,8 @@ internal object FirmwareArtifactStore {
|
|
|
507
582
|
private fun validateContentRange(
|
|
508
583
|
value: String?,
|
|
509
584
|
expectedStart: Long,
|
|
510
|
-
expectedTotal: Long
|
|
585
|
+
expectedTotal: Long?,
|
|
586
|
+
maxBytes: Long,
|
|
511
587
|
): Boolean {
|
|
512
588
|
val match = value
|
|
513
589
|
?.lowercase()
|
|
@@ -519,7 +595,39 @@ internal object FirmwareArtifactStore {
|
|
|
519
595
|
return start == expectedStart &&
|
|
520
596
|
end >= start &&
|
|
521
597
|
end < total &&
|
|
522
|
-
total ==
|
|
598
|
+
(expectedTotal?.let { total == it } ?: (total in 1..maxBytes))
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private fun validateDownloadedArtifactOrNull(
|
|
602
|
+
file: File,
|
|
603
|
+
expectedSize: Long?,
|
|
604
|
+
expectedSha256: String?,
|
|
605
|
+
maxBytes: Long,
|
|
606
|
+
): StoredFirmwareArtifact? = try {
|
|
607
|
+
validateDownloadedArtifact(file, expectedSize, expectedSha256, maxBytes)
|
|
608
|
+
} catch (_: Throwable) {
|
|
609
|
+
null
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private fun validateDownloadedArtifact(
|
|
613
|
+
file: File,
|
|
614
|
+
expectedSize: Long?,
|
|
615
|
+
expectedSha256: String?,
|
|
616
|
+
maxBytes: Long,
|
|
617
|
+
): StoredFirmwareArtifact {
|
|
618
|
+
val size = file.length()
|
|
619
|
+
require(
|
|
620
|
+
file.isFile &&
|
|
621
|
+
size in 1..maxBytes &&
|
|
622
|
+
(expectedSize == null || size == expectedSize)
|
|
623
|
+
) {
|
|
624
|
+
"ARTIFACT_INTEGRITY_FAILED: firmware artifact size mismatch"
|
|
625
|
+
}
|
|
626
|
+
val sha256 = hashFile(file)
|
|
627
|
+
require(expectedSha256 == null || sha256 == expectedSha256) {
|
|
628
|
+
"ARTIFACT_INTEGRITY_FAILED: firmware artifact SHA-256 mismatch"
|
|
629
|
+
}
|
|
630
|
+
return StoredFirmwareArtifact("fw:$sha256", size, sha256, file)
|
|
523
631
|
}
|
|
524
632
|
|
|
525
633
|
private fun validateStoredArtifactOrNull(
|
|
@@ -563,13 +671,11 @@ internal object FirmwareArtifactStore {
|
|
|
563
671
|
"Invalid firmware transactionId"
|
|
564
672
|
}
|
|
565
673
|
return synchronized(leaseLock) {
|
|
566
|
-
val leases = loadLeasesLocked()
|
|
567
674
|
require(leases.size < 32) {
|
|
568
675
|
"Too many firmware artifact leases"
|
|
569
676
|
}
|
|
570
677
|
val leaseRef = "fwlease:${UUID.randomUUID()}"
|
|
571
678
|
leases[leaseRef] = LeaseState(transactionId, mutableSetOf())
|
|
572
|
-
saveLeasesLocked(leases)
|
|
573
679
|
leaseRef
|
|
574
680
|
}
|
|
575
681
|
}
|
|
@@ -592,41 +698,18 @@ internal object FirmwareArtifactStore {
|
|
|
592
698
|
"Invalid firmware lease disposition"
|
|
593
699
|
}
|
|
594
700
|
val transactionId = synchronized(leaseLock) {
|
|
595
|
-
val leases = loadLeasesLocked()
|
|
596
701
|
val removed = leases.remove(validateLeaseRef(leaseRef))
|
|
597
702
|
require(removed != null) {
|
|
598
703
|
"Firmware artifact lease is unavailable"
|
|
599
704
|
}
|
|
600
|
-
saveLeasesLocked(leases)
|
|
601
705
|
removed.transactionId
|
|
602
706
|
}
|
|
603
707
|
cancelledTransactions.remove(transactionId)
|
|
604
|
-
downloadLocks.keys.removeIf { it.transactionId == transactionId }
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
fun reconcileLeases(activeLeaseRefs: Array<String>) {
|
|
608
|
-
require(activeLeaseRefs.size <= 32) {
|
|
609
|
-
"Too many active firmware artifact leases"
|
|
610
|
-
}
|
|
611
|
-
val active = activeLeaseRefs.mapTo(mutableSetOf()) {
|
|
612
|
-
validateLeaseRef(it)
|
|
613
|
-
}
|
|
614
|
-
require(active.size == activeLeaseRefs.size) {
|
|
615
|
-
"Duplicate active firmware artifact lease"
|
|
616
|
-
}
|
|
617
|
-
synchronized(leaseLock) {
|
|
618
|
-
val leases = loadLeasesLocked()
|
|
619
|
-
require(active.all { leases.containsKey(it) }) {
|
|
620
|
-
"Firmware artifact lease reconciliation is incomplete"
|
|
621
|
-
}
|
|
622
|
-
leases.keys.retainAll(active)
|
|
623
|
-
saveLeasesLocked(leases)
|
|
624
|
-
}
|
|
625
708
|
}
|
|
626
709
|
|
|
627
710
|
fun sweepOrphans(): Pair<Int, Long> {
|
|
628
711
|
val retainedSha256 = synchronized(leaseLock) {
|
|
629
|
-
|
|
712
|
+
leases.values
|
|
630
713
|
.flatMap { it.artifactRefs }
|
|
631
714
|
.mapTo(mutableSetOf()) { it.removePrefix("fw:") }
|
|
632
715
|
}
|
|
@@ -636,45 +719,31 @@ internal object FirmwareArtifactStore {
|
|
|
636
719
|
val openFiles = synchronized(readerLock) {
|
|
637
720
|
readers.values.mapTo(mutableSetOf()) { it.file.absolutePath }
|
|
638
721
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
if (
|
|
646
|
-
!sha256Pattern.matches(sha256) ||
|
|
647
|
-
sha256 in retainedSha256 ||
|
|
648
|
-
sha256 in activeSha256 ||
|
|
649
|
-
file.absolutePath in openFiles
|
|
650
|
-
) {
|
|
651
|
-
return@forEach
|
|
652
|
-
}
|
|
653
|
-
val grace = if (file.name.endsWith(".bin")) {
|
|
654
|
-
FINAL_ARTIFACT_GRACE_MS
|
|
655
|
-
} else if (file.name.endsWith(".partial")) {
|
|
656
|
-
PARTIAL_ARTIFACT_GRACE_MS
|
|
657
|
-
} else {
|
|
658
|
-
return@forEach
|
|
659
|
-
}
|
|
660
|
-
if (now - file.lastModified() < grace) return@forEach
|
|
661
|
-
val size = file.length()
|
|
662
|
-
if (file.delete()) {
|
|
663
|
-
deletedFiles += 1
|
|
664
|
-
deletedBytes += size
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
return deletedFiles to deletedBytes
|
|
722
|
+
return sweepFirmwareArtifactOrphansAtRoot(
|
|
723
|
+
root = root,
|
|
724
|
+
retainedSha256 = retainedSha256,
|
|
725
|
+
activeSha256 = activeSha256,
|
|
726
|
+
openPaths = openFiles,
|
|
727
|
+
)
|
|
668
728
|
}
|
|
669
729
|
|
|
670
730
|
private fun requireLease(leaseRef: String) {
|
|
671
731
|
synchronized(leaseLock) {
|
|
672
|
-
require(
|
|
732
|
+
require(leases.containsKey(validateLeaseRef(leaseRef))) {
|
|
673
733
|
"Firmware artifact lease is unavailable"
|
|
674
734
|
}
|
|
675
735
|
}
|
|
676
736
|
}
|
|
677
737
|
|
|
738
|
+
private fun requireLeaseTransaction(leaseRef: String, transactionId: String) {
|
|
739
|
+
synchronized(leaseLock) {
|
|
740
|
+
val lease = leases[validateLeaseRef(leaseRef)]
|
|
741
|
+
require(lease?.transactionId == transactionId) {
|
|
742
|
+
"Firmware artifact lease transaction mismatch"
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
678
747
|
private fun retainExpectedArtifact(
|
|
679
748
|
leaseRef: String,
|
|
680
749
|
transactionId: String?,
|
|
@@ -684,7 +753,6 @@ internal object FirmwareArtifactStore {
|
|
|
684
753
|
"Invalid firmware artifactRef"
|
|
685
754
|
}
|
|
686
755
|
synchronized(leaseLock) {
|
|
687
|
-
val leases = loadLeasesLocked()
|
|
688
756
|
val lease = leases[validateLeaseRef(leaseRef)]
|
|
689
757
|
?: error("Firmware artifact lease is unavailable")
|
|
690
758
|
if (transactionId != null) {
|
|
@@ -692,9 +760,7 @@ internal object FirmwareArtifactStore {
|
|
|
692
760
|
"Firmware artifact lease transaction mismatch"
|
|
693
761
|
}
|
|
694
762
|
}
|
|
695
|
-
|
|
696
|
-
saveLeasesLocked(leases)
|
|
697
|
-
}
|
|
763
|
+
lease.artifactRefs.add(artifactRef)
|
|
698
764
|
}
|
|
699
765
|
}
|
|
700
766
|
|
|
@@ -705,81 +771,6 @@ internal object FirmwareArtifactStore {
|
|
|
705
771
|
return leaseRef
|
|
706
772
|
}
|
|
707
773
|
|
|
708
|
-
private fun loadLeasesLocked(): MutableMap<String, LeaseState> {
|
|
709
|
-
val file = File(root, "leases.json")
|
|
710
|
-
if (!file.exists()) return mutableMapOf()
|
|
711
|
-
require(file.length() in 1..MAX_LEASE_METADATA_BYTES) {
|
|
712
|
-
"Firmware lease metadata is too large"
|
|
713
|
-
}
|
|
714
|
-
val envelope = JSONObject(file.readText(Charsets.UTF_8))
|
|
715
|
-
require(envelope.optInt("schemaVersion") == 1) {
|
|
716
|
-
"Unsupported firmware lease schema"
|
|
717
|
-
}
|
|
718
|
-
val result = mutableMapOf<String, LeaseState>()
|
|
719
|
-
val jsonLeases = envelope.getJSONObject("leases")
|
|
720
|
-
val keys = jsonLeases.keys()
|
|
721
|
-
while (keys.hasNext()) {
|
|
722
|
-
val leaseRef = validateLeaseRef(keys.next())
|
|
723
|
-
val jsonLease = jsonLeases.getJSONObject(leaseRef)
|
|
724
|
-
val transactionId = jsonLease.getString("transactionId")
|
|
725
|
-
require(identifierPattern.matches(transactionId)) {
|
|
726
|
-
"Invalid persisted firmware transactionId"
|
|
727
|
-
}
|
|
728
|
-
val jsonRefs = jsonLease.getJSONArray("artifactRefs")
|
|
729
|
-
require(jsonRefs.length() <= 4096) {
|
|
730
|
-
"Too many persisted firmware artifact refs"
|
|
731
|
-
}
|
|
732
|
-
val refs = mutableSetOf<String>()
|
|
733
|
-
for (index in 0 until jsonRefs.length()) {
|
|
734
|
-
val artifactRef = jsonRefs.getString(index)
|
|
735
|
-
require(artifactRefPattern.matches(artifactRef) && refs.add(artifactRef)) {
|
|
736
|
-
"Invalid persisted firmware artifact ref"
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
result[leaseRef] = LeaseState(transactionId, refs)
|
|
740
|
-
}
|
|
741
|
-
require(result.size <= 32) {
|
|
742
|
-
"Too many persisted firmware artifact leases"
|
|
743
|
-
}
|
|
744
|
-
require(result.values.sumOf { it.artifactRefs.size } <= MAX_TOTAL_LEASE_REFS) {
|
|
745
|
-
"Too many persisted firmware artifact refs"
|
|
746
|
-
}
|
|
747
|
-
return result
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
private fun saveLeasesLocked(leases: Map<String, LeaseState>) {
|
|
751
|
-
require(
|
|
752
|
-
leases.size <= 32 &&
|
|
753
|
-
leases.values.sumOf { it.artifactRefs.size } <= MAX_TOTAL_LEASE_REFS
|
|
754
|
-
) {
|
|
755
|
-
"Firmware lease metadata is too large"
|
|
756
|
-
}
|
|
757
|
-
val jsonLeases = JSONObject()
|
|
758
|
-
leases.toSortedMap().forEach { (leaseRef, lease) ->
|
|
759
|
-
jsonLeases.put(
|
|
760
|
-
leaseRef,
|
|
761
|
-
JSONObject()
|
|
762
|
-
.put("transactionId", lease.transactionId)
|
|
763
|
-
.put("artifactRefs", JSONArray(lease.artifactRefs.sorted())),
|
|
764
|
-
)
|
|
765
|
-
}
|
|
766
|
-
val bytes = JSONObject()
|
|
767
|
-
.put("schemaVersion", 1)
|
|
768
|
-
.put("leases", jsonLeases)
|
|
769
|
-
.toString()
|
|
770
|
-
.toByteArray(Charsets.UTF_8)
|
|
771
|
-
require(bytes.size.toLong() <= MAX_LEASE_METADATA_BYTES) {
|
|
772
|
-
"Firmware lease metadata is too large"
|
|
773
|
-
}
|
|
774
|
-
val destination = File(root, "leases.json")
|
|
775
|
-
val temporary = File(root, ".leases-${UUID.randomUUID()}.tmp")
|
|
776
|
-
FileOutputStream(temporary).use { output ->
|
|
777
|
-
output.write(bytes)
|
|
778
|
-
output.fd.sync()
|
|
779
|
-
}
|
|
780
|
-
Os.rename(temporary.absolutePath, destination.absolutePath)
|
|
781
|
-
}
|
|
782
|
-
|
|
783
774
|
private fun markDownloadActive(sha256: String, delta: Int) {
|
|
784
775
|
synchronized(activeDownloadLock) {
|
|
785
776
|
val count = (activeDownloadCounts[sha256] ?: 0) + delta
|
|
@@ -804,6 +795,11 @@ internal object FirmwareArtifactStore {
|
|
|
804
795
|
return digest.digest().toHex()
|
|
805
796
|
}
|
|
806
797
|
|
|
798
|
+
private fun sha256(value: String): String =
|
|
799
|
+
MessageDigest.getInstance("SHA-256")
|
|
800
|
+
.digest(value.toByteArray(Charsets.UTF_8))
|
|
801
|
+
.toHex()
|
|
802
|
+
|
|
807
803
|
private fun promoteAtomically(source: File, destination: File) {
|
|
808
804
|
destination.parentFile?.mkdirs()
|
|
809
805
|
Os.rename(source.absolutePath, destination.absolutePath)
|