@onekeyfe/react-native-range-downloader 3.0.81-alpha.7 → 3.0.81-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ReactNativeRangeDownloader.podspec +1 -1
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRules.kt +12 -5
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactOrphanSweep.kt +111 -0
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactStore.kt +132 -68
- package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ReactNativeRangeDownloader.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRulesTest.kt +39 -0
- package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactOrphanSweepTest.kt +107 -0
- package/ios/FirmwareArtifactStore.swift +242 -172
- package/ios/RangeDownloadLogic.swift +194 -36
- package/ios/ReactNativeRangeDownloader.swift +9 -423
- package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts +3 -3
- package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFirmwareArchiveMaterializeParams.hpp +7 -6
- package/nitrogen/generated/android/c++/JFirmwareArtifactDownloadParams.hpp +7 -7
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveMaterializeParams.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactDownloadParams.kt +3 -3
- package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Bridge.hpp +15 -0
- package/nitrogen/generated/ios/swift/FirmwareArchiveMaterializeParams.swift +32 -13
- package/nitrogen/generated/ios/swift/FirmwareArtifactDownloadParams.swift +39 -8
- package/nitrogen/generated/shared/c++/FirmwareArchiveMaterializeParams.hpp +6 -5
- package/nitrogen/generated/shared/c++/FirmwareArtifactDownloadParams.hpp +9 -9
- package/package.json +2 -2
- package/src/ReactNativeRangeDownloader.nitro.ts +3 -3
- package/ios/FirmwareBackgroundSessionEventRouter.swift +0 -17
|
@@ -23,7 +23,7 @@ Pod::Spec.new do |s|
|
|
|
23
23
|
s.dependency 'React-jsi'
|
|
24
24
|
s.dependency 'React-callinvoker'
|
|
25
25
|
s.dependency 'ReactNativeNativeLogger'
|
|
26
|
-
s.dependency 'SniConnect', package["
|
|
26
|
+
s.dependency 'SniConnect', package["peerDependencies"]["@onekeyfe/react-native-sni-connect"]
|
|
27
27
|
s.public_header_files = "ios/FirmwareArchiveMinizipBridge.h"
|
|
28
28
|
s.pod_target_xcconfig = {
|
|
29
29
|
'HEADER_SEARCH_PATHS' => '"$(PODS_ROOT)/SSZipArchive/SSZipArchive/minizip"',
|
package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArchiveRules.kt
CHANGED
|
@@ -78,24 +78,31 @@ internal object FirmwareArchiveRules {
|
|
|
78
78
|
|
|
79
79
|
fun validateCentralDirectory(
|
|
80
80
|
file: File,
|
|
81
|
-
requirements: List<FirmwareArchiveExpectedEntry
|
|
81
|
+
requirements: List<FirmwareArchiveExpectedEntry>?,
|
|
82
82
|
): List<FirmwareArchiveCentralEntry> {
|
|
83
83
|
val entries = scanCentralDirectory(file)
|
|
84
|
-
require(entries.size == requirements.size) {
|
|
84
|
+
require(requirements == null || entries.size == requirements.size) {
|
|
85
85
|
"Firmware archive has missing or extra entries"
|
|
86
86
|
}
|
|
87
|
-
val requirementsByName = requirements
|
|
87
|
+
val requirementsByName = requirements?.associateBy { it.entryName }.orEmpty()
|
|
88
88
|
val names = mutableSetOf<String>()
|
|
89
89
|
val canonicalNames = mutableSetOf<String>()
|
|
90
|
+
var totalSize = 0L
|
|
90
91
|
entries.forEach { entry ->
|
|
91
92
|
val requirement = requirementsByName[entry.name]
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
require(requirements == null || requirement != null) {
|
|
94
|
+
"Firmware archive contains an unexpected entry"
|
|
95
|
+
}
|
|
96
|
+
val expectedSize = requirement?.expectedSize?.toLong()
|
|
97
|
+
?: entry.uncompressedSize
|
|
98
|
+
totalSize = Math.addExact(totalSize, entry.uncompressedSize)
|
|
94
99
|
require(
|
|
95
100
|
names.add(entry.name) &&
|
|
96
101
|
validatePortableName(entry.name, canonicalNames) &&
|
|
97
102
|
entry.uncompressedSize == expectedSize &&
|
|
98
103
|
entry.uncompressedSize > 0 &&
|
|
104
|
+
entry.uncompressedSize <= MAX_ARCHIVE_ENTRY_BYTES &&
|
|
105
|
+
totalSize <= MAX_ARCHIVE_EXPANDED_BYTES &&
|
|
99
106
|
entry.compressedSize in 0..MAX_ARCHIVE_EXPANDED_BYTES &&
|
|
100
107
|
entry.uncompressedSize <=
|
|
101
108
|
Math.multiplyExact(entry.compressedSize.coerceAtLeast(1), 1000) &&
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
package com.margelo.nitro.reactnativerangedownloader
|
|
2
|
+
|
|
3
|
+
import java.io.File
|
|
4
|
+
|
|
5
|
+
internal const val FIRMWARE_ARTIFACT_FINAL_GRACE_MS = 24L * 60 * 60 * 1000
|
|
6
|
+
internal const val FIRMWARE_ARTIFACT_PARTIAL_GRACE_MS = 7L * 24 * 60 * 60 * 1000
|
|
7
|
+
internal const val FIRMWARE_ARTIFACT_SCRATCH_GRACE_MS =
|
|
8
|
+
FIRMWARE_ARTIFACT_PARTIAL_GRACE_MS
|
|
9
|
+
|
|
10
|
+
private val firmwareArtifactSha256Pattern = Regex("^[a-f0-9]{64}$")
|
|
11
|
+
private val firmwareArchiveScratchPattern = Regex(
|
|
12
|
+
"^archive-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-" +
|
|
13
|
+
"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
|
|
14
|
+
)
|
|
15
|
+
private val firmwarePromoteScratchPattern = Regex(
|
|
16
|
+
"^\\.promote-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-" +
|
|
17
|
+
"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
private fun File.isSymbolicLinkEntry(): Boolean = try {
|
|
21
|
+
val canonicalParent = parentFile?.canonicalFile
|
|
22
|
+
val entryFromCanonicalParent = canonicalParent?.let { File(it, name) } ?: this
|
|
23
|
+
entryFromCanonicalParent.canonicalFile != entryFromCanonicalParent.absoluteFile
|
|
24
|
+
} catch (_: Exception) {
|
|
25
|
+
true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private fun firmwareArtifactEntrySize(entry: File): Long {
|
|
29
|
+
if (entry.isSymbolicLinkEntry()) return 0
|
|
30
|
+
if (entry.isFile) return entry.length()
|
|
31
|
+
if (!entry.isDirectory) return 0
|
|
32
|
+
return entry.listFiles()?.sumOf(::firmwareArtifactEntrySize) ?: 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private fun deleteFirmwareArtifactEntry(entry: File): Boolean {
|
|
36
|
+
if (entry.isSymbolicLinkEntry()) return entry.delete()
|
|
37
|
+
if (entry.isDirectory) {
|
|
38
|
+
val children = entry.listFiles() ?: return false
|
|
39
|
+
if (children.any { !deleteFirmwareArtifactEntry(it) }) return false
|
|
40
|
+
}
|
|
41
|
+
return entry.delete()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private fun firmwareArtifactEntryExceededGrace(
|
|
45
|
+
entry: File,
|
|
46
|
+
nowMs: Long,
|
|
47
|
+
graceMs: Long,
|
|
48
|
+
): Boolean {
|
|
49
|
+
val modifiedAt = entry.lastModified()
|
|
50
|
+
return modifiedAt > 0 && modifiedAt <= nowMs && nowMs - modifiedAt >= graceMs
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
internal fun sweepFirmwareArtifactOrphansAtRoot(
|
|
54
|
+
root: File,
|
|
55
|
+
retainedSha256: Set<String>,
|
|
56
|
+
activeSha256: Set<String>,
|
|
57
|
+
openPaths: Set<String>,
|
|
58
|
+
nowMs: Long = System.currentTimeMillis(),
|
|
59
|
+
): Pair<Int, Long> {
|
|
60
|
+
var deletedFiles = 0
|
|
61
|
+
var deletedBytes = 0L
|
|
62
|
+
root.listFiles()?.forEach { entry ->
|
|
63
|
+
val isSymbolicLink = entry.isSymbolicLinkEntry()
|
|
64
|
+
val isArchiveScratch = firmwareArchiveScratchPattern.matches(entry.name)
|
|
65
|
+
val isPromoteScratch = firmwarePromoteScratchPattern.matches(entry.name)
|
|
66
|
+
val isScratchCandidate = !isSymbolicLink &&
|
|
67
|
+
((isArchiveScratch && entry.isDirectory) ||
|
|
68
|
+
(isPromoteScratch && entry.isFile))
|
|
69
|
+
if (isScratchCandidate) {
|
|
70
|
+
if (
|
|
71
|
+
firmwareArtifactEntryExceededGrace(
|
|
72
|
+
entry,
|
|
73
|
+
nowMs,
|
|
74
|
+
FIRMWARE_ARTIFACT_SCRATCH_GRACE_MS,
|
|
75
|
+
)
|
|
76
|
+
) {
|
|
77
|
+
val size = firmwareArtifactEntrySize(entry)
|
|
78
|
+
if (deleteFirmwareArtifactEntry(entry)) {
|
|
79
|
+
deletedFiles += 1
|
|
80
|
+
deletedBytes += size
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return@forEach
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!entry.isFile || isSymbolicLink || entry.name.length < 64) return@forEach
|
|
87
|
+
val sha256 = entry.name.take(64)
|
|
88
|
+
if (
|
|
89
|
+
!firmwareArtifactSha256Pattern.matches(sha256) ||
|
|
90
|
+
sha256 in retainedSha256 ||
|
|
91
|
+
sha256 in activeSha256 ||
|
|
92
|
+
entry.absolutePath in openPaths
|
|
93
|
+
) {
|
|
94
|
+
return@forEach
|
|
95
|
+
}
|
|
96
|
+
val grace = if (entry.name.endsWith(".bin")) {
|
|
97
|
+
FIRMWARE_ARTIFACT_FINAL_GRACE_MS
|
|
98
|
+
} else if (entry.name.endsWith(".partial")) {
|
|
99
|
+
FIRMWARE_ARTIFACT_PARTIAL_GRACE_MS
|
|
100
|
+
} else {
|
|
101
|
+
return@forEach
|
|
102
|
+
}
|
|
103
|
+
if (!firmwareArtifactEntryExceededGrace(entry, nowMs, grace)) return@forEach
|
|
104
|
+
val size = entry.length()
|
|
105
|
+
if (entry.delete()) {
|
|
106
|
+
deletedFiles += 1
|
|
107
|
+
deletedBytes += size
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return deletedFiles to deletedBytes
|
|
111
|
+
}
|
package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactStore.kt
CHANGED
|
@@ -57,7 +57,9 @@ private data class StagedFirmwareArchiveEntry(
|
|
|
57
57
|
)
|
|
58
58
|
|
|
59
59
|
private data class FirmwareDownloadKey(
|
|
60
|
-
val
|
|
60
|
+
val transactionId: String,
|
|
61
|
+
val taskId: String,
|
|
62
|
+
val downloadToken: String,
|
|
61
63
|
)
|
|
62
64
|
|
|
63
65
|
private class FirmwareDownloadLock {
|
|
@@ -69,8 +71,6 @@ internal object FirmwareArtifactStore {
|
|
|
69
71
|
const val MAX_READ_BYTES = 256 * 1024
|
|
70
72
|
|
|
71
73
|
private const val MAX_ARTIFACT_BYTES = 512L * 1024 * 1024
|
|
72
|
-
private const val FINAL_ARTIFACT_GRACE_MS = 24L * 60 * 60 * 1000
|
|
73
|
-
private const val PARTIAL_ARTIFACT_GRACE_MS = 7L * 24 * 60 * 60 * 1000
|
|
74
74
|
private val sha256Pattern = Regex("^[a-fA-F0-9]{64}$")
|
|
75
75
|
private val artifactRefPattern = Regex("^fw:[a-f0-9]{64}$")
|
|
76
76
|
private val leaseRefPattern = Regex("^fwlease:[a-f0-9-]{36}$")
|
|
@@ -115,27 +115,39 @@ internal object FirmwareArtifactStore {
|
|
|
115
115
|
check(!cancelledTransactions.contains(params.transactionId)) {
|
|
116
116
|
"ARTIFACT_CANCELLED: firmware artifact download was cancelled"
|
|
117
117
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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)
|
|
125
|
+
val lockKey = FirmwareDownloadKey(
|
|
126
|
+
params.transactionId,
|
|
127
|
+
params.taskId,
|
|
128
|
+
validated.downloadToken,
|
|
122
129
|
)
|
|
123
|
-
val lockKey = FirmwareDownloadKey(validated.expectedSha256)
|
|
124
130
|
val downloadLock = downloadLocks.compute(lockKey) { _, current ->
|
|
125
131
|
(current ?: FirmwareDownloadLock()).also {
|
|
126
132
|
it.references += 1
|
|
127
133
|
}
|
|
128
134
|
} ?: error("Firmware artifact lock is unavailable")
|
|
129
|
-
markDownloadActive(validated.
|
|
135
|
+
markDownloadActive(validated.downloadToken, 1)
|
|
130
136
|
try {
|
|
131
|
-
|
|
137
|
+
val artifact = synchronized(downloadLock.monitor) {
|
|
132
138
|
check(!cancelledTransactions.contains(params.transactionId)) {
|
|
133
139
|
"ARTIFACT_CANCELLED: firmware artifact download was cancelled"
|
|
134
140
|
}
|
|
135
141
|
downloadLocked(params, validated)
|
|
136
142
|
}
|
|
143
|
+
retainExpectedArtifact(
|
|
144
|
+
leaseRef = params.leaseRef,
|
|
145
|
+
transactionId = params.transactionId,
|
|
146
|
+
artifactRef = artifact.artifactRef,
|
|
147
|
+
)
|
|
148
|
+
return artifact
|
|
137
149
|
} finally {
|
|
138
|
-
markDownloadActive(validated.
|
|
150
|
+
markDownloadActive(validated.downloadToken, -1)
|
|
139
151
|
downloadLocks.compute(lockKey) { _, current ->
|
|
140
152
|
if (current !== downloadLock) {
|
|
141
153
|
current
|
|
@@ -213,16 +225,19 @@ internal object FirmwareArtifactStore {
|
|
|
213
225
|
fun materializeArchive(
|
|
214
226
|
leaseRef: String,
|
|
215
227
|
artifactRef: String,
|
|
216
|
-
expectedEntries: Array<FirmwareArchiveExpectedEntry
|
|
228
|
+
expectedEntries: Array<FirmwareArchiveExpectedEntry>?,
|
|
217
229
|
): List<StoredFirmwareArchiveEntry> {
|
|
218
230
|
requireLease(leaseRef)
|
|
219
231
|
val archiveFile = resolveArtifactFile(artifactRef)
|
|
220
|
-
val requirements =
|
|
232
|
+
val requirements = expectedEntries?.let {
|
|
233
|
+
FirmwareArchiveRules.validateRequirements(it)
|
|
234
|
+
}
|
|
221
235
|
val centralEntries = FirmwareArchiveRules.validateCentralDirectory(
|
|
222
236
|
archiveFile,
|
|
223
237
|
requirements,
|
|
224
238
|
)
|
|
225
|
-
val requirementsByName = requirements
|
|
239
|
+
val requirementsByName = requirements?.associateBy { it.entryName }.orEmpty()
|
|
240
|
+
val centralEntriesByName = centralEntries.associateBy { it.name }
|
|
226
241
|
val centralNames = centralEntries.mapTo(mutableSetOf()) { it.name }
|
|
227
242
|
val scratchDir = File(root, "archive-${UUID.randomUUID()}")
|
|
228
243
|
check(scratchDir.mkdirs()) { "Firmware archive scratch directory cannot be created" }
|
|
@@ -235,16 +250,18 @@ internal object FirmwareArtifactStore {
|
|
|
235
250
|
require(!zipEntry.isDirectory) {
|
|
236
251
|
"Firmware archive contains an unexpected directory"
|
|
237
252
|
}
|
|
238
|
-
val
|
|
253
|
+
val centralEntry = centralEntriesByName[zipEntry.name]
|
|
239
254
|
?: error("Firmware archive contains an unexpected entry")
|
|
255
|
+
val requirement = requirementsByName[zipEntry.name]
|
|
240
256
|
require(
|
|
241
257
|
centralNames.contains(zipEntry.name) &&
|
|
242
258
|
entryNames.add(zipEntry.name)
|
|
243
259
|
) {
|
|
244
260
|
"Firmware archive contains a duplicate or mismatched entry"
|
|
245
261
|
}
|
|
246
|
-
val expectedSize = requirement
|
|
247
|
-
|
|
262
|
+
val expectedSize = requirement?.expectedSize?.toLong()
|
|
263
|
+
?: centralEntry.uncompressedSize
|
|
264
|
+
val expectedSha256 = requirement?.expectedSha256?.lowercase()
|
|
248
265
|
val scratchFile = File(scratchDir, "${staged.size}.entry")
|
|
249
266
|
val digest = MessageDigest.getInstance("SHA-256")
|
|
250
267
|
var entrySize = 0L
|
|
@@ -264,11 +281,14 @@ internal object FirmwareArtifactStore {
|
|
|
264
281
|
output.fd.sync()
|
|
265
282
|
}
|
|
266
283
|
val sha256 = digest.digest().toHex()
|
|
267
|
-
require(
|
|
284
|
+
require(
|
|
285
|
+
entrySize == expectedSize &&
|
|
286
|
+
(expectedSha256 == null || sha256 == expectedSha256)
|
|
287
|
+
) {
|
|
268
288
|
"Firmware archive entry integrity mismatch"
|
|
269
289
|
}
|
|
270
290
|
staged += StagedFirmwareArchiveEntry(
|
|
271
|
-
entryName =
|
|
291
|
+
entryName = zipEntry.name,
|
|
272
292
|
size = entrySize,
|
|
273
293
|
sha256 = sha256,
|
|
274
294
|
file = scratchFile,
|
|
@@ -307,9 +327,10 @@ internal object FirmwareArtifactStore {
|
|
|
307
327
|
}
|
|
308
328
|
|
|
309
329
|
private data class ValidatedDownload(
|
|
310
|
-
val expectedSize: Long
|
|
330
|
+
val expectedSize: Long?,
|
|
311
331
|
val maxBytes: Long,
|
|
312
|
-
val expectedSha256: String
|
|
332
|
+
val expectedSha256: String?,
|
|
333
|
+
val downloadToken: String,
|
|
313
334
|
val hostname: String,
|
|
314
335
|
val overallDeadlineSeconds: Double,
|
|
315
336
|
)
|
|
@@ -344,12 +365,16 @@ internal object FirmwareArtifactStore {
|
|
|
344
365
|
) {
|
|
345
366
|
"Firmware URL must use HTTPS port 443"
|
|
346
367
|
}
|
|
347
|
-
val expectedSize = params.expectedSize
|
|
368
|
+
val expectedSize = params.expectedSize?.toExactPositiveLong("expectedSize")
|
|
348
369
|
val maxBytes = params.maxBytes.toExactPositiveLong("maxBytes")
|
|
349
|
-
require(
|
|
370
|
+
require(
|
|
371
|
+
maxBytes <= MAX_ARTIFACT_BYTES &&
|
|
372
|
+
(expectedSize == null || expectedSize <= maxBytes)
|
|
373
|
+
) {
|
|
350
374
|
"Invalid firmware maxBytes"
|
|
351
375
|
}
|
|
352
|
-
|
|
376
|
+
val expectedSha256 = params.expectedSha256?.lowercase()
|
|
377
|
+
require(expectedSha256 == null || sha256Pattern.matches(expectedSha256)) {
|
|
353
378
|
"Invalid firmware artifact SHA-256"
|
|
354
379
|
}
|
|
355
380
|
val overallDeadlineSeconds =
|
|
@@ -369,7 +394,8 @@ internal object FirmwareArtifactStore {
|
|
|
369
394
|
return ValidatedDownload(
|
|
370
395
|
expectedSize = expectedSize,
|
|
371
396
|
maxBytes = maxBytes,
|
|
372
|
-
expectedSha256 =
|
|
397
|
+
expectedSha256 = expectedSha256,
|
|
398
|
+
downloadToken = expectedSha256 ?: sha256(params.url),
|
|
373
399
|
hostname = url.host,
|
|
374
400
|
overallDeadlineSeconds = overallDeadlineSeconds,
|
|
375
401
|
)
|
|
@@ -379,26 +405,37 @@ internal object FirmwareArtifactStore {
|
|
|
379
405
|
params: FirmwareArtifactDownloadParams,
|
|
380
406
|
validated: ValidatedDownload,
|
|
381
407
|
): StoredFirmwareArtifact {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
+
}
|
|
388
417
|
|
|
418
|
+
val transactionToken = sha256(params.transactionId).take(16)
|
|
389
419
|
val partialFile = File(
|
|
390
420
|
root,
|
|
391
|
-
"${validated.
|
|
421
|
+
"${validated.downloadToken}.${params.taskId}.$transactionToken.partial",
|
|
392
422
|
)
|
|
393
|
-
if (partialFile.length() >
|
|
423
|
+
if (validated.expectedSha256 == null && partialFile.length() > 0) {
|
|
424
|
+
check(partialFile.delete()) { "Unverified firmware partial cannot be removed" }
|
|
425
|
+
} else if (partialFile.length() > validated.maxBytes) {
|
|
394
426
|
check(partialFile.delete()) { "Invalid firmware partial cannot be removed" }
|
|
395
427
|
}
|
|
396
|
-
if (
|
|
397
|
-
|
|
428
|
+
if (
|
|
429
|
+
validated.expectedSize != null &&
|
|
430
|
+
partialFile.length() == validated.expectedSize
|
|
431
|
+
) {
|
|
432
|
+
validateDownloadedArtifactOrNull(
|
|
398
433
|
partialFile,
|
|
399
434
|
validated.expectedSize,
|
|
400
435
|
validated.expectedSha256,
|
|
436
|
+
validated.maxBytes,
|
|
401
437
|
)?.let {
|
|
438
|
+
val finalFile = artifactFile(it.sha256)
|
|
402
439
|
promoteAtomically(partialFile, finalFile)
|
|
403
440
|
return StoredFirmwareArtifact(
|
|
404
441
|
it.artifactRef,
|
|
@@ -475,15 +512,17 @@ internal object FirmwareArtifactStore {
|
|
|
475
512
|
}
|
|
476
513
|
|
|
477
514
|
val artifact = try {
|
|
478
|
-
|
|
515
|
+
validateDownloadedArtifact(
|
|
479
516
|
partialFile,
|
|
480
517
|
validated.expectedSize,
|
|
481
518
|
validated.expectedSha256,
|
|
519
|
+
validated.maxBytes,
|
|
482
520
|
)
|
|
483
521
|
} catch (error: Throwable) {
|
|
484
522
|
partialFile.delete()
|
|
485
523
|
throw error
|
|
486
524
|
}
|
|
525
|
+
val finalFile = artifactFile(artifact.sha256)
|
|
487
526
|
promoteAtomically(partialFile, finalFile)
|
|
488
527
|
return StoredFirmwareArtifact(
|
|
489
528
|
artifact.artifactRef,
|
|
@@ -497,7 +536,7 @@ internal object FirmwareArtifactStore {
|
|
|
497
536
|
response: Response,
|
|
498
537
|
partialFile: File,
|
|
499
538
|
resumeOffset: Long,
|
|
500
|
-
expectedSize: Long
|
|
539
|
+
expectedSize: Long?,
|
|
501
540
|
maxBytes: Long,
|
|
502
541
|
) {
|
|
503
542
|
require(response.code == 200 || response.code == 206) {
|
|
@@ -510,6 +549,7 @@ internal object FirmwareArtifactStore {
|
|
|
510
549
|
response.header("Content-Range"),
|
|
511
550
|
if (append) resumeOffset else 0,
|
|
512
551
|
expectedSize,
|
|
552
|
+
maxBytes,
|
|
513
553
|
)
|
|
514
554
|
) {
|
|
515
555
|
"ARTIFACT_PROTOCOL_INVALID: firmware resume Content-Range is invalid"
|
|
@@ -542,7 +582,8 @@ internal object FirmwareArtifactStore {
|
|
|
542
582
|
private fun validateContentRange(
|
|
543
583
|
value: String?,
|
|
544
584
|
expectedStart: Long,
|
|
545
|
-
expectedTotal: Long
|
|
585
|
+
expectedTotal: Long?,
|
|
586
|
+
maxBytes: Long,
|
|
546
587
|
): Boolean {
|
|
547
588
|
val match = value
|
|
548
589
|
?.lowercase()
|
|
@@ -554,7 +595,39 @@ internal object FirmwareArtifactStore {
|
|
|
554
595
|
return start == expectedStart &&
|
|
555
596
|
end >= start &&
|
|
556
597
|
end < total &&
|
|
557
|
-
total ==
|
|
598
|
+
(expectedTotal?.let { total == it } ?: (total in 1..maxBytes))
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private fun validateDownloadedArtifactOrNull(
|
|
602
|
+
file: File,
|
|
603
|
+
expectedSize: Long?,
|
|
604
|
+
expectedSha256: String?,
|
|
605
|
+
maxBytes: Long,
|
|
606
|
+
): StoredFirmwareArtifact? = try {
|
|
607
|
+
validateDownloadedArtifact(file, expectedSize, expectedSha256, maxBytes)
|
|
608
|
+
} catch (_: Throwable) {
|
|
609
|
+
null
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private fun validateDownloadedArtifact(
|
|
613
|
+
file: File,
|
|
614
|
+
expectedSize: Long?,
|
|
615
|
+
expectedSha256: String?,
|
|
616
|
+
maxBytes: Long,
|
|
617
|
+
): StoredFirmwareArtifact {
|
|
618
|
+
val size = file.length()
|
|
619
|
+
require(
|
|
620
|
+
file.isFile &&
|
|
621
|
+
size in 1..maxBytes &&
|
|
622
|
+
(expectedSize == null || size == expectedSize)
|
|
623
|
+
) {
|
|
624
|
+
"ARTIFACT_INTEGRITY_FAILED: firmware artifact size mismatch"
|
|
625
|
+
}
|
|
626
|
+
val sha256 = hashFile(file)
|
|
627
|
+
require(expectedSha256 == null || sha256 == expectedSha256) {
|
|
628
|
+
"ARTIFACT_INTEGRITY_FAILED: firmware artifact SHA-256 mismatch"
|
|
629
|
+
}
|
|
630
|
+
return StoredFirmwareArtifact("fw:$sha256", size, sha256, file)
|
|
558
631
|
}
|
|
559
632
|
|
|
560
633
|
private fun validateStoredArtifactOrNull(
|
|
@@ -646,35 +719,12 @@ internal object FirmwareArtifactStore {
|
|
|
646
719
|
val openFiles = synchronized(readerLock) {
|
|
647
720
|
readers.values.mapTo(mutableSetOf()) { it.file.absolutePath }
|
|
648
721
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (
|
|
656
|
-
!sha256Pattern.matches(sha256) ||
|
|
657
|
-
sha256 in retainedSha256 ||
|
|
658
|
-
sha256 in activeSha256 ||
|
|
659
|
-
file.absolutePath in openFiles
|
|
660
|
-
) {
|
|
661
|
-
return@forEach
|
|
662
|
-
}
|
|
663
|
-
val grace = if (file.name.endsWith(".bin")) {
|
|
664
|
-
FINAL_ARTIFACT_GRACE_MS
|
|
665
|
-
} else if (file.name.endsWith(".partial")) {
|
|
666
|
-
PARTIAL_ARTIFACT_GRACE_MS
|
|
667
|
-
} else {
|
|
668
|
-
return@forEach
|
|
669
|
-
}
|
|
670
|
-
if (now - file.lastModified() < grace) return@forEach
|
|
671
|
-
val size = file.length()
|
|
672
|
-
if (file.delete()) {
|
|
673
|
-
deletedFiles += 1
|
|
674
|
-
deletedBytes += size
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
return deletedFiles to deletedBytes
|
|
722
|
+
return sweepFirmwareArtifactOrphansAtRoot(
|
|
723
|
+
root = root,
|
|
724
|
+
retainedSha256 = retainedSha256,
|
|
725
|
+
activeSha256 = activeSha256,
|
|
726
|
+
openPaths = openFiles,
|
|
727
|
+
)
|
|
678
728
|
}
|
|
679
729
|
|
|
680
730
|
private fun requireLease(leaseRef: String) {
|
|
@@ -685,6 +735,15 @@ internal object FirmwareArtifactStore {
|
|
|
685
735
|
}
|
|
686
736
|
}
|
|
687
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
|
+
|
|
688
747
|
private fun retainExpectedArtifact(
|
|
689
748
|
leaseRef: String,
|
|
690
749
|
transactionId: String?,
|
|
@@ -736,6 +795,11 @@ internal object FirmwareArtifactStore {
|
|
|
736
795
|
return digest.digest().toHex()
|
|
737
796
|
}
|
|
738
797
|
|
|
798
|
+
private fun sha256(value: String): String =
|
|
799
|
+
MessageDigest.getInstance("SHA-256")
|
|
800
|
+
.digest(value.toByteArray(Charsets.UTF_8))
|
|
801
|
+
.toHex()
|
|
802
|
+
|
|
739
803
|
private fun promoteAtomically(source: File, destination: File) {
|
|
740
804
|
destination.parentFile?.mkdirs()
|
|
741
805
|
Os.rename(source.absolutePath, destination.absolutePath)
|
|
@@ -272,7 +272,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
|
|
|
272
272
|
|
|
273
273
|
override fun getFirmwareArtifactCapabilities(): FirmwareArtifactCapabilities {
|
|
274
274
|
return FirmwareArtifactCapabilities(
|
|
275
|
-
firmwareArtifactProtocolVersion =
|
|
275
|
+
firmwareArtifactProtocolVersion = 3.0,
|
|
276
276
|
supportedRouteTypes = arrayOf("domain", "pinnedIp"),
|
|
277
277
|
supportsArchiveMaterialization = true,
|
|
278
278
|
maxReadBytes = FirmwareArtifactStore.MAX_READ_BYTES.toDouble(),
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
package com.margelo.nitro.reactnativerangedownloader
|
|
2
2
|
|
|
3
|
+
import java.io.File
|
|
4
|
+
import java.io.FileOutputStream
|
|
5
|
+
import java.util.zip.ZipEntry
|
|
6
|
+
import java.util.zip.ZipOutputStream
|
|
3
7
|
import org.junit.Assert.assertEquals
|
|
4
8
|
import org.junit.Assert.assertThrows
|
|
9
|
+
import org.junit.Rule
|
|
5
10
|
import org.junit.Test
|
|
11
|
+
import org.junit.rules.TemporaryFolder
|
|
6
12
|
|
|
7
13
|
class FirmwareArchiveRulesTest {
|
|
14
|
+
@get:Rule
|
|
15
|
+
val temporaryFolder = TemporaryFolder()
|
|
16
|
+
|
|
8
17
|
private fun entry(
|
|
9
18
|
artifactId: String = "resource-entry",
|
|
10
19
|
entryName: String = "assets/icon.png",
|
|
@@ -72,4 +81,34 @@ class FirmwareArchiveRulesTest {
|
|
|
72
81
|
)
|
|
73
82
|
}
|
|
74
83
|
}
|
|
84
|
+
|
|
85
|
+
@Test
|
|
86
|
+
fun acceptsPortableEntriesWithoutExpectedIntegrityMetadata() {
|
|
87
|
+
val archive = createArchive("assets/icon.png", byteArrayOf(1, 2, 3))
|
|
88
|
+
|
|
89
|
+
val entries = FirmwareArchiveRules.validateCentralDirectory(archive, null)
|
|
90
|
+
|
|
91
|
+
assertEquals(1, entries.size)
|
|
92
|
+
assertEquals("assets/icon.png", entries.single().name)
|
|
93
|
+
assertEquals(3, entries.single().uncompressedSize)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Test
|
|
97
|
+
fun rejectsTraversalWithoutExpectedIntegrityMetadata() {
|
|
98
|
+
val archive = createArchive("../icon.png", byteArrayOf(1))
|
|
99
|
+
|
|
100
|
+
assertThrows(IllegalArgumentException::class.java) {
|
|
101
|
+
FirmwareArchiveRules.validateCentralDirectory(archive, null)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private fun createArchive(entryName: String, content: ByteArray): File {
|
|
106
|
+
val archive = temporaryFolder.newFile("firmware.zip")
|
|
107
|
+
ZipOutputStream(FileOutputStream(archive)).use { zip ->
|
|
108
|
+
zip.putNextEntry(ZipEntry(entryName))
|
|
109
|
+
zip.write(content)
|
|
110
|
+
zip.closeEntry()
|
|
111
|
+
}
|
|
112
|
+
return archive
|
|
113
|
+
}
|
|
75
114
|
}
|