@swiftpatch/react-native 2.0.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 (182) hide show
  1. package/README.md +430 -0
  2. package/android/build.gradle +105 -0
  3. package/android/src/main/AndroidManifest.xml +6 -0
  4. package/android/src/main/java/com/swiftpatch/BundleManager.kt +107 -0
  5. package/android/src/main/java/com/swiftpatch/CrashDetector.kt +79 -0
  6. package/android/src/main/java/com/swiftpatch/CryptoVerifier.kt +69 -0
  7. package/android/src/main/java/com/swiftpatch/DownloadManager.kt +120 -0
  8. package/android/src/main/java/com/swiftpatch/EventQueue.kt +86 -0
  9. package/android/src/main/java/com/swiftpatch/FileUtils.kt +60 -0
  10. package/android/src/main/java/com/swiftpatch/PatchApplier.kt +60 -0
  11. package/android/src/main/java/com/swiftpatch/SignalCrashHandler.kt +84 -0
  12. package/android/src/main/java/com/swiftpatch/SlotManager.kt +299 -0
  13. package/android/src/main/java/com/swiftpatch/SwiftPatchModule.kt +630 -0
  14. package/android/src/main/java/com/swiftpatch/SwiftPatchPackage.kt +21 -0
  15. package/android/src/main/jni/CMakeLists.txt +12 -0
  16. package/android/src/main/jni/bspatch.c +188 -0
  17. package/android/src/main/jni/bspatch.h +57 -0
  18. package/android/src/main/jni/bspatch_jni.c +28 -0
  19. package/ios/Libraries/bspatch/bspatch.c +188 -0
  20. package/ios/Libraries/bspatch/bspatch.h +50 -0
  21. package/ios/Libraries/bspatch/module.modulemap +4 -0
  22. package/ios/SwiftPatch/BundleManager.swift +113 -0
  23. package/ios/SwiftPatch/CrashDetector.swift +71 -0
  24. package/ios/SwiftPatch/CryptoVerifier.swift +70 -0
  25. package/ios/SwiftPatch/DownloadManager.swift +125 -0
  26. package/ios/SwiftPatch/EventQueue.swift +116 -0
  27. package/ios/SwiftPatch/FileUtils.swift +38 -0
  28. package/ios/SwiftPatch/PatchApplier.swift +41 -0
  29. package/ios/SwiftPatch/SignalCrashHandler.swift +129 -0
  30. package/ios/SwiftPatch/SlotManager.swift +360 -0
  31. package/ios/SwiftPatch/SwiftPatchModule.m +56 -0
  32. package/ios/SwiftPatch/SwiftPatchModule.swift +621 -0
  33. package/lib/commonjs/SwiftPatchCore.js +140 -0
  34. package/lib/commonjs/SwiftPatchCore.js.map +1 -0
  35. package/lib/commonjs/SwiftPatchProvider.js +617 -0
  36. package/lib/commonjs/SwiftPatchProvider.js.map +1 -0
  37. package/lib/commonjs/constants.js +50 -0
  38. package/lib/commonjs/constants.js.map +1 -0
  39. package/lib/commonjs/core/Downloader.js +63 -0
  40. package/lib/commonjs/core/Downloader.js.map +1 -0
  41. package/lib/commonjs/core/Installer.js +46 -0
  42. package/lib/commonjs/core/Installer.js.map +1 -0
  43. package/lib/commonjs/core/Rollback.js +36 -0
  44. package/lib/commonjs/core/Rollback.js.map +1 -0
  45. package/lib/commonjs/core/UpdateChecker.js +57 -0
  46. package/lib/commonjs/core/UpdateChecker.js.map +1 -0
  47. package/lib/commonjs/core/Verifier.js +82 -0
  48. package/lib/commonjs/core/Verifier.js.map +1 -0
  49. package/lib/commonjs/core/index.js +41 -0
  50. package/lib/commonjs/core/index.js.map +1 -0
  51. package/lib/commonjs/index.js +154 -0
  52. package/lib/commonjs/index.js.map +1 -0
  53. package/lib/commonjs/modal/SwiftPatchModal.js +667 -0
  54. package/lib/commonjs/modal/SwiftPatchModal.js.map +1 -0
  55. package/lib/commonjs/modal/useSwiftPatchModal.js +26 -0
  56. package/lib/commonjs/modal/useSwiftPatchModal.js.map +1 -0
  57. package/lib/commonjs/native/NativeSwiftPatch.js +85 -0
  58. package/lib/commonjs/native/NativeSwiftPatch.js.map +1 -0
  59. package/lib/commonjs/native/NativeSwiftPatchSpec.js +15 -0
  60. package/lib/commonjs/native/NativeSwiftPatchSpec.js.map +1 -0
  61. package/lib/commonjs/package.json +1 -0
  62. package/lib/commonjs/types.js +126 -0
  63. package/lib/commonjs/types.js.map +1 -0
  64. package/lib/commonjs/useSwiftPatch.js +31 -0
  65. package/lib/commonjs/useSwiftPatch.js.map +1 -0
  66. package/lib/commonjs/utils/api.js +206 -0
  67. package/lib/commonjs/utils/api.js.map +1 -0
  68. package/lib/commonjs/utils/device.js +23 -0
  69. package/lib/commonjs/utils/device.js.map +1 -0
  70. package/lib/commonjs/utils/logger.js +30 -0
  71. package/lib/commonjs/utils/logger.js.map +1 -0
  72. package/lib/commonjs/utils/storage.js +31 -0
  73. package/lib/commonjs/utils/storage.js.map +1 -0
  74. package/lib/commonjs/withSwiftPatch.js +42 -0
  75. package/lib/commonjs/withSwiftPatch.js.map +1 -0
  76. package/lib/module/SwiftPatchCore.js +135 -0
  77. package/lib/module/SwiftPatchCore.js.map +1 -0
  78. package/lib/module/SwiftPatchProvider.js +611 -0
  79. package/lib/module/SwiftPatchProvider.js.map +1 -0
  80. package/lib/module/constants.js +46 -0
  81. package/lib/module/constants.js.map +1 -0
  82. package/lib/module/core/Downloader.js +57 -0
  83. package/lib/module/core/Downloader.js.map +1 -0
  84. package/lib/module/core/Installer.js +41 -0
  85. package/lib/module/core/Installer.js.map +1 -0
  86. package/lib/module/core/Rollback.js +31 -0
  87. package/lib/module/core/Rollback.js.map +1 -0
  88. package/lib/module/core/UpdateChecker.js +51 -0
  89. package/lib/module/core/UpdateChecker.js.map +1 -0
  90. package/lib/module/core/Verifier.js +76 -0
  91. package/lib/module/core/Verifier.js.map +1 -0
  92. package/lib/module/core/index.js +8 -0
  93. package/lib/module/core/index.js.map +1 -0
  94. package/lib/module/index.js +34 -0
  95. package/lib/module/index.js.map +1 -0
  96. package/lib/module/modal/SwiftPatchModal.js +661 -0
  97. package/lib/module/modal/SwiftPatchModal.js.map +1 -0
  98. package/lib/module/modal/useSwiftPatchModal.js +22 -0
  99. package/lib/module/modal/useSwiftPatchModal.js.map +1 -0
  100. package/lib/module/native/NativeSwiftPatch.js +78 -0
  101. package/lib/module/native/NativeSwiftPatch.js.map +1 -0
  102. package/lib/module/native/NativeSwiftPatchSpec.js +12 -0
  103. package/lib/module/native/NativeSwiftPatchSpec.js.map +1 -0
  104. package/lib/module/types.js +139 -0
  105. package/lib/module/types.js.map +1 -0
  106. package/lib/module/useSwiftPatch.js +26 -0
  107. package/lib/module/useSwiftPatch.js.map +1 -0
  108. package/lib/module/utils/api.js +197 -0
  109. package/lib/module/utils/api.js.map +1 -0
  110. package/lib/module/utils/device.js +18 -0
  111. package/lib/module/utils/device.js.map +1 -0
  112. package/lib/module/utils/logger.js +26 -0
  113. package/lib/module/utils/logger.js.map +1 -0
  114. package/lib/module/utils/storage.js +24 -0
  115. package/lib/module/utils/storage.js.map +1 -0
  116. package/lib/module/withSwiftPatch.js +37 -0
  117. package/lib/module/withSwiftPatch.js.map +1 -0
  118. package/lib/typescript/SwiftPatchCore.d.ts +64 -0
  119. package/lib/typescript/SwiftPatchCore.d.ts.map +1 -0
  120. package/lib/typescript/SwiftPatchProvider.d.ts +22 -0
  121. package/lib/typescript/SwiftPatchProvider.d.ts.map +1 -0
  122. package/lib/typescript/constants.d.ts +33 -0
  123. package/lib/typescript/constants.d.ts.map +1 -0
  124. package/lib/typescript/core/Downloader.d.ts +34 -0
  125. package/lib/typescript/core/Downloader.d.ts.map +1 -0
  126. package/lib/typescript/core/Installer.d.ts +25 -0
  127. package/lib/typescript/core/Installer.d.ts.map +1 -0
  128. package/lib/typescript/core/Rollback.d.ts +18 -0
  129. package/lib/typescript/core/Rollback.d.ts.map +1 -0
  130. package/lib/typescript/core/UpdateChecker.d.ts +27 -0
  131. package/lib/typescript/core/UpdateChecker.d.ts.map +1 -0
  132. package/lib/typescript/core/Verifier.d.ts +31 -0
  133. package/lib/typescript/core/Verifier.d.ts.map +1 -0
  134. package/lib/typescript/core/index.d.ts +8 -0
  135. package/lib/typescript/core/index.d.ts.map +1 -0
  136. package/lib/typescript/index.d.ts +13 -0
  137. package/lib/typescript/index.d.ts.map +1 -0
  138. package/lib/typescript/modal/SwiftPatchModal.d.ts +11 -0
  139. package/lib/typescript/modal/SwiftPatchModal.d.ts.map +1 -0
  140. package/lib/typescript/modal/useSwiftPatchModal.d.ts +7 -0
  141. package/lib/typescript/modal/useSwiftPatchModal.d.ts.map +1 -0
  142. package/lib/typescript/native/NativeSwiftPatch.d.ts +61 -0
  143. package/lib/typescript/native/NativeSwiftPatch.d.ts.map +1 -0
  144. package/lib/typescript/native/NativeSwiftPatchSpec.d.ts +67 -0
  145. package/lib/typescript/native/NativeSwiftPatchSpec.d.ts.map +1 -0
  146. package/lib/typescript/types.d.ts +266 -0
  147. package/lib/typescript/types.d.ts.map +1 -0
  148. package/lib/typescript/useSwiftPatch.d.ts +12 -0
  149. package/lib/typescript/useSwiftPatch.d.ts.map +1 -0
  150. package/lib/typescript/utils/api.d.ts +87 -0
  151. package/lib/typescript/utils/api.d.ts.map +1 -0
  152. package/lib/typescript/utils/device.d.ts +9 -0
  153. package/lib/typescript/utils/device.d.ts.map +1 -0
  154. package/lib/typescript/utils/logger.d.ts +8 -0
  155. package/lib/typescript/utils/logger.d.ts.map +1 -0
  156. package/lib/typescript/utils/storage.d.ts +14 -0
  157. package/lib/typescript/utils/storage.d.ts.map +1 -0
  158. package/lib/typescript/withSwiftPatch.d.ts +12 -0
  159. package/lib/typescript/withSwiftPatch.d.ts.map +1 -0
  160. package/package.json +99 -0
  161. package/react-native-swiftpatch.podspec +50 -0
  162. package/src/SwiftPatchCore.ts +148 -0
  163. package/src/SwiftPatchProvider.tsx +514 -0
  164. package/src/constants.ts +49 -0
  165. package/src/core/Downloader.ts +74 -0
  166. package/src/core/Installer.ts +38 -0
  167. package/src/core/Rollback.ts +28 -0
  168. package/src/core/UpdateChecker.ts +70 -0
  169. package/src/core/Verifier.ts +92 -0
  170. package/src/core/index.ts +11 -0
  171. package/src/index.ts +64 -0
  172. package/src/modal/SwiftPatchModal.tsx +657 -0
  173. package/src/modal/useSwiftPatchModal.ts +24 -0
  174. package/src/native/NativeSwiftPatch.ts +205 -0
  175. package/src/native/NativeSwiftPatchSpec.ts +139 -0
  176. package/src/types.ts +336 -0
  177. package/src/useSwiftPatch.ts +29 -0
  178. package/src/utils/api.ts +244 -0
  179. package/src/utils/device.ts +15 -0
  180. package/src/utils/logger.ts +29 -0
  181. package/src/utils/storage.ts +23 -0
  182. package/src/withSwiftPatch.tsx +41 -0
