@hot-updater/react-native 0.20.15 → 0.21.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.
Files changed (56) hide show
  1. package/HotUpdater.podspec +7 -4
  2. package/android/build.gradle +3 -0
  3. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +78 -21
  4. package/android/src/main/java/com/hotupdater/DecompressService.kt +83 -0
  5. package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +26 -0
  6. package/android/src/main/java/com/hotupdater/HashUtils.kt +47 -0
  7. package/android/src/main/java/com/hotupdater/HotUpdater.kt +3 -0
  8. package/android/src/main/java/com/hotupdater/HotUpdaterFactory.kt +3 -3
  9. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +3 -1
  10. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +293 -0
  11. package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +105 -0
  12. package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +117 -0
  13. package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +175 -0
  14. package/android/src/newarch/HotUpdaterModule.kt +2 -0
  15. package/android/src/oldarch/HotUpdaterModule.kt +2 -0
  16. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +133 -60
  17. package/ios/HotUpdater/Internal/DecompressService.swift +89 -0
  18. package/ios/HotUpdater/Internal/DecompressionStrategy.swift +22 -0
  19. package/ios/HotUpdater/Internal/HashUtils.swift +63 -0
  20. package/ios/HotUpdater/Internal/HotUpdater.mm +5 -2
  21. package/ios/HotUpdater/Internal/HotUpdaterFactory.swift +4 -4
  22. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +23 -10
  23. package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +229 -0
  24. package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +177 -0
  25. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +73 -7
  26. package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +165 -0
  27. package/lib/commonjs/checkForUpdate.js +1 -0
  28. package/lib/commonjs/checkForUpdate.js.map +1 -1
  29. package/lib/commonjs/fetchUpdateInfo.js +1 -1
  30. package/lib/commonjs/fetchUpdateInfo.js.map +1 -1
  31. package/lib/commonjs/native.js +3 -1
  32. package/lib/commonjs/native.js.map +1 -1
  33. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  34. package/lib/module/checkForUpdate.js +1 -0
  35. package/lib/module/checkForUpdate.js.map +1 -1
  36. package/lib/module/fetchUpdateInfo.js +1 -1
  37. package/lib/module/fetchUpdateInfo.js.map +1 -1
  38. package/lib/module/native.js +3 -1
  39. package/lib/module/native.js.map +1 -1
  40. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  41. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  42. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +5 -0
  44. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  45. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  46. package/lib/typescript/module/native.d.ts.map +1 -1
  47. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +5 -0
  48. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  49. package/package.json +5 -5
  50. package/src/checkForUpdate.ts +1 -0
  51. package/src/fetchUpdateInfo.ts +1 -1
  52. package/src/native.ts +6 -0
  53. package/src/specs/NativeHotUpdater.ts +5 -0
  54. package/android/src/main/java/com/hotupdater/HttpDownloadService.kt +0 -98
  55. package/android/src/main/java/com/hotupdater/ZipFileUnzipService.kt +0 -74
  56. package/ios/HotUpdater/Internal/SSZipArchiveUnzipService.swift +0 -25
