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