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