@@ -42,12 +42,14 @@ class HotUpdaterModule internal constructor(
42
42
  try {
43
43
  val bundleId = params.getString("bundleId")!!
44
44
  val fileUrl = params.getString("fileUrl")
45
+ val fileHash = params.getString("fileHash")
45
46
 
46
47
  val isSuccess =
47
48
  HotUpdater.updateBundle(
48
49
  mReactApplicationContext,
49
50
  bundleId,
50
51
  fileUrl,
52
+ fileHash,
51
53
  ) { progress ->
52
54
  val progressParams =
53
55
  WritableNativeMap().apply {
@@ -6,6 +6,9 @@ public enum BundleStorageError: Error {
6
6
  case downloadFailed(Error)
7
7
  case extractionFailed(Error)
8
8
  case invalidBundle
9
+ case invalidZipFile
10
+ case insufficientDiskSpace
11
+ case hashMismatch
9
12
  case moveOperationFailed(Error)
10
13
  case copyOperationFailed(Error)
11
14
  case fileSystemError(Error)
@@ -26,30 +29,30 @@ public protocol BundleStorageService {
26
29
  func getBundleURL() -> URL?
27
30
 
28
31
  // Bundle update
29
- func updateBundle(bundleId: String, fileUrl: URL?, completion: @escaping (Result<Bool, Error>) -> Void)
32
+ func updateBundle(bundleId: String, fileUrl: URL?, fileHash: String?, progressHandler: @escaping (Double) -> Void, completion: @escaping (Result<Bool, Error>) -> Void)
30
33
  }
31
34
 
32
35
  class BundleFileStorageService: BundleStorageService {
33
36
  private let fileSystem: FileSystemService
34
37
  private let downloadService: DownloadService
35
- private let unzipService: UnzipService
38
+ private let decompressService: DecompressService
36
39
  private let preferences: PreferencesService
37
-
40
+
38
41
  // Queue for potentially long-running sequences within updateBundle or for explicit background tasks.
39
42
  private let fileOperationQueue: DispatchQueue
40
-
43
+
41
44
  private var activeTasks: [URLSessionTask] = []
42
-
45
+
43
46
  public init(fileSystem: FileSystemService,
44
47
  downloadService: DownloadService,
45
- unzipService: UnzipService,
48
+ decompressService: DecompressService,
46
49
  preferences: PreferencesService) {
47
-
50
+
48
51
  self.fileSystem = fileSystem
49
52
  self.downloadService = downloadService
50
- self.unzipService = unzipService
53
+ self.decompressService = decompressService
51
54
  self.preferences = preferences
52
-
55
+
53
56
  self.fileOperationQueue = DispatchQueue(label: "com.hotupdater.fileoperations",
54
57
  qos: .utility,
55
58
  attributes: .concurrent)
@@ -273,9 +276,11 @@ class BundleFileStorageService: BundleStorageService {
273
276
  * Updates the bundle from the specified URL. This operation is asynchronous.
274
277
  * @param bundleId ID of the bundle to update
275
278
  * @param fileUrl URL of the bundle file to download (or nil to reset)
279
+ * @param fileHash SHA256 hash of the bundle file for verification (nullable)
280
+ * @param progressHandler Callback for download and extraction progress (0.0 to 1.0)
276
281
  * @param completion Callback with result of the operation
277
282
  */
278
- func updateBundle(bundleId: String, fileUrl: URL?, completion: @escaping (Result<Bool, Error>) -> Void) {
283
+ func updateBundle(bundleId: String, fileUrl: URL?, fileHash: String?, progressHandler: @escaping (Double) -> Void, completion: @escaping (Result<Bool, Error>) -> Void) {
279
284
  // Get the current bundle ID from the cached bundle URL (exclude fallback bundles)
280
285
  let currentBundleId = self.getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent
281
286
 
@@ -344,7 +349,7 @@ class BundleFileStorageService: BundleStorageService {
344
349
  do {
345
350
  try self.fileSystem.removeItem(atPath: finalBundleDir)
346
351
  // Continue with download process on success
347
- self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, storeDir: storeDir, completion: completion)
352
+ self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, fileHash: fileHash, storeDir: storeDir, progressHandler: progressHandler, completion: completion)
348
353
  } catch let error {
349
354
  NSLog("[BundleStorage] Failed to remove invalid bundle dir: \(error.localizedDescription)")
350
355
  completion(.failure(BundleStorageError.fileSystemError(error)))
@@ -354,7 +359,7 @@ class BundleFileStorageService: BundleStorageService {
354
359
  completion(.failure(error))
355
360
  }
356
361
  } else {
357
- self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, storeDir: storeDir, completion: completion)
362
+ self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, fileHash: fileHash, storeDir: storeDir, progressHandler: progressHandler, completion: completion)
358
363
  }
359
364
  }
360
365
  }
@@ -364,13 +369,17 @@ class BundleFileStorageService: BundleStorageService {
364
369
  * This method is part of the asynchronous `updateBundle` flow.
365
370
  * @param bundleId ID of the bundle to update
366
371
  * @param fileUrl URL of the bundle file to download
372
+ * @param fileHash SHA256 hash of the bundle file for verification (nullable)
367
373
  * @param storeDir Path to the bundle-store directory
374
+ * @param progressHandler Callback for download and extraction progress
368
375
  * @param completion Callback with result of the operation
369
376
  */
370
377
  private func prepareAndDownloadBundle(
371
378
  bundleId: String,
372
379
  fileUrl: URL,
380
+ fileHash: String?,
373
381
  storeDir: String,
382
+ progressHandler: @escaping (Double) -> Void,
374
383
  completion: @escaping (Result<Bool, Error>) -> Void
375
384
  ) {
376
385
  // 1) Prepare temp directory for download
@@ -388,36 +397,70 @@ class BundleFileStorageService: BundleStorageService {
388
397
  completion(.failure(BundleStorageError.directoryCreationFailed))
389
398
  return
390
399
  }
391
-
392
- // 4) Define paths for ZIP file
393
- let tempZipFile = (tempDirectory as NSString).appendingPathComponent("bundle.zip")
394
-
395
- NSLog("[BundleStorage] Starting download from \(fileUrl)")
396
-
397
- // 5) DownloadService handles its own threading for the download task.
398
- // The completion handler for downloadService.downloadFile is then dispatched to fileOperationQueue.
399
- let task = self.downloadService.downloadFile(from: fileUrl,
400
- to: tempZipFile,
401
- progressHandler: { _ in
402
- // Progress updates handled by notification system
403
- },
404
- completion: { [weak self] result in
400
+
401
+ // 4) Determine bundle filename from URL
402
+ let bundleFileName = fileUrl.lastPathComponent.isEmpty ? "bundle.zip" : fileUrl.lastPathComponent
403
+ let tempBundleFile = (tempDirectory as NSString).appendingPathComponent(bundleFileName)
404
+
405
+ NSLog("[BundleStorage] Checking file size and disk space...")
406
+
407
+ // 5) Check file size and disk space before download
408
+ self.downloadService.getFileSize(from: fileUrl) { [weak self] sizeResult in
409
+ guard let self = self else { return }
410
+
411
+ if case .success(let fileSize) = sizeResult {
412
+ // Check available disk space
413
+ do {
414
+ let attributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory())
415
+ if let freeSize = attributes[.systemFreeSize] as? Int64 {
416
+ let requiredSpace = fileSize * 2 // ZIP + extracted files
417
+
418
+ NSLog("[BundleStorage] File size: \(fileSize) bytes, Available: \(freeSize) bytes, Required: \(requiredSpace) bytes")
419
+
420
+ if freeSize < requiredSpace {
421
+ NSLog("[BundleStorage] Insufficient disk space")
422
+ self.cleanupTemporaryFiles([tempDirectory])
423
+ completion(.failure(BundleStorageError.insufficientDiskSpace))
424
+ return
425
+ }
426
+ }
427
+ } catch {
428
+ NSLog("[BundleStorage] Failed to check disk space: \(error.localizedDescription)")
429
+ // Continue with download despite disk check failure
430
+ }
431
+ } else {
432
+ NSLog("[BundleStorage] Unable to determine file size, proceeding with download")
433
+ }
434
+
435
+ NSLog("[BundleStorage] Starting download from \(fileUrl)")
436
+
437
+ // 6) DownloadService handles its own threading for the download task.
438
+ // The completion handler for downloadService.downloadFile is then dispatched to fileOperationQueue.
439
+ let task = self.downloadService.downloadFile(from: fileUrl,
440
+ to: tempBundleFile,
441
+ progressHandler: { downloadProgress in
442
+ // Map download progress to 0.0 - 0.8
443
+ progressHandler(downloadProgress * 0.8)
444
+ },
445
+ completion: { [weak self] result in
405
446
  guard let self = self else {
406
447
  let error = NSError(domain: "HotUpdaterError", code: 998,
407
448
  userInfo: [NSLocalizedDescriptionKey: "Self deallocated during download"])
408
449
  completion(.failure(error))
409
450
  return
410
451
  }
411
-
452
+
412
453
  // Dispatch the processing of the downloaded file to the file operation queue
413
454
  let workItem = DispatchWorkItem {
414
455
  switch result {
415
456
  case .success(let location):
416
457
  self.processDownloadedFileWithTmp(location: location,
417
- tempZipFile: tempZipFile,
458
+ tempBundleFile: tempBundleFile,
459
+ fileHash: fileHash,
418
460
  storeDir: storeDir,
419
461
  bundleId: bundleId,
420
462
  tempDirectory: tempDirectory,
463
+ progressHandler: progressHandler,
421
464
  completion: completion)
422
465
  case .failure(let error):
423
466
  NSLog("[BundleStorage] Download failed: \(error.localizedDescription)")
@@ -427,9 +470,10 @@ class BundleFileStorageService: BundleStorageService {
427
470
  }
428
471
  self.fileOperationQueue.async(execute: workItem)
429
472
  })
430
-
431
- if let task = task {
432
- self.activeTasks.append(task) // Manage active tasks
473
+
474
+ if let task = task {
475
+ self.activeTasks.append(task) // Manage active tasks
476
+ }
433
477
  }
434
478
  }
435
479
 
@@ -437,24 +481,28 @@ class BundleFileStorageService: BundleStorageService {
437
481
  * Processes a downloaded bundle file using the "tmp" rename approach.
438
482
  * This method is part of the asynchronous `updateBundle` flow and is expected to run on a background thread.
439
483
  * @param location URL of the downloaded file
440
- * @param tempZipFile Path to store the downloaded zip file
484
+ * @param tempBundleFile Path to store the downloaded bundle file
485
+ * @param fileHash SHA256 hash of the bundle file for verification (nullable)
441
486
  * @param storeDir Path to the bundle-store directory
442
487
  * @param bundleId ID of the bundle being processed
443
488
  * @param tempDirectory Temporary directory for processing
489
+ * @param progressHandler Callback for extraction progress (0.8 to 1.0)
444
490
  * @param completion Callback with result of the operation
445
491
  */
446
492
  private func processDownloadedFileWithTmp(
447
493
  location: URL,
448
- tempZipFile: String,
494
+ tempBundleFile: String,
495
+ fileHash: String?,
449
496
  storeDir: String,
450
497
  bundleId: String,
451
498
  tempDirectory: String,
499
+ progressHandler: @escaping (Double) -> Void,
452
500
  completion: @escaping (Result<Bool, Error>) -> Void
453
501
  ) {
454
502
  let currentBundleId = self.getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent
455
503
  NSLog("[BundleStorage] Processing downloaded file atPath: \(location.path)")
456
-
457
- // 1) Ensure the ZIP file exists
504
+
505
+ // 1) Ensure the bundle file exists
458
506
  guard self.fileSystem.fileExists(atPath: location.path) else {
459
507
  self.cleanupTemporaryFiles([tempDirectory])
460
508
  completion(.failure(BundleStorageError.fileSystemError(NSError(
@@ -464,58 +512,83 @@ class BundleFileStorageService: BundleStorageService {
464
512
  ))))
465
513
  return
466
514
  }
467
-
515
+
468
516
  // 2) Define tmpDir and realDir
469
517
  let tmpDir = (storeDir as NSString).appendingPathComponent("\(bundleId).tmp")
470
518
  let realDir = (storeDir as NSString).appendingPathComponent(bundleId)
471
-
519
+
472
520
  do {
473
521
  // 3) Remove any existing tmpDir
474
522
  if self.fileSystem.fileExists(atPath: tmpDir) {
475
523
  try self.fileSystem.removeItem(atPath: tmpDir)
476
524
  NSLog("[BundleStorage] Removed existing tmpDir: \(tmpDir)")
477
525
  }
478
-
526
+
479
527
  // 4) Create tmpDir
480
528
  try self.fileSystem.createDirectory(atPath: tmpDir)
481
529
  NSLog("[BundleStorage] Created tmpDir: \(tmpDir)")
482
-
483
- // 5) Unzip directly into tmpDir
484
- NSLog("[BundleStorage] Unzipping \(tempZipFile) \(tmpDir)")
485
- try self.unzipService.unzip(file: tempZipFile, to: tmpDir)
486
- NSLog("[BundleStorage] Unzip complete at \(tmpDir)")
487
-
488
- // 6) Remove the downloaded ZIP file
489
- try? self.fileSystem.removeItem(atPath: tempZipFile)
490
-
491
- // 7) Verify that a valid bundle file exists inside tmpDir
530
+
531
+ // 5) Verify file hash if provided
532
+ if let expectedHash = fileHash {
533
+ NSLog("[BundleStorage] Verifying file hash...")
534
+ let tempBundleURL = URL(fileURLWithPath: tempBundleFile)
535
+ guard HashUtils.verifyHash(fileURL: tempBundleURL, expectedHash: expectedHash) else {
536
+ NSLog("[BundleStorage] Hash mismatch!")
537
+ try? self.fileSystem.removeItem(atPath: tmpDir)
538
+ self.cleanupTemporaryFiles([tempDirectory])
539
+ completion(.failure(BundleStorageError.hashMismatch))
540
+ return
541
+ }
542
+ NSLog("[BundleStorage] Hash verification passed")
543
+ }
544
+
545
+ // 6) Unzip directly into tmpDir with progress tracking (0.8 - 1.0)
546
+ NSLog("[BundleStorage] Extracting \(tempBundleFile) → \(tmpDir)")
547
+ do {
548
+ try self.decompressService.unzip(file: tempBundleFile, to: tmpDir, progressHandler: { unzipProgress in
549
+ // Map unzip progress (0.0 - 1.0) to overall progress (0.8 - 1.0)
550
+ progressHandler(0.8 + (unzipProgress * 0.2))
551
+ })
552
+ NSLog("[BundleStorage] Extraction complete at \(tmpDir)")
553
+ } catch {
554
+ NSLog("[BundleStorage] Extraction failed: \(error.localizedDescription)")
555
+ try? self.fileSystem.removeItem(atPath: tmpDir)
556
+ self.cleanupTemporaryFiles([tempDirectory])
557
+ completion(.failure(BundleStorageError.extractionFailed(error)))
558
+ return
559
+ }
560
+
561
+ // 7) Remove the downloaded bundle file
562
+ try? self.fileSystem.removeItem(atPath: tempBundleFile)
563
+
564
+ // 8) Verify that a valid bundle file exists inside tmpDir
492
565
  switch self.findBundleFile(in: tmpDir) {
493
566
  case .success(let maybeBundlePath):
494
567
  if let bundlePathInTmp = maybeBundlePath {
495
- // 8) Remove any existing realDir
568
+ // 9) Remove any existing realDir
496
569
  if self.fileSystem.fileExists(atPath: realDir) {
497
570
  try self.fileSystem.removeItem(atPath: realDir)
498
571
  NSLog("[BundleStorage] Removed existing realDir: \(realDir)")
499
572
  }
500
-
501
- // 9) Rename (move) tmpDir → realDir
573
+
574
+ // 10) Rename (move) tmpDir → realDir
502
575
  try self.fileSystem.moveItem(atPath: tmpDir, toPath: realDir)
503
576
  NSLog("[BundleStorage] Renamed tmpDir to realDir: \(realDir)")
504
-
505
- // 10) Construct final bundlePath for preferences
577
+
578
+ // 11) Construct final bundlePath for preferences
506
579
  let finalBundlePath = (realDir as NSString).appendingPathComponent((bundlePathInTmp as NSString).lastPathComponent)
507
-
508
- // 11) Set the bundle URL in preferences
580
+
581
+ // 12) Set the bundle URL in preferences
509
582
  let setResult = self.setBundleURL(localPath: finalBundlePath)
510
583
  switch setResult {
511
584
  case .success:
512
- // 12) Clean up the temporary directory
585
+ // 13) Clean up the temporary directory
513
586
  self.cleanupTemporaryFiles([tempDirectory])
514
-
515
- // 13) Clean up old bundles, preserving current and latest
587
+
588
+ // 14) Clean up old bundles, preserving current and latest
516
589
  let _ = self.cleanupOldBundles(currentBundleId: currentBundleId, bundleId: bundleId)
517
-
518
- // 14) Complete with success
590
+
591
+ // 15) Complete with success
519
592
  completion(.success(true))
520
593
  case .failure(let err):
521
594
  // Preferences save failed → remove realDir and clean up
@@ -0,0 +1,89 @@
1
+ import Foundation
2
+
3
+ /**
4
+ * Unified decompression service that uses Strategy pattern to handle multiple compression formats.
5
+ * Automatically detects format by trying each strategy's validation and delegates to appropriate decompression strategy.
6
+ */
7
+ class DecompressService {
8
+ /// Array of available strategies in order of detection priority
9
+ private let strategies: [DecompressionStrategy]
10
+
11
+ init() {
12
+ // Order matters: Try ZIP first (clear magic bytes), then TAR.GZ (GZIP magic bytes), then TAR.BR (fallback)
13
+ self.strategies = [
14
+ ZipDecompressionStrategy(),
15
+ TarGzDecompressionStrategy(),
16
+ TarBrDecompressionStrategy()
17
+ ]
18
+ }
19
+
20
+ /**
21
+ * Extracts a compressed file to the destination directory.
22
+ * Automatically detects compression format by trying each strategy's validation.
23
+ * @param file Path to the compressed file
24
+ * @param destination Path to the destination directory
25
+ * @param progressHandler Callback for progress updates (0.0 - 1.0)
26
+ * @throws Error if decompression fails or no valid strategy found
27
+ */
28
+ func unzip(file: String, to destination: String, progressHandler: @escaping (Double) -> Void) throws {
29
+ // Collect file information for better error messages
30
+ let fileURL = URL(fileURLWithPath: file)
31
+ let fileName = fileURL.lastPathComponent
32
+ let fileSize = (try? FileManager.default.attributesOfItem(atPath: file)[.size] as? UInt64) ?? 0
33
+
34
+ // Try each strategy's validation
35
+ for strategy in strategies {
36
+ if strategy.isValid(file: file) {
37
+ NSLog("[DecompressService] Using strategy for \(fileName)")
38
+ try strategy.decompress(file: file, to: destination, progressHandler: progressHandler)
39
+ return
40
+ }
41
+ }
42
+
43
+ // No valid strategy found - provide detailed error message
44
+ let errorMessage = """
45
+ Failed to decompress file: \(fileName) (\(fileSize) bytes)
46
+
47
+ Tried strategies: ZIP (magic bytes 0x504B0304), TAR.GZ (magic bytes 0x1F8B), TAR.BR (file extension)
48
+
49
+ Supported formats:
50
+ - ZIP archives (.zip)
51
+ - GZIP compressed TAR archives (.tar.gz)
52
+ - Brotli compressed TAR archives (.tar.br)
53
+
54
+ Please verify the file is not corrupted and matches one of the supported formats.
55
+ """
56
+
57
+ NSLog("[DecompressService] \(errorMessage)")
58
+ throw NSError(
59
+ domain: "DecompressService",
60
+ code: 1,
61
+ userInfo: [NSLocalizedDescriptionKey: errorMessage]
62
+ )
63
+ }
64
+
65
+ /**
66
+ * Extracts a compressed file to the destination directory (without progress tracking).
67
+ * @param file Path to the compressed file
68
+ * @param destination Path to the destination directory
69
+ * @throws Error if decompression fails or no valid strategy found
70
+ */
71
+ func unzip(file: String, to destination: String) throws {
72
+ try unzip(file: file, to: destination, progressHandler: { _ in })
73
+ }
74
+
75
+ /**
76
+ * Validates if a file is a valid compressed archive.
77
+ * @param file Path to the file to validate
78
+ * @return true if the file is valid for any strategy
79
+ */
80
+ func isValid(file: String) -> Bool {
81
+ for strategy in strategies {
82
+ if strategy.isValid(file: file) {
83
+ return true
84
+ }
85
+ }
86
+ NSLog("[DecompressService] No valid strategy found for file: \(file)")
87
+ return false
88
+ }
89
+ }
@@ -0,0 +1,22 @@
1
+ import Foundation
2
+
3
+ /**
4
+ * Protocol for decompression strategies
5
+ */
6
+ protocol DecompressionStrategy {
7
+ /**
8
+ * Validates if a file can be decompressed by this strategy
9
+ * @param file Path to the file to validate
10
+ * @return true if the file is valid for this strategy
11
+ */
12
+ func isValid(file: String) -> Bool
13
+
14
+ /**
15
+ * Decompresses a file to the destination directory
16
+ * @param file Path to the compressed file
17
+ * @param destination Path to the destination directory
18
+ * @param progressHandler Callback for progress updates (0.0 - 1.0)
19
+ * @throws Error if decompression fails
20
+ */
21
+ func decompress(file: String, to destination: String, progressHandler: @escaping (Double) -> Void) throws
22
+ }
@@ -0,0 +1,63 @@
1
+ import Foundation
2
+ import CryptoKit
3
+
4
+ /**
5
+ * Utility class for file hash operations
6
+ */
7
+ class HashUtils {
8
+ /// Buffer size for file reading operations (64KB for optimal I/O performance)
9
+ private static let BUFFER_SIZE = 65536
10
+
11
+ /**
12
+ * Calculates SHA256 hash of a file
13
+ * @param fileURL URL of the file to hash
14
+ * @return Hex string of the hash (lowercase), or nil if error occurs
15
+ */
16
+ static func calculateSHA256(fileURL: URL) -> String? {
17
+ guard let fileHandle = try? FileHandle(forReadingFrom: fileURL) else {
18
+ NSLog("[HashUtils] Failed to open file: \(fileURL.path)")
19
+ return nil
20
+ }
21
+
22
+ defer {
23
+ try? fileHandle.close()
24
+ }
25
+
26
+ var hasher = SHA256()
27
+
28
+ // Read file in chunks with autoreleasepool for memory efficiency
29
+ while autoreleasepool(invoking: {
30
+ let data = fileHandle.readData(ofLength: BUFFER_SIZE)
31
+ if data.count > 0 {
32
+ hasher.update(data: data)
33
+ return true
34
+ } else {
35
+ return false
36
+ }
37
+ }) { }
38
+
39
+ let digest = hasher.finalize()
40
+ return digest.map { String(format: "%02x", $0) }.joined()
41
+ }
42
+
43
+ /**
44
+ * Verifies file hash
45
+ * @param fileURL URL of the file to verify
46
+ * @param expectedHash Expected SHA256 hash (hex string, case-insensitive)
47
+ * @return true if hash matches, false otherwise
48
+ */
49
+ static func verifyHash(fileURL: URL, expectedHash: String) -> Bool {
50
+ guard let actualHash = calculateSHA256(fileURL: fileURL) else {
51
+ NSLog("[HashUtils] Failed to calculate hash")
52
+ return false
53
+ }
54
+
55
+ let matches = actualHash.caseInsensitiveCompare(expectedHash) == .orderedSame
56
+
57
+ if !matches {
58
+ NSLog("[HashUtils] Hash mismatch - Expected: \(expectedHash), Actual: \(actualHash)")
59
+ }
60
+
61
+ return matches
62
+ }
63
+ }
@@ -31,7 +31,7 @@ static HotUpdaterImpl *_hotUpdaterImpl = [HotUpdaterFactory.shared create];
31
31
  self = [super init];
32
32
  if (self) {
33
33
  observedTasks = [NSMutableSet set];
34
-
34
+
35
35
  // Start observing notifications needed for cleanup/events
36
36
  // Using self as observer
37
37
  [[NSNotificationCenter defaultCenter] addObserver:self
@@ -217,7 +217,10 @@ RCT_EXPORT_METHOD(updateBundle:(JS::NativeHotUpdater::UpdateBundleParams &)param
217
217
  if (params.fileUrl()) {
218
218
  paramDict[@"fileUrl"] = params.fileUrl();
219
219
  }
220
-
220
+ if (params.fileHash()) {
221
+ paramDict[@"fileHash"] = params.fileHash();
222
+ }
223
+
221
224
  [_hotUpdaterImpl updateBundle:paramDict resolver:resolve rejecter:reject];
222
225
  }
223
226
  #else
@@ -10,15 +10,15 @@ public class HotUpdaterFactory: NSObject {
10
10
  let fileSystem = FileManagerService()
11
11
  let preferences = VersionedPreferencesService()
12
12
  let downloadService = URLSessionDownloadService()
13
- let unzipService = SSZipArchiveUnzipService()
14
-
13
+ let decompressService = DecompressService()
14
+
15
15
  let bundleStorage = BundleFileStorageService(
16
16
  fileSystem: fileSystem,
17
17
  downloadService: downloadService,
18
- unzipService: unzipService,
18
+ decompressService: decompressService,
19
19
  preferences: preferences
20
20
  )
21
-
21
+
22
22
  return HotUpdaterImpl(bundleStorage: bundleStorage, preferences: preferences)
23
23
  }
24
24
  }
@@ -16,15 +16,15 @@ import React
16
16
  let fileSystem = FileManagerService()
17
17
  let preferences = VersionedPreferencesService()
18
18
  let downloadService = URLSessionDownloadService()
19
- let unzipService = SSZipArchiveUnzipService()
20
-
19
+ let decompressService = DecompressService()
20
+
21
21
  let bundleStorage = BundleFileStorageService(
22
22
  fileSystem: fileSystem,
23
23
  downloadService: downloadService,
24
- unzipService: unzipService,
24
+ decompressService: decompressService,
25
25
  preferences: preferences
26
26
  )
27
-
27
+
28
28
  self.init(bundleStorage: bundleStorage, preferences: preferences)
29
29
  }
30
30
 
@@ -132,20 +132,33 @@ import React
132
132
  }
133
133
 
134
134
  let fileUrlString = data["fileUrl"] as? String ?? ""
135
-
135
+
136
136
  var fileUrl: URL? = nil
137
137
  if !fileUrlString.isEmpty {
138
138
  guard let url = URL(string: fileUrlString) else {
139
- throw NSError(domain: "HotUpdaterError", code: 103,
139
+ throw NSError(domain: "HotUpdaterError", code: 103,
140
140
  userInfo: [NSLocalizedDescriptionKey: "Invalid 'fileUrl' provided: \(fileUrlString)"])
141
141
  }
142
142
  fileUrl = url
143
143
  }
144
-
145
- NSLog("[HotUpdaterImpl] updateBundle called with bundleId: \(bundleId), fileUrl: \(fileUrl?.absoluteString ?? "nil")")
146
-
144
+
145
+ // Extract fileHash if provided
146
+ let fileHash = data["fileHash"] as? String
147
+
148
+ // Extract progress callback if provided
149
+ let progressCallback = data["progressCallback"] as? RCTResponseSenderBlock
150
+
151
+ NSLog("[HotUpdaterImpl] updateBundle called with bundleId: \(bundleId), fileUrl: \(fileUrl?.absoluteString ?? "nil"), fileHash: \(fileHash ?? "nil")")
152
+
147
153
  // Heavy work is delegated to bundle storage service with safe error handling
148
- bundleStorage.updateBundle(bundleId: bundleId, fileUrl: fileUrl) { [weak self] result in
154
+ bundleStorage.updateBundle(bundleId: bundleId, fileUrl: fileUrl, fileHash: fileHash, progressHandler: { progress in
155
+ // Call JS progress callback if provided
156
+ if let callback = progressCallback {
157
+ DispatchQueue.main.async {
158
+ callback([progress])
159
+ }
160
+ }
161
+ }) { [weak self] result in
149
162
  guard self != nil else {
150
163
  let error = NSError(domain: "HotUpdaterError", code: 998,
151
164
  userInfo: [NSLocalizedDescriptionKey: "Self deallocated during update"])