@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.
Files changed (38) hide show
  1. package/ReactNativeRangeDownloader.podspec +1 -1
  2. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRules.kt +12 -5
  3. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactOrphanSweep.kt +111 -0
  4. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactStore.kt +185 -189
  5. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ReactNativeRangeDownloader.kt +16 -20
  6. package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRulesTest.kt +39 -0
  7. package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactDeadlineTest.kt +26 -0
  8. package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactOrphanSweepTest.kt +107 -0
  9. package/ios/FirmwareArtifactStore.swift +662 -408
  10. package/ios/RangeDownloadLogic.swift +297 -0
  11. package/ios/ReactNativeRangeDownloader.swift +42 -471
  12. package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts +3 -7
  13. package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts.map +1 -1
  14. package/nitrogen/generated/android/c++/JFirmwareArchiveMaterializeParams.hpp +7 -6
  15. package/nitrogen/generated/android/c++/JFirmwareArtifactDownloadParams.hpp +7 -7
  16. package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.cpp +0 -19
  17. package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.hpp +0 -1
  18. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveMaterializeParams.kt +2 -2
  19. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactDownloadParams.kt +3 -3
  20. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/HybridReactNativeRangeDownloaderSpec.kt +0 -4
  21. package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Bridge.hpp +15 -0
  22. package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Umbrella.hpp +0 -3
  23. package/nitrogen/generated/ios/c++/HybridReactNativeRangeDownloaderSpecSwift.hpp +0 -11
  24. package/nitrogen/generated/ios/swift/FirmwareArchiveMaterializeParams.swift +32 -13
  25. package/nitrogen/generated/ios/swift/FirmwareArtifactDownloadParams.swift +39 -8
  26. package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec.swift +0 -1
  27. package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec_cxx.swift +0 -19
  28. package/nitrogen/generated/shared/c++/FirmwareArchiveMaterializeParams.hpp +6 -5
  29. package/nitrogen/generated/shared/c++/FirmwareArtifactDownloadParams.hpp +9 -9
  30. package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.cpp +0 -1
  31. package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.hpp +0 -4
  32. package/package.json +2 -2
  33. package/src/ReactNativeRangeDownloader.nitro.ts +4 -11
  34. package/ios/FirmwareBackgroundSessionEventRouter.swift +0 -17
  35. package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseReconcileParams.hpp +0 -76
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseReconcileParams.kt +0 -38
  37. package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseReconcileParams.swift +0 -48
  38. package/nitrogen/generated/shared/c++/FirmwareArtifactLeaseReconcileParams.hpp +0 -76
@@ -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
- import org.json.JSONArray
24
- import org.json.JSONObject
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 expectedSha256: String,
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 = ConcurrentHashMap<FirmwareDownloadKey, Any>()
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
- retainExpectedArtifact(
101
- leaseRef = params.leaseRef,
102
- transactionId = params.transactionId,
103
- artifactRef = "fw:${validated.expectedSha256}",
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
- validated.expectedSha256,
127
+ params.taskId,
128
+ validated.downloadToken,
108
129
  )
109
- val lock = downloadLocks.computeIfAbsent(lockKey) { Any() }
110
- markDownloadActive(validated.expectedSha256, 1)
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
- return synchronized(lock) {
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.expectedSha256, -1)
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(loadLeasesLocked().values.none { artifactRef in it.artifactRefs }) {
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 = FirmwareArchiveRules.validateRequirements(expectedEntries)
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.associateBy { it.entryName }
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 requirement = requirementsByName[zipEntry.name]
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.expectedSize.toLong()
220
- val expectedSha256 = requirement.expectedSha256.lowercase()
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(entrySize == expectedSize && sha256 == expectedSha256) {
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 = requirement.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.toExactPositiveLong("expectedSize")
368
+ val expectedSize = params.expectedSize?.toExactPositiveLong("expectedSize")
320
369
  val maxBytes = params.maxBytes.toExactPositiveLong("maxBytes")
321
- require(maxBytes == expectedSize && maxBytes <= MAX_ARTIFACT_BYTES) {
370
+ require(
371
+ maxBytes <= MAX_ARTIFACT_BYTES &&
372
+ (expectedSize == null || expectedSize <= maxBytes)
373
+ ) {
322
374
  "Invalid firmware maxBytes"
323
375
  }
324
- require(sha256Pattern.matches(params.expectedSha256)) {
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 = params.expectedSha256.lowercase(),
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
- val finalFile = artifactFile(validated.expectedSha256)
352
- validateStoredArtifactOrNull(
353
- finalFile,
354
- validated.expectedSize,
355
- validated.expectedSha256,
356
- )?.let { return it }
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.expectedSha256}.${params.taskId}.partial",
421
+ "${validated.downloadToken}.${params.taskId}.$transactionToken.partial",
361
422
  )
362
- if (partialFile.length() > validated.expectedSize) {
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 (partialFile.length() == validated.expectedSize) {
366
- validateStoredArtifactOrNull(
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
- params.overallDeadlineSeconds?.let { deadline ->
404
- require(deadline.isFinite() && deadline > 0) {
405
- "Invalid firmware download deadline"
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
- validateStoredArtifact(
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 == expectedTotal
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
- loadLeasesLocked().values
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
- val now = System.currentTimeMillis()
640
- var deletedFiles = 0
641
- var deletedBytes = 0L
642
- root.listFiles()?.forEach { file ->
643
- if (!file.isFile || file.name == "leases.json") return@forEach
644
- val sha256 = file.name.take(64)
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(loadLeasesLocked().containsKey(validateLeaseRef(leaseRef))) {
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
- if (lease.artifactRefs.add(artifactRef)) {
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)