@hot-updater/react-native 0.36.4 → 0.36.6

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.
@@ -547,34 +547,28 @@ class BundleFileStorageService(
547
547
  return regex.find(currentUrl)?.groupValues?.get(1)
548
548
  }
549
549
 
550
- private fun resolveBundleFile(bundleDir: File): File? {
551
- val manifest = readManifestFromBundleDir(bundleDir)
552
- val manifestBundlePath =
553
- (manifest?.get("assets") as? Map<*, *>)
554
- ?.keys
555
- ?.mapNotNull { key ->
556
- (key as? String)
557
- ?.trim()
558
- ?.takeIf { it.isNotEmpty() }
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 file = File(urlString)
1094
- val exists = file.exists()
1095
- Log.d(TAG, "getCachedBundleURL: file exists = $exists at path: $urlString")
1096
- if (!exists) {
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: file doesn't exist, cleared preference")
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
- val components = path.split("/")
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
  }
@@ -48,29 +48,29 @@ class BundleFileStorageServiceTest {
48
48
  }
49
49
 
50
50
  @Test
51
- fun `resolveBundleFile falls back to root index when manifest has no android bundle candidate`() {
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
- val fallbackBundleFile = writeFile(bundleDir, "index.android.bundle")
55
+ writeFile(bundleDir, "index.android.bundle")
56
56
 
57
57
  writeManifest(bundleDir, listOf("index.ios.bundle", "assets/image.png"))
58
58
 
59
- assertResolvedBundlePath(service, bundleDir, fallbackBundleFile)
59
+ assertNull(invokeResolveBundleFile(service, bundleDir))
60
60
  }
61
61
 
62
62
  @Test
63
- fun `resolveBundleFile falls back to root index when manifest has multiple android bundle candidates`() {
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
- val fallbackBundleFile = writeFile(bundleDir, "index.android.bundle")
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
- assertResolvedBundlePath(service, bundleDir, fallbackBundleFile)
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
- writeManifest(stagingDir, listOf("dist/missing.android.bundle"))
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.canonicalFile.absolutePath, selection.bundleUrl)
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.canonicalFile.absolutePath, preferences.getItem("HotUpdaterBundleURL"))
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
+ }
@@ -791,7 +791,7 @@ class BundleFileStorageService: BundleStorageService {
791
791
  }
792
792
 
