@hot-updater/react-native 0.36.3 → 0.36.5
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/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +37 -37
- package/android/src/main/java/com/hotupdater/RelativePathResolver.kt +2 -1
- package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +101 -6
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +18 -10
- package/android/src/test/java/com/hotupdater/RelativePathResolverTest.kt +41 -0
- package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +68 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +90 -12
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/package.json +6 -6
- package/src/sdkVersion.ts +1 -1
|
@@ -547,34 +547,28 @@ class BundleFileStorageService(
|
|
|
547
547
|
return regex.find(currentUrl)?.groupValues?.get(1)
|
|
548
548
|
}
|
|
549
549
|
|
|
550
|
-
private fun resolveBundleFile(
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
?.takeIf { File(it).name.endsWith(".android.bundle") }
|
|
560
|
-
}?.singleOrNull()
|
|
561
|
-
|
|
562
|
-
if (manifestBundlePath != null) {
|
|
563
|
-
try {
|
|
564
|
-
val canonicalBundleDir = bundleDir.canonicalFile
|
|
565
|
-
val canonicalBundleFile = File(bundleDir, manifestBundlePath).canonicalFile
|
|
566
|
-
val canonicalBundleDirPath = canonicalBundleDir.path
|
|
567
|
-
val canonicalBundleFilePath = canonicalBundleFile.path
|
|
568
|
-
val isWithinBundleDir =
|
|
569
|
-
canonicalBundleFilePath == canonicalBundleDirPath ||
|
|
570
|
-
canonicalBundleFilePath.startsWith("$canonicalBundleDirPath${File.separator}")
|
|
571
|
-
|
|
572
|
-
if (isWithinBundleDir && canonicalBundleFile.isFile) {
|
|
573
|
-
return canonicalBundleFile
|
|
574
|
-
}
|
|
575
|
-
} catch (e: Exception) {
|
|
576
|
-
Log.w(TAG, "Failed to resolve manifest bundle file from ${bundleDir.absolutePath}: ${e.message}")
|
|
550
|
+
private fun resolveBundleFile(
|
|
551
|
+
bundleDir: File,
|
|
552
|
+
expectedBundleId: String? = null,
|
|
553
|
+
): File? {
|
|
554
|
+
val manifestFile = File(bundleDir, "manifest.json")
|
|
555
|
+
if (manifestFile.exists()) {
|
|
556
|
+
val manifest = parseBundleManifestFromFile(manifestFile) ?: return null
|
|
557
|
+
if (expectedBundleId != null && manifest.bundleId != expectedBundleId) {
|
|
558
|
+
return null
|
|
577
559
|
}
|
|
560
|
+
|
|
561
|
+
val resolvedAssets =
|
|
562
|
+
manifest.assets.keys.associateWith { assetPath ->
|
|
563
|
+
RelativePathResolver
|
|
564
|
+
.resolveInside(bundleDir, assetPath)
|
|
565
|
+
?.takeIf { it.isFile }
|
|
566
|
+
?: return null
|
|
567
|
+
}
|
|
568
|
+
return resolvedAssets
|
|
569
|
+
.filterKeys { assetPath -> File(assetPath).name.endsWith(".android.bundle") }
|
|
570
|
+
.values
|
|
571
|
+
.singleOrNull()
|
|
578
572
|
}
|
|
579
573
|
|
|
580
574
|
return File(bundleDir, "index.android.bundle").absoluteFile.takeIf { it.isFile }
|
|
@@ -582,7 +576,7 @@ class BundleFileStorageService(
|
|
|
582
576
|
|
|
583
577
|
private fun findBundleFile(bundleId: String): File? {
|
|
584
578
|
val bundleDir = File(getBundleStoreDir(), bundleId)
|
|
585
|
-
return resolveBundleFile(bundleDir)
|
|
579
|
+
return resolveBundleFile(bundleDir, bundleId)
|
|
586
580
|
}
|
|
587
581
|
|
|
588
582
|
private fun getBundleUrlForId(bundleId: String): String? = findBundleFile(bundleId)?.absolutePath
|
|
@@ -1090,15 +1084,21 @@ class BundleFileStorageService(
|
|
|
1090
1084
|
return null
|
|
1091
1085
|
}
|
|
1092
1086
|
|
|
1093
|
-
val
|
|
1094
|
-
val
|
|
1095
|
-
|
|
1096
|
-
|
|
1087
|
+
val cachedBundleId = extractBundleIdFromCurrentURL()
|
|
1088
|
+
val resolvedBundle = cachedBundleId?.let(::findBundleFile)
|
|
1089
|
+
val isValid =
|
|
1090
|
+
if (cachedBundleId == null) {
|
|
1091
|
+
File(urlString).isFile
|
|
1092
|
+
} else {
|
|
1093
|
+
resolvedBundle != null
|
|
1094
|
+
}
|
|
1095
|
+
Log.d(TAG, "getCachedBundleURL: cached bundle valid = $isValid at path: $urlString")
|
|
1096
|
+
if (!isValid) {
|
|
1097
1097
|
preferences.setItem("HotUpdaterBundleURL", null)
|
|
1098
|
-
Log.d(TAG, "getCachedBundleURL:
|
|
1098
|
+
Log.d(TAG, "getCachedBundleURL: cached bundle is invalid, cleared preference")
|
|
1099
1099
|
return null
|
|
1100
1100
|
}
|
|
1101
|
-
return urlString
|
|
1101
|
+
return resolvedBundle?.absolutePath ?: urlString
|
|
1102
1102
|
}
|
|
1103
1103
|
|
|
1104
1104
|
override fun getFallbackBundleURL(): String = "assets://index.android.bundle"
|
|
@@ -1180,7 +1180,7 @@ class BundleFileStorageService(
|
|
|
1180
1180
|
val finalBundleDir = File(bundleStoreDir, bundleId)
|
|
1181
1181
|
if (finalBundleDir.exists()) {
|
|
1182
1182
|
Log.d(TAG, "Bundle for bundleId $bundleId already exists. Using cached bundle.")
|
|
1183
|
-
val existingBundleFile = resolveBundleFile(finalBundleDir)
|
|
1183
|
+
val existingBundleFile = resolveBundleFile(finalBundleDir, bundleId)
|
|
1184
1184
|
if (existingBundleFile != null) {
|
|
1185
1185
|
// Update last modified time
|
|
1186
1186
|
finalBundleDir.setLastModified(System.currentTimeMillis())
|
|
@@ -1364,7 +1364,7 @@ class BundleFileStorageService(
|
|
|
1364
1364
|
}
|
|
1365
1365
|
|
|
1366
1366
|
// 4) Resolve the extracted Android bundle file.
|
|
1367
|
-
val extractedBundleFile = resolveBundleFile(tmpDir)
|
|
1367
|
+
val extractedBundleFile = resolveBundleFile(tmpDir, bundleId)
|
|
1368
1368
|
if (extractedBundleFile == null) {
|
|
1369
1369
|
Log.d("BundleStorage", "Android bundle file could not be resolved in tmpDir.")
|
|
1370
1370
|
tempDir.deleteRecursively()
|
|
@@ -1403,7 +1403,7 @@ class BundleFileStorageService(
|
|
|
1403
1403
|
}
|
|
1404
1404
|
|
|
1405
1405
|
// 8) Verify the Android bundle file exists inside finalBundleDir.
|
|
1406
|
-
val finalBundleFile = resolveBundleFile(finalBundleDir)
|
|
1406
|
+
val finalBundleFile = resolveBundleFile(finalBundleDir, bundleId)
|
|
1407
1407
|
if (finalBundleFile == null) {
|
|
1408
1408
|
Log.d("BundleStorage", "Android bundle file could not be resolved in realDir.")
|
|
1409
1409
|
tempDir.deleteRecursively()
|
|
@@ -12,7 +12,8 @@ internal object RelativePathResolver {
|
|
|
12
12
|
return null
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// Archive directory entries conventionally end with one slash.
|
|
16
|
+
val components = path.removeSuffix("/").split("/")
|
|
16
17
|
if (components.any { it.isEmpty() || it == "." || it == ".." }) {
|
|
17
18
|
return null
|
|
18
19
|
}
|
|
@@ -13,7 +13,7 @@ class TarArchiveInputStream(
|
|
|
13
13
|
) : InputStream() {
|
|
14
14
|
private var currentEntry: TarArchiveEntry? = null
|
|
15
15
|
private var currentEntryBytesRead: Long = 0
|
|
16
|
-
private var
|
|
16
|
+
private var pendingName: String? = null
|
|
17
17
|
|
|
18
18
|
companion object {
|
|
19
19
|
private const val TAG = "TarInputStream"
|
|
@@ -73,14 +73,20 @@ class TarArchiveInputStream(
|
|
|
73
73
|
|
|
74
74
|
// Handle GNU long filename extension
|
|
75
75
|
if (entry.typeFlag == 'L') {
|
|
76
|
-
|
|
76
|
+
pendingName = readLongName(entry.size)
|
|
77
77
|
continue
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
//
|
|
81
|
-
if (
|
|
82
|
-
entry.
|
|
83
|
-
|
|
80
|
+
// Handle POSIX PAX extended header
|
|
81
|
+
if (entry.typeFlag == 'x') {
|
|
82
|
+
readPaxPath(entry.size)?.let { pendingName = it }
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Apply extended name if present
|
|
87
|
+
if (pendingName != null) {
|
|
88
|
+
entry.name = pendingName!!
|
|
89
|
+
pendingName = null
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
// Validate entry
|
|
@@ -306,6 +312,95 @@ class TarArchiveInputStream(
|
|
|
306
312
|
return String(nameBytes, 0, nameLength, Charsets.UTF_8)
|
|
307
313
|
}
|
|
308
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Read the path attribute from a POSIX PAX extended header.
|
|
317
|
+
* Record lengths are measured in bytes and include the length itself.
|
|
318
|
+
*/
|
|
319
|
+
private fun readPaxPath(size: Long): String? {
|
|
320
|
+
if (size < 0 || size > MAX_FILE_SIZE) {
|
|
321
|
+
throw SecurityException("Invalid PAX header size: $size")
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
val data = ByteArray(size.toInt())
|
|
325
|
+
var bytesRead = 0
|
|
326
|
+
while (bytesRead < data.size) {
|
|
327
|
+
val count = input.read(data, bytesRead, data.size - bytesRead)
|
|
328
|
+
if (count < 0) throw EOFException("Unexpected end reading PAX header")
|
|
329
|
+
bytesRead += count
|
|
330
|
+
}
|
|
331
|
+
skipPadding(size)
|
|
332
|
+
|
|
333
|
+
var offset = 0
|
|
334
|
+
var path: String? = null
|
|
335
|
+
while (offset < data.size) {
|
|
336
|
+
val space = findByte(data, ' '.code.toByte(), offset, data.size)
|
|
337
|
+
if (space <= offset) throw IOException("Invalid PAX record length")
|
|
338
|
+
|
|
339
|
+
val recordLength = parsePaxRecordLength(data, offset, space)
|
|
340
|
+
if (recordLength <= 0 || recordLength > data.size - offset) {
|
|
341
|
+
throw IOException("Invalid PAX record length: $recordLength")
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
val recordEnd = offset + recordLength
|
|
345
|
+
if (recordEnd <= space + 2 || data[recordEnd - 1] != '\n'.code.toByte()) {
|
|
346
|
+
throw IOException("Invalid PAX record")
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
val valueEnd = recordEnd - 1
|
|
350
|
+
val equals = findByte(data, '='.code.toByte(), space + 1, valueEnd)
|
|
351
|
+
if (equals <= space + 1) throw IOException("Invalid PAX record")
|
|
352
|
+
|
|
353
|
+
val key =
|
|
354
|
+
String(
|
|
355
|
+
data,
|
|
356
|
+
space + 1,
|
|
357
|
+
equals - space - 1,
|
|
358
|
+
Charsets.UTF_8,
|
|
359
|
+
)
|
|
360
|
+
if (key == "path") {
|
|
361
|
+
path =
|
|
362
|
+
String(
|
|
363
|
+
data,
|
|
364
|
+
equals + 1,
|
|
365
|
+
valueEnd - equals - 1,
|
|
366
|
+
Charsets.UTF_8,
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
offset = recordEnd
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return path
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private fun parsePaxRecordLength(
|
|
377
|
+
bytes: ByteArray,
|
|
378
|
+
start: Int,
|
|
379
|
+
end: Int,
|
|
380
|
+
): Int {
|
|
381
|
+
var result = 0
|
|
382
|
+
for (index in start until end) {
|
|
383
|
+
val digit = bytes[index].toInt() - '0'.code
|
|
384
|
+
if (digit !in 0..9 || result > (Int.MAX_VALUE - digit) / 10) {
|
|
385
|
+
throw IOException("Invalid PAX record length")
|
|
386
|
+
}
|
|
387
|
+
result = result * 10 + digit
|
|
388
|
+
}
|
|
389
|
+
return result
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private fun findByte(
|
|
393
|
+
bytes: ByteArray,
|
|
394
|
+
value: Byte,
|
|
395
|
+
start: Int,
|
|
396
|
+
end: Int,
|
|
397
|
+
): Int {
|
|
398
|
+
for (index in start until end) {
|
|
399
|
+
if (bytes[index] == value) return index
|
|
400
|
+
}
|
|
401
|
+
return -1
|
|
402
|
+
}
|
|
403
|
+
|
|
309
404
|
/**
|
|
310
405
|
* Skip padding to 512-byte boundary
|
|
311
406
|
*/
|
|
@@ -48,29 +48,29 @@ class BundleFileStorageServiceTest {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
@Test
|
|
51
|
-
fun `resolveBundleFile
|
|
51
|
+
fun `resolveBundleFile rejects a manifest with missing assets even when root index exists`() {
|
|
52
52
|
val rootDir = temporaryFolder.newFolder("no-android-candidate")
|
|
53
53
|
val service = createService(rootDir)
|
|
54
54
|
val bundleDir = createBundleDir(rootDir, "bundle-no-candidate")
|
|
55
|
-
|
|
55
|
+
writeFile(bundleDir, "index.android.bundle")
|
|
56
56
|
|
|
57
57
|
writeManifest(bundleDir, listOf("index.ios.bundle", "assets/image.png"))
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
assertNull(invokeResolveBundleFile(service, bundleDir))
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
@Test
|
|
63
|
-
fun `resolveBundleFile
|
|
63
|
+
fun `resolveBundleFile rejects a manifest with multiple android bundle candidates`() {
|
|
64
64
|
val rootDir = temporaryFolder.newFolder("multiple-android-candidates")
|
|
65
65
|
val service = createService(rootDir)
|
|
66
66
|
val bundleDir = createBundleDir(rootDir, "bundle-multiple-candidates")
|
|
67
|
-
|
|
67
|
+
writeFile(bundleDir, "index.android.bundle")
|
|
68
68
|
|
|
69
69
|
writeFile(bundleDir, "foo.android.bundle")
|
|
70
70
|
writeFile(bundleDir, "dist/bar.android.bundle")
|
|
71
71
|
writeManifest(bundleDir, listOf("foo.android.bundle", "dist/bar.android.bundle"))
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
assertNull(invokeResolveBundleFile(service, bundleDir))
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
@Test
|
|
@@ -122,7 +122,14 @@ class BundleFileStorageServiceTest {
|
|
|
122
122
|
val service = createService(rootDir, preferences)
|
|
123
123
|
|
|
124
124
|
val stagingDir = createBundleDir(rootDir, "staging-bundle")
|
|
125
|
-
|
|
125
|
+
writeFile(stagingDir, "dist/staging.android.bundle")
|
|
126
|
+
writeManifest(
|
|
127
|
+
stagingDir,
|
|
128
|
+
listOf(
|
|
129
|
+
"dist/staging.android.bundle",
|
|
130
|
+
"assets/missing.png",
|
|
131
|
+
),
|
|
132
|
+
)
|
|
126
133
|
|
|
127
134
|
val stableDir = createBundleDir(rootDir, "stable-bundle")
|
|
128
135
|
val stableBundleFile = writeFile(stableDir, "dist/stable.android.bundle")
|
|
@@ -141,7 +148,7 @@ class BundleFileStorageServiceTest {
|
|
|
141
148
|
val selection = service.prepareLaunch(null)
|
|
142
149
|
val report = service.notifyAppReady()
|
|
143
150
|
|
|
144
|
-
assertEquals(stableBundleFile.
|
|
151
|
+
assertEquals(stableBundleFile.absolutePath, selection.bundleUrl)
|
|
145
152
|
assertEquals(stableDir.name, selection.launchedBundleId)
|
|
146
153
|
assertFalse(selection.shouldRollbackOnCrash)
|
|
147
154
|
assertFalse(stagingDir.exists())
|
|
@@ -153,7 +160,7 @@ class BundleFileStorageServiceTest {
|
|
|
153
160
|
assertEquals(stableDir.name, metadata?.stagingBundleId)
|
|
154
161
|
assertNull(metadata?.stableBundleId)
|
|
155
162
|
assertFalse(metadata?.verificationPending ?: true)
|
|
156
|
-
assertEquals(stableBundleFile.
|
|
163
|
+
assertEquals(stableBundleFile.absolutePath, preferences.getItem("HotUpdaterBundleURL"))
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
@Test
|
|
@@ -416,9 +423,10 @@ class BundleFileStorageServiceTest {
|
|
|
416
423
|
BundleFileStorageService::class.java.getDeclaredMethod(
|
|
417
424
|
"resolveBundleFile",
|
|
418
425
|
File::class.java,
|
|
426
|
+
String::class.java,
|
|
419
427
|
)
|
|
420
428
|
method.isAccessible = true
|
|
421
|
-
return method.invoke(service, bundleDir) as File?
|
|
429
|
+
return method.invoke(service, bundleDir, bundleDir.name) as File?
|
|
422
430
|
}
|
|
423
431
|
|
|
424
432
|
private fun invokeCanUseManifestDrivenInstall(service: BundleFileStorageService): Boolean {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertNull
|
|
5
|
+
import org.junit.Rule
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
import org.junit.rules.TemporaryFolder
|
|
8
|
+
import java.io.File
|
|
9
|
+
|
|
10
|
+
class RelativePathResolverTest {
|
|
11
|
+
@get:Rule
|
|
12
|
+
val temporaryFolder = TemporaryFolder()
|
|
13
|
+
|
|
14
|
+
@Test
|
|
15
|
+
fun `resolves a standard archive directory entry ending in a slash`() {
|
|
16
|
+
val root = temporaryFolder.root
|
|
17
|
+
|
|
18
|
+
val resolved = RelativePathResolver.resolveInside(root, "drawable-mdpi/")
|
|
19
|
+
|
|
20
|
+
assertEquals(
|
|
21
|
+
File(root, "drawable-mdpi").canonicalFile,
|
|
22
|
+
resolved?.canonicalFile,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Test
|
|
27
|
+
fun `rejects unsafe archive directory paths ending in a slash`() {
|
|
28
|
+
val root = temporaryFolder.root
|
|
29
|
+
val unsafePaths =
|
|
30
|
+
listOf(
|
|
31
|
+
"/",
|
|
32
|
+
"../",
|
|
33
|
+
"drawable-mdpi/../",
|
|
34
|
+
"drawable-mdpi//",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
unsafePaths.forEach { path ->
|
|
38
|
+
assertNull(path, RelativePathResolver.resolveInside(root, path))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
|
|
4
|
+
import org.junit.Assert.assertArrayEquals
|
|
5
|
+
import org.junit.Assert.assertEquals
|
|
6
|
+
import org.junit.Assert.assertNull
|
|
7
|
+
import org.junit.Assert.assertThrows
|
|
8
|
+
import org.junit.Test
|
|
9
|
+
import java.io.ByteArrayInputStream
|
|
10
|
+
import java.io.ByteArrayOutputStream
|
|
11
|
+
import org.apache.commons.compress.archivers.tar.TarArchiveEntry as CommonsTarArchiveEntry
|
|
12
|
+
|
|
13
|
+
class TarArchiveInputStreamTest {
|
|
14
|
+
@Test
|
|
15
|
+
fun `reads a long basename from a POSIX PAX header`() {
|
|
16
|
+
val path = "raw/${"long-name-".repeat(14)}asset.bmp"
|
|
17
|
+
val contents = "pax asset".toByteArray()
|
|
18
|
+
val archive = createPaxArchive(path, contents)
|
|
19
|
+
|
|
20
|
+
TarArchiveInputStream(ByteArrayInputStream(archive)).use { tar ->
|
|
21
|
+
val entry = tar.getNextEntry()
|
|
22
|
+
|
|
23
|
+
assertEquals(path, entry?.name)
|
|
24
|
+
assertArrayEquals(contents, tar.readBytes())
|
|
25
|
+
assertNull(tar.getNextEntry())
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
fun `rejects path traversal from a POSIX PAX header`() {
|
|
31
|
+
val path = "../${"long-name-".repeat(14)}asset.bmp"
|
|
32
|
+
val archive =
|
|
33
|
+
createPaxArchive(
|
|
34
|
+
path = path,
|
|
35
|
+
contents = "malicious".toByteArray(),
|
|
36
|
+
headerPath = "safe.bmp",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
val error =
|
|
40
|
+
assertThrows(SecurityException::class.java) {
|
|
41
|
+
TarArchiveInputStream(ByteArrayInputStream(archive)).use { tar ->
|
|
42
|
+
tar.getNextEntry()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
assertEquals("Path traversal detected: $path", error.message)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private fun createPaxArchive(
|
|
50
|
+
path: String,
|
|
51
|
+
contents: ByteArray,
|
|
52
|
+
headerPath: String = path,
|
|
53
|
+
): ByteArray {
|
|
54
|
+
val output = ByteArrayOutputStream()
|
|
55
|
+
TarArchiveOutputStream(output).use { tar ->
|
|
56
|
+
tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX)
|
|
57
|
+
val entry =
|
|
58
|
+
CommonsTarArchiveEntry(headerPath).apply {
|
|
59
|
+
size = contents.size.toLong()
|
|
60
|
+
if (path != headerPath) addPaxHeader("path", path)
|
|
61
|
+
}
|
|
62
|
+
tar.putArchiveEntry(entry)
|
|
63
|
+
tar.write(contents)
|
|
64
|
+
tar.closeArchiveEntry()
|
|
65
|
+
}
|
|
66
|
+
return output.toByteArray()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -1141,7 +1141,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1141
1141
|
let fallbackBundleId = metadata.stableBundleId.flatMap { candidate in
|
|
1142
1142
|
if case .success(let storeDir) = bundleStoreDir() {
|
|
1143
1143
|
let stableBundleDir = (storeDir as NSString).appendingPathComponent(candidate)
|
|
1144
|
-
if case .success(let bundlePath) = findBundleFile(
|
|
1144
|
+
if case .success(let bundlePath) = findBundleFile(
|
|
1145
|
+
in: stableBundleDir,
|
|
1146
|
+
expectedBundleId: candidate
|
|
1147
|
+
), bundlePath != nil {
|
|
1145
1148
|
return candidate
|
|
1146
1149
|
}
|
|
1147
1150
|
}
|
|
@@ -1163,7 +1166,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1163
1166
|
if let fallbackBundleId,
|
|
1164
1167
|
case .success(let storeDir) = bundleStoreDir() {
|
|
1165
1168
|
let fallbackBundleDir = (storeDir as NSString).appendingPathComponent(fallbackBundleId)
|
|
1166
|
-
if case .success(let bundlePath) = findBundleFile(
|
|
1169
|
+
if case .success(let bundlePath) = findBundleFile(
|
|
1170
|
+
in: fallbackBundleDir,
|
|
1171
|
+
expectedBundleId: fallbackBundleId
|
|
1172
|
+
), let bundlePath {
|
|
1167
1173
|
let _ = setBundleURL(localPath: bundlePath)
|
|
1168
1174
|
}
|
|
1169
1175
|
} else {
|
|
@@ -1278,9 +1284,45 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1278
1284
|
* @param directoryPath Directory to search in
|
|
1279
1285
|
* @return Result with path to bundle file or error
|
|
1280
1286
|
*/
|
|
1281
|
-
func findBundleFile(
|
|
1287
|
+
func findBundleFile(
|
|
1288
|
+
in directoryPath: String,
|
|
1289
|
+
expectedBundleId: String? = nil
|
|
1290
|
+
) -> Result<String?, Error> {
|
|
1282
1291
|
NSLog("[BundleStorage] Searching for bundle file in directory: \(directoryPath)")
|
|
1283
1292
|
|
|
1293
|
+
let manifestPath = (directoryPath as NSString).appendingPathComponent("manifest.json")
|
|
1294
|
+
if fileSystem.fileExists(atPath: manifestPath) {
|
|
1295
|
+
guard let manifest = readManifest(in: directoryPath),
|
|
1296
|
+
let parsedManifest = parseBundleManifest(from: manifest),
|
|
1297
|
+
expectedBundleId == nil || parsedManifest.bundleId == expectedBundleId else {
|
|
1298
|
+
return .success(nil)
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
do {
|
|
1302
|
+
var bundlePaths: [String] = []
|
|
1303
|
+
for assetPath in parsedManifest.assets.keys {
|
|
1304
|
+
let assetURL = try ArchiveExtractionUtilities.extractionURL(
|
|
1305
|
+
for: assetPath,
|
|
1306
|
+
destinationRoot: directoryPath
|
|
1307
|
+
)
|
|
1308
|
+
guard fileSystem.fileExists(atPath: assetURL.path),
|
|
1309
|
+
let attributes = try? fileSystem.attributesOfItem(atPath: assetURL.path),
|
|
1310
|
+
attributes[.type] as? FileAttributeType == .typeRegular else {
|
|
1311
|
+
return .success(nil)
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
let fileName = assetURL.lastPathComponent
|
|
1315
|
+
if fileName.hasSuffix(".bundle") || fileName == "main.jsbundle" {
|
|
1316
|
+
bundlePaths.append(assetURL.path)
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
return .success(bundlePaths.count == 1 ? bundlePaths[0] : nil)
|
|
1321
|
+
} catch {
|
|
1322
|
+
return .failure(error)
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1284
1326
|
let iosBundlePath = (directoryPath as NSString).appendingPathComponent("index.ios.bundle")
|
|
1285
1327
|
if self.fileSystem.fileExists(atPath: iosBundlePath) {
|
|
1286
1328
|
NSLog("[BundleStorage] Found iOS bundle atPath: \(iosBundlePath)")
|
|
@@ -1421,12 +1463,39 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1421
1463
|
*/
|
|
1422
1464
|
func getCachedBundleURL() -> URL? {
|
|
1423
1465
|
do {
|
|
1424
|
-
guard let savedURLString = try self.preferences.getItem(forKey: "HotUpdaterBundleURL")
|
|
1425
|
-
|
|
1426
|
-
|
|
1466
|
+
guard let savedURLString = try self.preferences.getItem(forKey: "HotUpdaterBundleURL") else {
|
|
1467
|
+
return nil
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
let savedURL = URL(string: savedURLString)
|
|
1471
|
+
let bundleURL = (
|
|
1472
|
+
(savedURL?.isFileURL == true ? savedURL : nil)
|
|
1473
|
+
?? URL(fileURLWithPath: savedURLString)
|
|
1474
|
+
).standardizedFileURL
|
|
1475
|
+
guard self.fileSystem.fileExists(atPath: bundleURL.path),
|
|
1476
|
+
case .success(let storeDir) = bundleStoreDir() else {
|
|
1477
|
+
return nil
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
let storeURL = URL(fileURLWithPath: storeDir, isDirectory: true).standardizedFileURL
|
|
1481
|
+
let storePrefix = storeURL.path + "/"
|
|
1482
|
+
guard bundleURL.path.hasPrefix(storePrefix),
|
|
1483
|
+
let bundleId = bundleURL.path
|
|
1484
|
+
.dropFirst(storePrefix.count)
|
|
1485
|
+
.split(separator: "/")
|
|
1486
|
+
.first
|
|
1487
|
+
.map(String.init) else {
|
|
1488
|
+
return nil
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
let bundleDirectory = storeURL.appendingPathComponent(bundleId).path
|
|
1492
|
+
guard case .success(let resolvedBundlePath) = findBundleFile(
|
|
1493
|
+
in: bundleDirectory,
|
|
1494
|
+
expectedBundleId: bundleId
|
|
1495
|
+
), let resolvedBundlePath else {
|
|
1427
1496
|
return nil
|
|
1428
1497
|
}
|
|
1429
|
-
return
|
|
1498
|
+
return URL(fileURLWithPath: resolvedBundlePath)
|
|
1430
1499
|
} catch {
|
|
1431
1500
|
NSLog("[BundleStorage] Error getting cached bundle URL: \(error.localizedDescription)")
|
|
1432
1501
|
return nil
|
|
@@ -1454,7 +1523,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1454
1523
|
if let stagingId = metadata.stagingBundleId,
|
|
1455
1524
|
case .success(let storeDir) = bundleStoreDir() {
|
|
1456
1525
|
let stagingBundleDir = (storeDir as NSString).appendingPathComponent(stagingId)
|
|
1457
|
-
if case .success(let bundlePath) = findBundleFile(
|
|
1526
|
+
if case .success(let bundlePath) = findBundleFile(
|
|
1527
|
+
in: stagingBundleDir,
|
|
1528
|
+
expectedBundleId: stagingId
|
|
1529
|
+
), let bundlePath {
|
|
1458
1530
|
return LaunchSelection(
|
|
1459
1531
|
bundleURL: URL(fileURLWithPath: bundlePath),
|
|
1460
1532
|
launchedBundleId: stagingId,
|
|
@@ -1470,7 +1542,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1470
1542
|
if let stableId = metadata.stableBundleId,
|
|
1471
1543
|
case .success(let storeDir) = bundleStoreDir() {
|
|
1472
1544
|
let stableBundleDir = (storeDir as NSString).appendingPathComponent(stableId)
|
|
1473
|
-
if case .success(let bundlePath) = findBundleFile(
|
|
1545
|
+
if case .success(let bundlePath) = findBundleFile(
|
|
1546
|
+
in: stableBundleDir,
|
|
1547
|
+
expectedBundleId: stableId
|
|
1548
|
+
), let bundlePath {
|
|
1474
1549
|
return LaunchSelection(
|
|
1475
1550
|
bundleURL: URL(fileURLWithPath: bundlePath),
|
|
1476
1551
|
launchedBundleId: stableId,
|
|
@@ -1552,7 +1627,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1552
1627
|
let finalBundleDir = (storeDir as NSString).appendingPathComponent(bundleId)
|
|
1553
1628
|
|
|
1554
1629
|
if self.fileSystem.fileExists(atPath: finalBundleDir) {
|
|
1555
|
-
let findResult = self.findBundleFile(
|
|
1630
|
+
let findResult = self.findBundleFile(
|
|
1631
|
+
in: finalBundleDir,
|
|
1632
|
+
expectedBundleId: bundleId
|
|
1633
|
+
)
|
|
1556
1634
|
switch findResult {
|
|
1557
1635
|
case .success(let existingBundlePath):
|
|
1558
1636
|
if let bundlePath = existingBundlePath {
|
|
@@ -1969,7 +2047,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1969
2047
|
let manifestDestination = (tmpDir as NSString).appendingPathComponent("manifest.json")
|
|
1970
2048
|
try writeManifestFile(targetManifest, to: manifestDestination)
|
|
1971
2049
|
|
|
1972
|
-
switch self.findBundleFile(in: tmpDir) {
|
|
2050
|
+
switch self.findBundleFile(in: tmpDir, expectedBundleId: bundleId) {
|
|
1973
2051
|
case .success(let maybeBundlePath):
|
|
1974
2052
|
guard let bundlePathInTmp = maybeBundlePath else {
|
|
1975
2053
|
throw BundleStorageError.invalidBundle
|
|
@@ -2289,7 +2367,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2289
2367
|
progressHandler: progressHandler,
|
|
2290
2368
|
progress: UpdateProgress.bundleValidation
|
|
2291
2369
|
)
|
|
2292
|
-
switch self.findBundleFile(in: tmpDir) {
|
|
2370
|
+
switch self.findBundleFile(in: tmpDir, expectedBundleId: bundleId) {
|
|
2293
2371
|
case .success(let maybeBundlePath):
|
|
2294
2372
|
if let bundlePathInTmp = maybeBundlePath {
|
|
2295
2373
|
NSLog("[BundleStorage] Found valid bundle in tmpDir: \(bundlePathInTmp)")
|
|
@@ -4,5 +4,5 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.HOT_UPDATER_SDK_VERSION = void 0;
|
|
7
|
-
const HOT_UPDATER_SDK_VERSION = exports.HOT_UPDATER_SDK_VERSION = "0.36.
|
|
7
|
+
const HOT_UPDATER_SDK_VERSION = exports.HOT_UPDATER_SDK_VERSION = "0.36.5";
|
|
8
8
|
//# sourceMappingURL=sdkVersion.js.map
|
package/lib/module/sdkVersion.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const HOT_UPDATER_SDK_VERSION = "0.36.
|
|
1
|
+
export declare const HOT_UPDATER_SDK_VERSION = "0.36.5";
|
|
2
2
|
//# sourceMappingURL=sdkVersion.d.ts.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const HOT_UPDATER_SDK_VERSION = "0.36.
|
|
1
|
+
export declare const HOT_UPDATER_SDK_VERSION = "0.36.5";
|
|
2
2
|
//# sourceMappingURL=sdkVersion.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/react-native",
|
|
3
|
-
"version": "0.36.
|
|
3
|
+
"version": "0.36.5",
|
|
4
4
|
"description": "React Native OTA solution for self-hosted",
|
|
5
5
|
"source": "./src/index.ts",
|
|
6
6
|
"main": "./lib/commonjs/index.js",
|
|
@@ -131,14 +131,14 @@
|
|
|
131
131
|
"react-native-builder-bob": "0.41.0",
|
|
132
132
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
133
133
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
134
|
-
"hot-updater": "0.36.
|
|
134
|
+
"hot-updater": "0.36.5"
|
|
135
135
|
},
|
|
136
136
|
"dependencies": {
|
|
137
137
|
"use-sync-external-store": "1.5.0",
|
|
138
|
-
"@hot-updater/cli-tools": "0.36.
|
|
139
|
-
"@hot-updater/js": "0.36.
|
|
140
|
-
"@hot-updater/core": "0.36.
|
|
141
|
-
"@hot-updater/plugin-core": "0.36.
|
|
138
|
+
"@hot-updater/cli-tools": "0.36.5",
|
|
139
|
+
"@hot-updater/js": "0.36.5",
|
|
140
|
+
"@hot-updater/core": "0.36.5",
|
|
141
|
+
"@hot-updater/plugin-core": "0.36.5"
|
|
142
142
|
},
|
|
143
143
|
"scripts": {
|
|
144
144
|
"build": "node ./scripts/sync-sdk-version.mjs run -- bob build && tsc -p plugin/tsconfig.build.json",
|
package/src/sdkVersion.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const HOT_UPDATER_SDK_VERSION = "0.36.
|
|
1
|
+
export const HOT_UPDATER_SDK_VERSION = "0.36.5";
|