@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.
- package/README.md +430 -0
- package/android/build.gradle +105 -0
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/java/com/swiftpatch/BundleManager.kt +107 -0
- package/android/src/main/java/com/swiftpatch/CrashDetector.kt +79 -0
- package/android/src/main/java/com/swiftpatch/CryptoVerifier.kt +69 -0
- package/android/src/main/java/com/swiftpatch/DownloadManager.kt +120 -0
- package/android/src/main/java/com/swiftpatch/EventQueue.kt +86 -0
- package/android/src/main/java/com/swiftpatch/FileUtils.kt +60 -0
- package/android/src/main/java/com/swiftpatch/PatchApplier.kt +60 -0
- package/android/src/main/java/com/swiftpatch/SignalCrashHandler.kt +84 -0
- package/android/src/main/java/com/swiftpatch/SlotManager.kt +299 -0
- package/android/src/main/java/com/swiftpatch/SwiftPatchModule.kt +630 -0
- package/android/src/main/java/com/swiftpatch/SwiftPatchPackage.kt +21 -0
- package/android/src/main/jni/CMakeLists.txt +12 -0
- package/android/src/main/jni/bspatch.c +188 -0
- package/android/src/main/jni/bspatch.h +57 -0
- package/android/src/main/jni/bspatch_jni.c +28 -0
- package/ios/Libraries/bspatch/bspatch.c +188 -0
- package/ios/Libraries/bspatch/bspatch.h +50 -0
- package/ios/Libraries/bspatch/module.modulemap +4 -0
- package/ios/SwiftPatch/BundleManager.swift +113 -0
- package/ios/SwiftPatch/CrashDetector.swift +71 -0
- package/ios/SwiftPatch/CryptoVerifier.swift +70 -0
- package/ios/SwiftPatch/DownloadManager.swift +125 -0
- package/ios/SwiftPatch/EventQueue.swift +116 -0
- package/ios/SwiftPatch/FileUtils.swift +38 -0
- package/ios/SwiftPatch/PatchApplier.swift +41 -0
- package/ios/SwiftPatch/SignalCrashHandler.swift +129 -0
- package/ios/SwiftPatch/SlotManager.swift +360 -0
- package/ios/SwiftPatch/SwiftPatchModule.m +56 -0
- package/ios/SwiftPatch/SwiftPatchModule.swift +621 -0
- package/lib/commonjs/SwiftPatchCore.js +140 -0
- package/lib/commonjs/SwiftPatchCore.js.map +1 -0
- package/lib/commonjs/SwiftPatchProvider.js +617 -0
- package/lib/commonjs/SwiftPatchProvider.js.map +1 -0
- package/lib/commonjs/constants.js +50 -0
- package/lib/commonjs/constants.js.map +1 -0
- package/lib/commonjs/core/Downloader.js +63 -0
- package/lib/commonjs/core/Downloader.js.map +1 -0
- package/lib/commonjs/core/Installer.js +46 -0
- package/lib/commonjs/core/Installer.js.map +1 -0
- package/lib/commonjs/core/Rollback.js +36 -0
- package/lib/commonjs/core/Rollback.js.map +1 -0
- package/lib/commonjs/core/UpdateChecker.js +57 -0
- package/lib/commonjs/core/UpdateChecker.js.map +1 -0
- package/lib/commonjs/core/Verifier.js +82 -0
- package/lib/commonjs/core/Verifier.js.map +1 -0
- package/lib/commonjs/core/index.js +41 -0
- package/lib/commonjs/core/index.js.map +1 -0
- package/lib/commonjs/index.js +154 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/modal/SwiftPatchModal.js +667 -0
- package/lib/commonjs/modal/SwiftPatchModal.js.map +1 -0
- package/lib/commonjs/modal/useSwiftPatchModal.js +26 -0
- package/lib/commonjs/modal/useSwiftPatchModal.js.map +1 -0
- package/lib/commonjs/native/NativeSwiftPatch.js +85 -0
- package/lib/commonjs/native/NativeSwiftPatch.js.map +1 -0
- package/lib/commonjs/native/NativeSwiftPatchSpec.js +15 -0
- package/lib/commonjs/native/NativeSwiftPatchSpec.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types.js +126 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/useSwiftPatch.js +31 -0
- package/lib/commonjs/useSwiftPatch.js.map +1 -0
- package/lib/commonjs/utils/api.js +206 -0
- package/lib/commonjs/utils/api.js.map +1 -0
- package/lib/commonjs/utils/device.js +23 -0
- package/lib/commonjs/utils/device.js.map +1 -0
- package/lib/commonjs/utils/logger.js +30 -0
- package/lib/commonjs/utils/logger.js.map +1 -0
- package/lib/commonjs/utils/storage.js +31 -0
- package/lib/commonjs/utils/storage.js.map +1 -0
- package/lib/commonjs/withSwiftPatch.js +42 -0
- package/lib/commonjs/withSwiftPatch.js.map +1 -0
- package/lib/module/SwiftPatchCore.js +135 -0
- package/lib/module/SwiftPatchCore.js.map +1 -0
- package/lib/module/SwiftPatchProvider.js +611 -0
- package/lib/module/SwiftPatchProvider.js.map +1 -0
- package/lib/module/constants.js +46 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/core/Downloader.js +57 -0
- package/lib/module/core/Downloader.js.map +1 -0
- package/lib/module/core/Installer.js +41 -0
- package/lib/module/core/Installer.js.map +1 -0
- package/lib/module/core/Rollback.js +31 -0
- package/lib/module/core/Rollback.js.map +1 -0
- package/lib/module/core/UpdateChecker.js +51 -0
- package/lib/module/core/UpdateChecker.js.map +1 -0
- package/lib/module/core/Verifier.js +76 -0
- package/lib/module/core/Verifier.js.map +1 -0
- package/lib/module/core/index.js +8 -0
- package/lib/module/core/index.js.map +1 -0
- package/lib/module/index.js +34 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/modal/SwiftPatchModal.js +661 -0
- package/lib/module/modal/SwiftPatchModal.js.map +1 -0
- package/lib/module/modal/useSwiftPatchModal.js +22 -0
- package/lib/module/modal/useSwiftPatchModal.js.map +1 -0
- package/lib/module/native/NativeSwiftPatch.js +78 -0
- package/lib/module/native/NativeSwiftPatch.js.map +1 -0
- package/lib/module/native/NativeSwiftPatchSpec.js +12 -0
- package/lib/module/native/NativeSwiftPatchSpec.js.map +1 -0
- package/lib/module/types.js +139 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useSwiftPatch.js +26 -0
- package/lib/module/useSwiftPatch.js.map +1 -0
- package/lib/module/utils/api.js +197 -0
- package/lib/module/utils/api.js.map +1 -0
- package/lib/module/utils/device.js +18 -0
- package/lib/module/utils/device.js.map +1 -0
- package/lib/module/utils/logger.js +26 -0
- package/lib/module/utils/logger.js.map +1 -0
- package/lib/module/utils/storage.js +24 -0
- package/lib/module/utils/storage.js.map +1 -0
- package/lib/module/withSwiftPatch.js +37 -0
- package/lib/module/withSwiftPatch.js.map +1 -0
- package/lib/typescript/SwiftPatchCore.d.ts +64 -0
- package/lib/typescript/SwiftPatchCore.d.ts.map +1 -0
- package/lib/typescript/SwiftPatchProvider.d.ts +22 -0
- package/lib/typescript/SwiftPatchProvider.d.ts.map +1 -0
- package/lib/typescript/constants.d.ts +33 -0
- package/lib/typescript/constants.d.ts.map +1 -0
- package/lib/typescript/core/Downloader.d.ts +34 -0
- package/lib/typescript/core/Downloader.d.ts.map +1 -0
- package/lib/typescript/core/Installer.d.ts +25 -0
- package/lib/typescript/core/Installer.d.ts.map +1 -0
- package/lib/typescript/core/Rollback.d.ts +18 -0
- package/lib/typescript/core/Rollback.d.ts.map +1 -0
- package/lib/typescript/core/UpdateChecker.d.ts +27 -0
- package/lib/typescript/core/UpdateChecker.d.ts.map +1 -0
- package/lib/typescript/core/Verifier.d.ts +31 -0
- package/lib/typescript/core/Verifier.d.ts.map +1 -0
- package/lib/typescript/core/index.d.ts +8 -0
- package/lib/typescript/core/index.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +13 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/modal/SwiftPatchModal.d.ts +11 -0
- package/lib/typescript/modal/SwiftPatchModal.d.ts.map +1 -0
- package/lib/typescript/modal/useSwiftPatchModal.d.ts +7 -0
- package/lib/typescript/modal/useSwiftPatchModal.d.ts.map +1 -0
- package/lib/typescript/native/NativeSwiftPatch.d.ts +61 -0
- package/lib/typescript/native/NativeSwiftPatch.d.ts.map +1 -0
- package/lib/typescript/native/NativeSwiftPatchSpec.d.ts +67 -0
- package/lib/typescript/native/NativeSwiftPatchSpec.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +266 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/useSwiftPatch.d.ts +12 -0
- package/lib/typescript/useSwiftPatch.d.ts.map +1 -0
- package/lib/typescript/utils/api.d.ts +87 -0
- package/lib/typescript/utils/api.d.ts.map +1 -0
- package/lib/typescript/utils/device.d.ts +9 -0
- package/lib/typescript/utils/device.d.ts.map +1 -0
- package/lib/typescript/utils/logger.d.ts +8 -0
- package/lib/typescript/utils/logger.d.ts.map +1 -0
- package/lib/typescript/utils/storage.d.ts +14 -0
- package/lib/typescript/utils/storage.d.ts.map +1 -0
- package/lib/typescript/withSwiftPatch.d.ts +12 -0
- package/lib/typescript/withSwiftPatch.d.ts.map +1 -0
- package/package.json +99 -0
- package/react-native-swiftpatch.podspec +50 -0
- package/src/SwiftPatchCore.ts +148 -0
- package/src/SwiftPatchProvider.tsx +514 -0
- package/src/constants.ts +49 -0
- package/src/core/Downloader.ts +74 -0
- package/src/core/Installer.ts +38 -0
- package/src/core/Rollback.ts +28 -0
- package/src/core/UpdateChecker.ts +70 -0
- package/src/core/Verifier.ts +92 -0
- package/src/core/index.ts +11 -0
- package/src/index.ts +64 -0
- package/src/modal/SwiftPatchModal.tsx +657 -0
- package/src/modal/useSwiftPatchModal.ts +24 -0
- package/src/native/NativeSwiftPatch.ts +205 -0
- package/src/native/NativeSwiftPatchSpec.ts +139 -0
- package/src/types.ts +336 -0
- package/src/useSwiftPatch.ts +29 -0
- package/src/utils/api.ts +244 -0
- package/src/utils/device.ts +15 -0
- package/src/utils/logger.ts +29 -0
- package/src/utils/storage.ts +23 -0
- 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
|
+
}
|