@@ -0,0 +1,70 @@
1
+ import Foundation
2
+ import CommonCrypto
3
+ import Security
4
+
5
+ class CryptoVerifier {
6
+
7
+ /// Calculate SHA-256 hash of a file
8
+ func sha256(fileURL: URL) throws -> String {
9
+ let data = try Data(contentsOf: fileURL)
10
+ var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
11
+
12
+ data.withUnsafeBytes { buffer in
13
+ _ = CC_SHA256(buffer.baseAddress, CC_LONG(data.count), &hash)
14
+ }
15
+
16
+ return hash.map { String(format: "%02x", $0) }.joined()
17
+ }
18
+
19
+ /// Verify RSA signature
20
+ ///
21
+ /// - Parameters:
22
+ /// - data: The data that was signed (usually the bundle hash)
23
+ /// - signature: The signature in base64 format
24
+ /// - publicKey: The public key in PEM format
25
+ func verifySignature(data: String, signature: String, publicKey: String) -> Bool {
26
+ guard let signatureData = Data(base64Encoded: signature),
27
+ let dataBytes = data.data(using: .utf8),
28
+ let secKey = parsePublicKey(pem: publicKey) else {
29
+ return false
30
+ }
31
+
32
+ var error: Unmanaged<CFError>?
33
+ let result = SecKeyVerifySignature(
34
+ secKey,
35
+ .rsaSignatureMessagePKCS1v15SHA256,
36
+ dataBytes as CFData,
37
+ signatureData as CFData,
38
+ &error
39
+ )
40
+
41
+ return result
42
+ }
43
+
44
+ // MARK: - Private
45
+
46
+ private func parsePublicKey(pem: String) -> SecKey? {
47
+ // Strip PEM headers
48
+ let stripped = pem
49
+ .replacingOccurrences(of: "-----BEGIN PUBLIC KEY-----", with: "")
50
+ .replacingOccurrences(of: "-----END PUBLIC KEY-----", with: "")
51
+ .replacingOccurrences(of: "\n", with: "")
52
+ .replacingOccurrences(of: "\r", with: "")
53
+ .trimmingCharacters(in: .whitespaces)
54
+
55
+ guard let keyData = Data(base64Encoded: stripped) else {
56
+ return nil
57
+ }
58
+
59
+ let attributes: [String: Any] = [
60
+ kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
61
+ kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
62
+ kSecAttrKeySizeInBits as String: 2048
63
+ ]
64
+
65
+ var error: Unmanaged<CFError>?
66
+ let key = SecKeyCreateWithData(keyData as CFData, attributes as CFDictionary, &error)
67
+
68
+ return key
69
+ }
70
+ }
@@ -0,0 +1,125 @@
1
+ import Foundation
2
+
3
+ struct SPDownloadProgress {
4
+ let downloadedBytes: Int64
5
+ let totalBytes: Int64
6
+ let percentage: Int
7
+ }
8
+
9
+ class SPDownloadManager {
10
+
11
+ /// Download a file from a URL with progress reporting (synchronous)
12
+ func download(
13
+ urlString: String,
14
+ to outputURL: URL,
15
+ onProgress: @escaping (SPDownloadProgress) -> Void
16
+ ) throws {
17
+ guard let url = URL(string: urlString) else {
18
+ throw NSError(
19
+ domain: "SwiftPatch",
20
+ code: -1,
21
+ userInfo: [NSLocalizedDescriptionKey: "Invalid URL: \(urlString)"]
22
+ )
23
+ }
24
+
25
+ let semaphore = DispatchSemaphore(value: 0)
26
+ var downloadError: Error?
27
+
28
+ let delegate = DownloadDelegate(
29
+ outputURL: outputURL,
30
+ onProgress: onProgress,
31
+ completion: { error in
32
+ downloadError = error
33
+ semaphore.signal()
34
+ }
35
+ )
36
+
37
+ let session = URLSession(
38
+ configuration: .default,
39
+ delegate: delegate,
40
+ delegateQueue: nil
41
+ )
42
+
43
+ let task = session.downloadTask(with: url)
44
+ task.resume()
45
+
46
+ semaphore.wait()
47
+ session.invalidateAndCancel()
48
+
49
+ if let error = downloadError {
50
+ throw error
51
+ }
52
+
53
+ guard FileManager.default.fileExists(atPath: outputURL.path) else {
54
+ throw NSError(
55
+ domain: "SwiftPatch",
56
+ code: -2,
57
+ userInfo: [NSLocalizedDescriptionKey: "Downloaded file not found"]
58
+ )
59
+ }
60
+ }
61
+ }
62
+
63
+ // MARK: - Download Delegate
64
+
65
+ private class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
66
+ let outputURL: URL
67
+ let onProgress: (SPDownloadProgress) -> Void
68
+ let completion: (Error?) -> Void
69
+
70
+ init(outputURL: URL, onProgress: @escaping (SPDownloadProgress) -> Void, completion: @escaping (Error?) -> Void) {
71
+ self.outputURL = outputURL
72
+ self.onProgress = onProgress
73
+ self.completion = completion
74
+ }
75
+
76
+ func urlSession(_ session: URLSession,
77
+ downloadTask: URLSessionDownloadTask,
78
+ didFinishDownloadingTo location: URL) {
79
+ do {
80
+ // Remove existing file if present
81
+ let fm = FileManager.default
82
+ if fm.fileExists(atPath: outputURL.path) {
83
+ try fm.removeItem(at: outputURL)
84
+ }
85
+
86
+ // Ensure parent directory exists
87
+ try fm.createDirectory(
88
+ at: outputURL.deletingLastPathComponent(),
89
+ withIntermediateDirectories: true
90
+ )
91
+
92
+ try fm.moveItem(at: location, to: outputURL)
93
+ completion(nil)
94
+ } catch {
95
+ completion(error)
96
+ }
97
+ }
98
+
99
+ func urlSession(_ session: URLSession,
100
+ downloadTask: URLSessionDownloadTask,
101
+ didWriteData bytesWritten: Int64,
102
+ totalBytesWritten: Int64,
103
+ totalBytesExpectedToWrite: Int64) {
104
+ let total = totalBytesExpectedToWrite > 0
105
+ ? totalBytesExpectedToWrite
106
+ : totalBytesWritten
107
+ let percentage = total > 0
108
+ ? Int((totalBytesWritten * 100) / total)
109
+ : 0
110
+
111
+ onProgress(SPDownloadProgress(
112
+ downloadedBytes: totalBytesWritten,
113
+ totalBytes: total,
114
+ percentage: min(percentage, 100)
115
+ ))
116
+ }
117
+
118
+ func urlSession(_ session: URLSession,
119
+ task: URLSessionTask,
120
+ didCompleteWithError error: Error?) {
121
+ if let error = error {
122
+ completion(error)
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,116 @@
1
+ import Foundation
2
+
3
+ /// Types of SDK events that are queued and sent to the analytics backend
4
+ enum SwiftPatchEventType: String {
5
+ case downloadProdStarted = "DOWNLOAD_PROD_STARTED"
6
+ case downloadProdCompleted = "DOWNLOAD_PROD_COMPLETED"
7
+ case downloadProdFailed = "DOWNLOAD_PROD_FAILED"
8
+ case installedProd = "INSTALLED_PROD"
9
+ case rollbackProd = "ROLLBACK_PROD"
10
+ case stabilizeProd = "STABILIZE_PROD"
11
+ case corruptionProd = "CORRUPTION_PROD"
12
+ case downloadStageStarted = "DOWNLOAD_STAGE_STARTED"
13
+ case downloadStageCompleted = "DOWNLOAD_STAGE_COMPLETED"
14
+ case downloadStageFailed = "DOWNLOAD_STAGE_FAILED"
15
+ case installedStage = "INSTALLED_STAGE"
16
+ case syncError = "SYNC_ERROR"
17
+ case versionChanged = "VERSION_CHANGED"
18
+ case crashDetected = "CRASH_DETECTED"
19
+ }
20
+
21
+ /// A single event entry
22
+ struct SwiftPatchEvent {
23
+ let id: String
24
+ let eventType: SwiftPatchEventType
25
+ let timestamp: TimeInterval
26
+ let releaseHash: String?
27
+ let errorMessage: String?
28
+ let metadata: [String: Any]?
29
+
30
+ func toDictionary() -> [String: Any] {
31
+ var dict: [String: Any] = [
32
+ "id": id,
33
+ "eventType": eventType.rawValue,
34
+ "timestamp": timestamp,
35
+ ]
36
+ if let hash = releaseHash { dict["releaseHash"] = hash }
37
+ if let error = errorMessage { dict["errorMessage"] = error }
38
+ if let meta = metadata { dict["metadata"] = meta }
39
+ return dict
40
+ }
41
+ }
42
+
43
+ /// Thread-safe event queue that persists events to UserDefaults.
44
+ /// Events are queued natively and polled by JS for batch sending to the analytics backend.
45
+ class EventQueue {
46
+
47
+ private let userDefaults: UserDefaults
48
+ private let prefsKey: String
49
+ private let queue = DispatchQueue(label: "com.swiftpatch.eventqueue", attributes: .concurrent)
50
+
51
+ init(userDefaults: UserDefaults, prefsKey: String) {
52
+ self.userDefaults = userDefaults
53
+ self.prefsKey = prefsKey
54
+ }
55
+
56
+ // MARK: - Push Events
57
+
58
+ func pushEvent(type: SwiftPatchEventType, releaseHash: String? = nil, errorMessage: String? = nil, metadata: [String: Any]? = nil) {
59
+ let event = SwiftPatchEvent(
60
+ id: UUID().uuidString,
61
+ eventType: type,
62
+ timestamp: Date().timeIntervalSince1970,
63
+ releaseHash: releaseHash,
64
+ errorMessage: errorMessage,
65
+ metadata: metadata
66
+ )
67
+
68
+ queue.async(flags: .barrier) { [weak self] in
69
+ guard let self = self else { return }
70
+ var events = self.loadEvents()
71
+ events.append(event.toDictionary())
72
+ self.saveEvents(events)
73
+ }
74
+ }
75
+
76
+ // MARK: - Pop Events (called from JS)
77
+
78
+ /// Pop all pending events. Returns JSON-encoded array.
79
+ func popEvents() -> [[String: Any]] {
80
+ var result: [[String: Any]] = []
81
+ queue.sync {
82
+ result = loadEvents()
83
+ }
84
+ return result
85
+ }
86
+
87
+ // MARK: - Acknowledge Events (remove processed events)
88
+
89
+ func acknowledgeEvents(eventIds: [String]) {
90
+ queue.async(flags: .barrier) { [weak self] in
91
+ guard let self = self else { return }
92
+ var events = self.loadEvents()
93
+ events.removeAll { event in
94
+ guard let id = event["id"] as? String else { return false }
95
+ return eventIds.contains(id)
96
+ }
97
+ self.saveEvents(events)
98
+ }
99
+ }
100
+
101
+ // MARK: - Persistence
102
+
103
+ private func loadEvents() -> [[String: Any]] {
104
+ guard let data = userDefaults.data(forKey: "\(prefsKey)_event_queue"),
105
+ let events = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
106
+ return []
107
+ }
108
+ return events
109
+ }
110
+
111
+ private func saveEvents(_ events: [[String: Any]]) {
112
+ if let data = try? JSONSerialization.data(withJSONObject: events) {
113
+ userDefaults.set(data, forKey: "\(prefsKey)_event_queue")
114
+ }
115
+ }
116
+ }
@@ -0,0 +1,38 @@
1
+ import Foundation
2
+
3
+ enum SPFileUtils {
4
+
5
+ /// Decompress a gzip-compressed file.
6
+ /// For production brotli support, integrate a proper Brotli framework.
7
+ static func decompressGzip(from input: URL, to output: URL) throws {
8
+ let inputData = try Data(contentsOf: input)
9
+
10
+ // Check for gzip magic bytes (0x1f 0x8b)
11
+ if inputData.count >= 2 && inputData[0] == 0x1F && inputData[1] == 0x8B {
12
+ // Use built-in NSData decompression
13
+ let decompressed = try (inputData as NSData).decompressed(using: .zlib)
14
+ try (decompressed as Data).write(to: output)
15
+ } else {
16
+ // Not compressed, just copy
17
+ try FileManager.default.copyItem(at: input, to: output)
18
+ }
19
+ }
20
+
21
+ /// Safely delete a file, ignoring errors
22
+ static func safeDelete(at url: URL) {
23
+ try? FileManager.default.removeItem(at: url)
24
+ }
25
+
26
+ /// Get the size of a file
27
+ static func fileSize(at url: URL) -> Int64 {
28
+ let attrs = try? FileManager.default.attributesOfItem(atPath: url.path)
29
+ return (attrs?[.size] as? Int64) ?? 0
30
+ }
31
+
32
+ /// Get a human-readable size string
33
+ static func humanReadableSize(bytes: Int64) -> String {
34
+ let formatter = ByteCountFormatter()
35
+ formatter.countStyle = .file
36
+ return formatter.string(fromByteCount: bytes)
37
+ }
38
+ }
@@ -0,0 +1,41 @@
1
+ import Foundation
2
+ import BSpatch
3
+
4
+ class PatchApplier {
5
+
6
+ /// Apply a bsdiff patch to create a new file
7
+ ///
8
+ /// - Parameters:
9
+ /// - oldFile: URL to the original file
10
+ /// - patchFile: URL to the patch file
11
+ /// - newFile: URL where the new file will be written
12
+ func applyPatch(oldFile: URL, patchFile: URL, newFile: URL) throws {
13
+ guard FileManager.default.fileExists(atPath: oldFile.path) else {
14
+ throw SwiftPatchError.patchFailed
15
+ }
16
+ guard FileManager.default.fileExists(atPath: patchFile.path) else {
17
+ throw SwiftPatchError.patchFailed
18
+ }
19
+
20
+ // Ensure parent directory exists
21
+ try FileManager.default.createDirectory(
22
+ at: newFile.deletingLastPathComponent(),
23
+ withIntermediateDirectories: true
24
+ )
25
+
26
+ let result = bspatch_files(
27
+ oldFile.path.cString(using: .utf8),
28
+ patchFile.path.cString(using: .utf8),
29
+ newFile.path.cString(using: .utf8)
30
+ )
31
+
32
+ guard result == 0 else {
33
+ try? FileManager.default.removeItem(at: newFile)
34
+ throw SwiftPatchError.patchFailed
35
+ }
36
+
37
+ guard FileManager.default.fileExists(atPath: newFile.path) else {
38
+ throw SwiftPatchError.patchFailed
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,129 @@
1
+ import Foundation
2
+
3
+ /// Native signal-level crash handler for detecting startup crashes
4
+ /// and triggering automatic rollbacks.
5
+ ///
6
+ /// Unlike the timer-based CrashDetector, this catches actual fatal signals
7
+ /// (SIGABRT, SIGSEGV, etc.) and writes crash markers to disk.
8
+ /// On next launch, if a crash marker exists and the app was not mounted
9
+ /// (i.e., crashed during startup), auto-rollback is triggered.
10
+ final class SignalCrashHandler {
11
+
12
+ static let shared = SignalCrashHandler()
13
+
14
+ private let crashMarkerFile: URL
15
+ private let prefsKey: String = "swiftpatch_prefs"
16
+
17
+ /// Whether the React Native component tree has mounted
18
+ private(set) var isMounted = false
19
+
20
+ private init() {
21
+ let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
22
+ crashMarkerFile = documentsDir.appendingPathComponent("swiftpatch").appendingPathComponent("crash_marker.json")
23
+ }
24
+
25
+ // MARK: - Signal Handlers
26
+
27
+ /// Install signal handlers for fatal signals
28
+ func installSignalHandlers() {
29
+ signal(SIGABRT, signalHandler)
30
+ signal(SIGILL, signalHandler)
31
+ signal(SIGSEGV, signalHandler)
32
+ signal(SIGFPE, signalHandler)
33
+ signal(SIGBUS, signalHandler)
34
+ signal(SIGTRAP, signalHandler)
35
+ signal(SIGPIPE, signalHandler)
36
+ signal(SIGSYS, signalHandler)
37
+ }
38
+
39
+ /// Mark the app UI as mounted (called from JS when component tree renders)
40
+ func markMounted() {
41
+ isMounted = true
42
+ }
43
+
44
+ /// Mark the app UI as unmounted
45
+ func markUnmounted() {
46
+ isMounted = false
47
+ }
48
+
49
+ // MARK: - Crash Marker
50
+
51
+ /// Check for crash marker from previous run.
52
+ /// Returns the crash info if a crash marker exists, nil otherwise.
53
+ func checkForCrashMarker() -> CrashMarkerInfo? {
54
+ guard FileManager.default.fileExists(atPath: crashMarkerFile.path) else {
55
+ return nil
56
+ }
57
+
58
+ guard let data = try? Data(contentsOf: crashMarkerFile),
59
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
60
+ // Remove corrupt marker
61
+ try? FileManager.default.removeItem(at: crashMarkerFile)
62
+ return nil
63
+ }
64
+
65
+ // Remove marker after reading
66
+ try? FileManager.default.removeItem(at: crashMarkerFile)
67
+
68
+ return CrashMarkerInfo(
69
+ signalNumber: json["signal"] as? Int ?? 0,
70
+ signalName: json["signalName"] as? String ?? "UNKNOWN",
71
+ timestamp: json["timestamp"] as? TimeInterval ?? 0,
72
+ wasMounted: json["wasMounted"] as? Bool ?? false
73
+ )
74
+ }
75
+
76
+ /// Write a crash marker to disk. Called from signal handler.
77
+ private func writeCrashMarker(signalNumber: Int32) {
78
+ let signalName: String
79
+ switch signalNumber {
80
+ case SIGABRT: signalName = "SIGABRT"
81
+ case SIGILL: signalName = "SIGILL"
82
+ case SIGSEGV: signalName = "SIGSEGV"
83
+ case SIGFPE: signalName = "SIGFPE"
84
+ case SIGBUS: signalName = "SIGBUS"
85
+ case SIGTRAP: signalName = "SIGTRAP"
86
+ case SIGPIPE: signalName = "SIGPIPE"
87
+ case SIGSYS: signalName = "SIGSYS"
88
+ default: signalName = "SIGNAL_\(signalNumber)"
89
+ }
90
+
91
+ let crashInfo: [String: Any] = [
92
+ "signal": signalNumber,
93
+ "signalName": signalName,
94
+ "timestamp": Date().timeIntervalSince1970,
95
+ "wasMounted": isMounted,
96
+ ]
97
+
98
+ // Write synchronously (we're in a signal handler, limited operations)
99
+ if let data = try? JSONSerialization.data(withJSONObject: crashInfo) {
100
+ // Ensure directory exists
101
+ let dir = crashMarkerFile.deletingLastPathComponent()
102
+ try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
103
+ try? data.write(to: crashMarkerFile, options: .atomic)
104
+ }
105
+ }
106
+
107
+ /// Check if we should auto-rollback based on crash marker
108
+ func shouldAutoRollback(crashInfo: CrashMarkerInfo) -> Bool {
109
+ // Only auto-rollback if the app crashed before mounting (startup crash)
110
+ return !crashInfo.wasMounted
111
+ }
112
+ }
113
+
114
+ /// Info about a crash from the previous app run
115
+ struct CrashMarkerInfo {
116
+ let signalNumber: Int
117
+ let signalName: String
118
+ let timestamp: TimeInterval
119
+ let wasMounted: Bool
120
+ }
121
+
122
+ // C-level signal handler function (must be a plain C function)
123
+ private func signalHandler(_ signal: Int32) {
124
+ SignalCrashHandler.shared.writeCrashMarker(signalNumber: signal)
125
+
126
+ // Re-raise the signal to get default behavior (crash report, etc.)
127
+ Foundation.signal(signal, SIG_DFL)
128
+ raise(signal)
129
+ }