793
793
  private func createInitialMetadata() -> BundleMetadata {
794
- let currentBundleId = getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent
794
+ let currentBundleId = getCachedBundleId()
795
795
  return BundleMetadata(
796
796
  isolationKey: isolationKey,
797
797
  stableBundleId: nil,
@@ -808,7 +808,7 @@ class BundleFileStorageService: BundleStorageService {
808
808
  }
809
809
 
810
810
  private func getActiveBundleId() -> String? {
811
- if let cachedBundleId = getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent {
811
+ if let cachedBundleId = getCachedBundleId() {
812
812
  return cachedBundleId
813
813
  }
814
814
 
@@ -870,12 +870,11 @@ class BundleFileStorageService: BundleStorageService {
870
870
  }
871
871
 
872
872
  func canUseManifestDrivenInstall() -> Bool {
873
- guard let currentBundleURL = getCachedBundleURL(),
873
+ guard let currentBundleId = getCachedBundleId(),
874
874
  case .success(let storeDir) = bundleStoreDir() else {
875
875
  return false
876
876
  }
877
877
 
878
- let currentBundleId = currentBundleURL.deletingLastPathComponent().lastPathComponent
879
878
  let currentBundleDir = (storeDir as NSString).appendingPathComponent(currentBundleId)
880
879
  guard fileSystem.fileExists(atPath: currentBundleDir),
881
880
  let snapshot = getActiveBundleMetadataSnapshot(),
@@ -1141,7 +1140,10 @@ class BundleFileStorageService: BundleStorageService {
1141
1140
  let fallbackBundleId = metadata.stableBundleId.flatMap { candidate in
1142
1141
  if case .success(let storeDir) = bundleStoreDir() {
1143
1142
  let stableBundleDir = (storeDir as NSString).appendingPathComponent(candidate)
1144
- if case .success(let bundlePath) = findBundleFile(in: stableBundleDir), bundlePath != nil {
1143
+ if case .success(let bundlePath) = findBundleFile(
1144
+ in: stableBundleDir,
1145
+ expectedBundleId: candidate
1146
+ ), bundlePath != nil {
1145
1147
  return candidate
1146
1148
  }
1147
1149
  }
@@ -1163,7 +1165,10 @@ class BundleFileStorageService: BundleStorageService {
1163
1165
  if let fallbackBundleId,
1164
1166
  case .success(let storeDir) = bundleStoreDir() {
1165
1167
  let fallbackBundleDir = (storeDir as NSString).appendingPathComponent(fallbackBundleId)
1166
- if case .success(let bundlePath) = findBundleFile(in: fallbackBundleDir), let bundlePath {
1168
+ if case .success(let bundlePath) = findBundleFile(
1169
+ in: fallbackBundleDir,
1170
+ expectedBundleId: fallbackBundleId
1171
+ ), let bundlePath {
1167
1172
  let _ = setBundleURL(localPath: bundlePath)
1168
1173
  }
1169
1174
  } else {
@@ -1278,9 +1283,45 @@ class BundleFileStorageService: BundleStorageService {
1278
1283
  * @param directoryPath Directory to search in
1279
1284
  * @return Result with path to bundle file or error
1280
1285
  */
1281
- func findBundleFile(in directoryPath: String) -> Result<String?, Error> {
1286
+ func findBundleFile(
1287
+ in directoryPath: String,
1288
+ expectedBundleId: String? = nil
1289
+ ) -> Result<String?, Error> {
1282
1290
  NSLog("[BundleStorage] Searching for bundle file in directory: \(directoryPath)")
1283
1291
 
1292
+ let manifestPath = (directoryPath as NSString).appendingPathComponent("manifest.json")
1293
+ if fileSystem.fileExists(atPath: manifestPath) {
1294
+ guard let manifest = readManifest(in: directoryPath),
1295
+ let parsedManifest = parseBundleManifest(from: manifest),
1296
+ expectedBundleId == nil || parsedManifest.bundleId == expectedBundleId else {
1297
+ return .success(nil)
1298
+ }
1299
+
1300
+ do {
1301
+ var bundlePaths: [String] = []
1302
+ for assetPath in parsedManifest.assets.keys {
1303
+ let assetURL = try ArchiveExtractionUtilities.extractionURL(
1304
+ for: assetPath,
1305
+ destinationRoot: directoryPath
1306
+ )
1307
+ guard fileSystem.fileExists(atPath: assetURL.path),
1308
+ let attributes = try? fileSystem.attributesOfItem(atPath: assetURL.path),
1309
+ attributes[.type] as? FileAttributeType == .typeRegular else {
1310
+ return .success(nil)
1311
+ }
1312
+
1313
+ let fileName = assetURL.lastPathComponent
1314
+ if fileName.hasSuffix(".bundle") || fileName == "main.jsbundle" {
1315
+ bundlePaths.append(assetURL.path)
1316
+ }
1317
+ }
1318
+
1319
+ return .success(bundlePaths.count == 1 ? bundlePaths[0] : nil)
1320
+ } catch {
1321
+ return .failure(error)
1322
+ }
1323
+ }
1324
+
1284
1325
  let iosBundlePath = (directoryPath as NSString).appendingPathComponent("index.ios.bundle")
1285
1326
  if self.fileSystem.fileExists(atPath: iosBundlePath) {
1286
1327
  NSLog("[BundleStorage] Found iOS bundle atPath: \(iosBundlePath)")
@@ -1415,18 +1456,91 @@ class BundleFileStorageService: BundleStorageService {
1415
1456
  return .failure(error)
1416
1457
  }
1417
1458
  }
1459
+
1460
+ private func bundleId(for bundleURL: URL, storeDir: String) -> String? {
1461
+ let storeURL = URL(fileURLWithPath: storeDir, isDirectory: true).standardizedFileURL
1462
+ let bundleURL = bundleURL.standardizedFileURL
1463
+ let storePrefix = storeURL.path + "/"
1464
+
1465
+ guard bundleURL.path.hasPrefix(storePrefix) else {
1466
+ return nil
1467
+ }
1468
+
1469
+ return bundleURL.path
1470
+ .dropFirst(storePrefix.count)
1471
+ .split(separator: "/")
1472
+ .first
1473
+ .map(String.init)
1474
+ }
1475
+
1476
+ private func getCachedBundleId() -> String? {
1477
+ guard let bundleURL = getCachedBundleURL(),
1478
+ case .success(let storeDir) = bundleStoreDir() else {
1479
+ return nil
1480
+ }
1481
+
1482
+ return bundleId(for: bundleURL, storeDir: storeDir)
1483
+ }
1484
+
1485
+ func resolveBundlePathAfterMove(
1486
+ _ bundlePath: String,
1487
+ from sourceDirectory: String,
1488
+ to destinationDirectory: String
1489
+ ) throws -> String {
1490
+ let sourceURL = URL(
1491
+ fileURLWithPath: sourceDirectory,
1492
+ isDirectory: true
1493
+ ).standardizedFileURL
1494
+ let bundleURL = URL(fileURLWithPath: bundlePath).standardizedFileURL
1495
+ let sourcePrefix = sourceURL.path + "/"
1496
+
1497
+ guard bundleURL.path.hasPrefix(sourcePrefix) else {
1498
+ throw BundleStorageError.invalidBundle
1499
+ }
1500
+
1501
+ let relativePath = String(bundleURL.path.dropFirst(sourcePrefix.count))
1502
+ guard ArchiveExtractionUtilities.normalizedRelativePath(from: relativePath) == relativePath else {
1503
+ throw BundleStorageError.invalidBundle
1504
+ }
1505
+
1506
+ return try ArchiveExtractionUtilities.extractionURL(
1507
+ for: relativePath,
1508
+ destinationRoot: destinationDirectory
1509
+ ).path
1510
+ }
1418
1511
 
1419
1512
  /**
1420
1513
  * Gets the URL to the cached bundle file if it exists.
1421
1514
  */
1422
1515
  func getCachedBundleURL() -> URL? {
1423
1516
  do {
1424
- guard let savedURLString = try self.preferences.getItem(forKey: "HotUpdaterBundleURL"),
1425
- let bundleURL = URL(string: savedURLString),
1426
- self.fileSystem.fileExists(atPath: bundleURL.path) else {
1517
+ guard let savedURLString = try self.preferences.getItem(forKey: "HotUpdaterBundleURL") else {
1427
1518
  return nil
1428
1519
  }
1429
- return bundleURL
1520
+
1521
+ let savedURL = URL(string: savedURLString)
1522
+ let bundleURL = (
1523
+ (savedURL?.isFileURL == true ? savedURL : nil)
1524
+ ?? URL(fileURLWithPath: savedURLString)
1525
+ ).standardizedFileURL
1526
+ guard self.fileSystem.fileExists(atPath: bundleURL.path),
1527
+ case .success(let storeDir) = bundleStoreDir() else {
1528
+ return nil
1529
+ }
1530
+
1531
+ let storeURL = URL(fileURLWithPath: storeDir, isDirectory: true).standardizedFileURL
1532
+ guard let bundleId = bundleId(for: bundleURL, storeDir: storeDir) else {
1533
+ return nil
1534
+ }
1535
+
1536
+ let bundleDirectory = storeURL.appendingPathComponent(bundleId).path
1537
+ guard case .success(let resolvedBundlePath) = findBundleFile(
1538
+ in: bundleDirectory,
1539
+ expectedBundleId: bundleId
1540
+ ), let resolvedBundlePath else {
1541
+ return nil
1542
+ }
1543
+ return URL(fileURLWithPath: resolvedBundlePath)
1430
1544
  } catch {
1431
1545
  NSLog("[BundleStorage] Error getting cached bundle URL: \(error.localizedDescription)")
1432
1546
  return nil
@@ -1446,7 +1560,7 @@ class BundleFileStorageService: BundleStorageService {
1446
1560
  guard let metadata = loadMetadataOrNull() else {
1447
1561
  return LaunchSelection(
1448
1562
  bundleURL: getCachedBundleURL() ?? getFallbackBundleURL(bundle: bundle),
1449
- launchedBundleId: getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent,
1563
+ launchedBundleId: getCachedBundleId(),
1450
1564
  shouldRollbackOnCrash: false
1451
1565
  )
1452
1566
  }
@@ -1454,7 +1568,10 @@ class BundleFileStorageService: BundleStorageService {
1454
1568
  if let stagingId = metadata.stagingBundleId,
1455
1569
  case .success(let storeDir) = bundleStoreDir() {
1456
1570
  let stagingBundleDir = (storeDir as NSString).appendingPathComponent(stagingId)
1457
- if case .success(let bundlePath) = findBundleFile(in: stagingBundleDir), let bundlePath {
1571
+ if case .success(let bundlePath) = findBundleFile(
1572
+ in: stagingBundleDir,
1573
+ expectedBundleId: stagingId
1574
+ ), let bundlePath {
1458
1575
  return LaunchSelection(
1459
1576
  bundleURL: URL(fileURLWithPath: bundlePath),
1460
1577
  launchedBundleId: stagingId,
@@ -1470,7 +1587,10 @@ class BundleFileStorageService: BundleStorageService {
1470
1587
  if let stableId = metadata.stableBundleId,
1471
1588
  case .success(let storeDir) = bundleStoreDir() {
1472
1589
  let stableBundleDir = (storeDir as NSString).appendingPathComponent(stableId)
1473
- if case .success(let bundlePath) = findBundleFile(in: stableBundleDir), let bundlePath {
1590
+ if case .success(let bundlePath) = findBundleFile(
1591
+ in: stableBundleDir,
1592
+ expectedBundleId: stableId
1593
+ ), let bundlePath {
1474
1594
  return LaunchSelection(
1475
1595
  bundleURL: URL(fileURLWithPath: bundlePath),
1476
1596
  launchedBundleId: stableId,
@@ -1512,7 +1632,7 @@ class BundleFileStorageService: BundleStorageService {
1512
1632
  }
1513
1633
 
1514
1634
  // Get the current bundle ID from the cached bundle URL (exclude fallback bundles)
1515
- let currentBundleId = self.getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent
1635
+ let currentBundleId = self.getCachedBundleId()
1516
1636
 
1517
1637
  guard let validFileUrl = fileUrl else {
1518
1638
  NSLog("[BundleStorage] fileUrl is nil, resetting bundle URL.")
@@ -1552,7 +1672,10 @@ class BundleFileStorageService: BundleStorageService {
1552
1672
  let finalBundleDir = (storeDir as NSString).appendingPathComponent(bundleId)
1553
1673
 
1554
1674
  if self.fileSystem.fileExists(atPath: finalBundleDir) {
1555
- let findResult = self.findBundleFile(in: finalBundleDir)
1675
+ let findResult = self.findBundleFile(
1676
+ in: finalBundleDir,
1677
+ expectedBundleId: bundleId
1678
+ )
1556
1679
  switch findResult {
1557
1680
  case .success(let existingBundlePath):
1558
1681
  if let bundlePath = existingBundlePath {
@@ -1670,7 +1793,7 @@ class BundleFileStorageService: BundleStorageService {
1670
1793
  return
1671
1794
  }
1672
1795
 
1673
- let currentBundleId = self.getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent
1796
+ let currentBundleId = self.getCachedBundleId()
1674
1797
  let currentBundleDir = currentBundleId.map {
1675
1798
  (storeDir as NSString).appendingPathComponent($0)
1676
1799
  }
@@ -1969,7 +2092,7 @@ class BundleFileStorageService: BundleStorageService {
1969
2092
  let manifestDestination = (tmpDir as NSString).appendingPathComponent("manifest.json")
1970
2093
  try writeManifestFile(targetManifest, to: manifestDestination)
1971
2094
 
1972
- switch self.findBundleFile(in: tmpDir) {
2095
+ switch self.findBundleFile(in: tmpDir, expectedBundleId: bundleId) {
1973
2096
  case .success(let maybeBundlePath):
1974
2097
  guard let bundlePathInTmp = maybeBundlePath else {
1975
2098
  throw BundleStorageError.invalidBundle
@@ -1985,8 +2108,10 @@ class BundleFileStorageService: BundleStorageService {
1985
2108
  throw BundleStorageError.moveOperationFailed(error)
1986
2109
  }
1987
2110
 
1988
- let finalBundlePath = (realDir as NSString).appendingPathComponent(
1989
- (bundlePathInTmp as NSString).lastPathComponent
2111
+ let finalBundlePath = try resolveBundlePathAfterMove(
2112
+ bundlePathInTmp,
2113
+ from: tmpDir,
2114
+ to: realDir
1990
2115
  )
1991
2116
  switch self.setBundleURL(localPath: finalBundlePath) {
1992
2117
  case .success:
@@ -2199,7 +2324,7 @@ class BundleFileStorageService: BundleStorageService {
2199
2324
  progressHandler: @escaping (UpdateProgressPayload) -> Void,
2200
2325
  completion: @escaping (Result<Bool, Error>) -> Void
2201
2326
  ) {
2202
- let currentBundleId = self.getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent
2327
+ let currentBundleId = self.getCachedBundleId()
2203
2328
  NSLog("[BundleStorage] Processing downloaded file atPath: \(location.path)")
2204
2329
 
2205
2330
  // 1) Ensure the bundle file exists
@@ -2289,7 +2414,7 @@ class BundleFileStorageService: BundleStorageService {
2289
2414
  progressHandler: progressHandler,
2290
2415
  progress: UpdateProgress.bundleValidation
2291
2416
  )
2292
- switch self.findBundleFile(in: tmpDir) {
2417
+ switch self.findBundleFile(in: tmpDir, expectedBundleId: bundleId) {
2293
2418
  case .success(let maybeBundlePath):
2294
2419
  if let bundlePathInTmp = maybeBundlePath {
2295
2420
  NSLog("[BundleStorage] Found valid bundle in tmpDir: \(bundlePathInTmp)")
@@ -2315,7 +2440,11 @@ class BundleFileStorageService: BundleStorageService {
2315
2440
  }
2316
2441
 
2317
2442
  // 11) Construct final bundlePath for preferences
2318
- let finalBundlePath = (realDir as NSString).appendingPathComponent((bundlePathInTmp as NSString).lastPathComponent)
2443
+ let finalBundlePath = try resolveBundlePathAfterMove(
2444
+ bundlePathInTmp,
2445
+ from: tmpDir,
2446
+ to: realDir
2447
+ )
2319
2448
 
2320
2449
  // 12) Set the bundle URL in preferences (for backwards compatibility)
2321
2450
  let setResult = self.setBundleURL(localPath: finalBundlePath)
@@ -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.4";
7
+ const HOT_UPDATER_SDK_VERSION = exports.HOT_UPDATER_SDK_VERSION = "0.36.6";
8
8
  //# sourceMappingURL=sdkVersion.js.map
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
 
3
- export const HOT_UPDATER_SDK_VERSION = "0.36.4";
3
+ export const HOT_UPDATER_SDK_VERSION = "0.36.6";
4
4
  //# sourceMappingURL=sdkVersion.js.map
@@ -1,2 +1,2 @@
1
- export declare const HOT_UPDATER_SDK_VERSION = "0.36.4";
1
+ export declare const HOT_UPDATER_SDK_VERSION = "0.36.6";
2
2
  //# sourceMappingURL=sdkVersion.d.ts.map
@@ -1,2 +1,2 @@
1
- export declare const HOT_UPDATER_SDK_VERSION = "0.36.4";
1
+ export declare const HOT_UPDATER_SDK_VERSION = "0.36.6";
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.4",
3
+ "version": "0.36.6",
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.4"
134
+ "hot-updater": "0.36.6"
135
135
  },
136
136
  "dependencies": {
137
137
  "use-sync-external-store": "1.5.0",
138
- "@hot-updater/cli-tools": "0.36.4",
139
- "@hot-updater/core": "0.36.4",
140
- "@hot-updater/js": "0.36.4",
141
- "@hot-updater/plugin-core": "0.36.4"
138
+ "@hot-updater/cli-tools": "0.36.6",
139
+ "@hot-updater/core": "0.36.6",
140
+ "@hot-updater/js": "0.36.6",
141
+ "@hot-updater/plugin-core": "0.36.6"
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.4";
1
+ export const HOT_UPDATER_SDK_VERSION = "0.36.6";