react-native-stallion 2.4.0-alpha.3 → 2.4.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/android/src/main/cpp/CMakeLists.txt +41 -0
  2. package/android/src/main/cpp/bspatch_bridge.c +268 -0
  3. package/android/src/main/cpp/bspatch_bridge.h +26 -0
  4. package/android/src/main/cpp/bzip2/blocksort.c +1094 -0
  5. package/android/src/main/cpp/bzip2/bzlib.c +1572 -0
  6. package/android/src/main/cpp/bzip2/bzlib.h +282 -0
  7. package/android/src/main/cpp/bzip2/bzlib_private.h +509 -0
  8. package/android/src/main/cpp/bzip2/compress.c +672 -0
  9. package/android/src/main/cpp/bzip2/crctable.c +104 -0
  10. package/android/src/main/cpp/bzip2/decompress.c +652 -0
  11. package/android/src/main/cpp/bzip2/huffman.c +205 -0
  12. package/android/src/main/cpp/bzip2/randtable.c +84 -0
  13. package/android/src/main/cpp/stallion_bspatch_jni.cpp +39 -0
  14. package/android/src/main/java/com/stallion/StallionModule.java +13 -1
  15. package/android/src/main/java/com/stallion/networkmanager/StallionPatchHandler.java +222 -0
  16. package/android/src/main/java/com/stallion/networkmanager/StallionSyncHandler.java +88 -16
  17. package/android/src/main/java/com/stallion/storage/StallionMeta.java +12 -0
  18. package/android/src/main/java/com/stallion/storage/StallionStateManager.java +25 -0
  19. package/android/src/main/java/com/stallion/utils/StallionBSPatch.java +30 -0
  20. package/ios/Stallion.xcodeproj/project.pbxproj +6 -0
  21. package/ios/main/Stallion-Bridging-Header.h +1 -0
  22. package/ios/main/Stallion.m +22 -0
  23. package/ios/main/Stallion.swift +8 -1
  24. package/ios/main/StallionBSPatch.swift +35 -0
  25. package/ios/main/StallionMeta.h +1 -0
  26. package/ios/main/StallionMeta.m +12 -0
  27. package/ios/main/StallionModule.m +3 -3
  28. package/ios/main/StallionPatchHandler.swift +206 -0
  29. package/ios/main/StallionSignatureVerification.swift +1 -1
  30. package/ios/main/StallionStateManager.h +3 -0
  31. package/ios/main/StallionStateManager.m +3 -0
  32. package/ios/main/StallionSyncHandler.swift +86 -10
  33. package/ios/main/bspatch.c +270 -0
  34. package/ios/main/bspatch_bridge.h +25 -0
  35. package/package.json +1 -1
  36. package/react-native-stallion.podspec +3 -1
  37. package/src/index.js +2 -0
  38. package/src/index.js.map +1 -1
  39. package/src/main/utils/StallionNativeUtils.js +3 -0
  40. package/src/main/utils/StallionNativeUtils.js.map +1 -1
  41. package/types/index.d.ts +1 -0
  42. package/types/index.d.ts.map +1 -1
  43. package/types/main/utils/StallionNativeUtils.d.ts +2 -1
  44. package/types/main/utils/StallionNativeUtils.d.ts.map +1 -1
  45. package/types/types/utils.types.d.ts +1 -0
  46. package/types/types/utils.types.d.ts.map +1 -1
@@ -70,8 +70,8 @@ public class StallionSyncHandler {
70
70
  JSONObject data = releaseMeta.optJSONObject("data");
71
71
  if (data == null) return;
72
72
 
73
- handleAppliedReleaseData(data.optJSONObject("appliedBundleData"), appVersion);
74
- handleNewReleaseData(data.optJSONObject("newBundleData"));
73
+ handleAppliedReleaseData(data.optJSONObject("appliedBundleData"), appVersion);
74
+ handleNewReleaseData(data.optJSONObject("newBundleData"));
75
75
  }
76
76
  }
77
77
 
@@ -91,6 +91,23 @@ public class StallionSyncHandler {
91
91
  String newReleaseUrl = newReleaseData.optString("downloadUrl");
92
92
  String newReleaseHash = newReleaseData.optString("checksum");
93
93
 
94
+ // Extract diffData if it exists
95
+ JSONObject diffData = newReleaseData.optJSONObject("bundleDiff");
96
+ String diffUrl = null;
97
+ boolean isBundlePatched = false;
98
+ String bundleDiffId = null;
99
+ if (diffData != null) {
100
+ diffUrl = diffData.optString("url");
101
+ if (diffUrl != null && diffUrl.isEmpty()) {
102
+ diffUrl = null;
103
+ }
104
+ isBundlePatched = diffData.optBoolean("isBundlePatched", false);
105
+ bundleDiffId = diffData.optString("id");
106
+ if (bundleDiffId != null && bundleDiffId.isEmpty()) {
107
+ bundleDiffId = null;
108
+ }
109
+ }
110
+
94
111
  StallionStateManager stateManager = StallionStateManager.getInstance();
95
112
  String lastRolledBackHash = stateManager.stallionMeta.getLastRolledBackHash();
96
113
  String lastUnverifiedHash = stateManager.getStallionConfig().getLastUnverifiedHash();
@@ -102,14 +119,29 @@ public class StallionSyncHandler {
102
119
  && !newReleaseHash.equals(lastUnverifiedHash)
103
120
  ) {
104
121
  if(stateManager.getIsMounted()) {
105
- downloadNewRelease(newReleaseHash, newReleaseUrl);
122
+ downloadNewRelease(newReleaseHash, newReleaseUrl, diffUrl, isBundlePatched, bundleDiffId);
106
123
  } else {
107
- stateManager.setPendingRelease(newReleaseUrl, newReleaseHash);
124
+ stateManager.setPendingRelease(newReleaseUrl, newReleaseHash, diffUrl, isBundlePatched, bundleDiffId);
108
125
  }
109
126
  }
110
127
  }
111
128
 
129
+ // Overloaded method for backward compatibility
112
130
  public static void downloadNewRelease(String newReleaseHash, String newReleaseUrl) {
131
+ downloadNewRelease(newReleaseHash, newReleaseUrl, null, false);
132
+ }
133
+
134
+ // Overloaded method for backward compatibility
135
+ public static void downloadNewRelease(String newReleaseHash, String newReleaseUrl, String diffUrl) {
136
+ downloadNewRelease(newReleaseHash, newReleaseUrl, diffUrl, false, null);
137
+ }
138
+
139
+ // Overloaded method for backward compatibility
140
+ public static void downloadNewRelease(String newReleaseHash, String newReleaseUrl, String diffUrl, boolean isBundlePatched) {
141
+ downloadNewRelease(newReleaseHash, newReleaseUrl, diffUrl, isBundlePatched, null);
142
+ }
143
+
144
+ public static void downloadNewRelease(String newReleaseHash, String newReleaseUrl, String diffUrl, boolean isBundlePatched, String bundleDiffId) {
113
145
  // Ensure only one download job runs at a time
114
146
  if (!isDownloadInProgress.compareAndSet(false, true)) {
115
147
  return; // Exit if another job is already running
@@ -121,12 +153,24 @@ public class StallionSyncHandler {
121
153
  + StallionConfigConstants.PROD_DIRECTORY
122
154
  + StallionConfigConstants.TEMP_FOLDER_SLOT;
123
155
  String projectId = config.getProjectId();
124
- String downloadUrl = newReleaseUrl + "?projectId=" + projectId;
156
+
157
+ // Use diffUrl if it exists, otherwise use newReleaseUrl
158
+ String urlToDownload = (diffUrl != null && !diffUrl.isEmpty()) ? diffUrl : newReleaseUrl;
159
+ String downloadUrl = urlToDownload + "?projectId=" + projectId;
125
160
  String publicSigningKey = config.getPublicSigningKey();
126
161
 
162
+ // Track if this is a diff download
163
+ final boolean isDiffDownload = (diffUrl != null && !diffUrl.isEmpty());
164
+ // Store original newReleaseUrl for potential retry
165
+ final String originalNewReleaseUrl = newReleaseUrl;
166
+ // Store isBundlePatched flag for patch handler
167
+ final boolean isBundlePatchedFlag = isBundlePatched;
168
+ // Store bundleDiffId for events
169
+ final String bundleDiffIdForEvents = bundleDiffId;
170
+
127
171
  long alreadyDownloaded = StallionDownloadCacheManager.getDownloadCache(config, downloadUrl, downloadPath);
128
172
 
129
- emitDownloadStarted(newReleaseHash, alreadyDownloaded > 0);
173
+ emitDownloadStarted(newReleaseHash, alreadyDownloaded > 0, bundleDiffIdForEvents);
130
174
 
131
175
  StallionFileDownloader.downloadBundle(
132
176
  downloadUrl,
@@ -144,6 +188,28 @@ public class StallionSyncHandler {
144
188
  isDownloadInProgress.set(false);
145
189
  StallionDownloadCacheManager.deleteDownloadCache(downloadPath);
146
190
 
191
+ // If this was a diff download, handle patch
192
+ if (isDiffDownload) {
193
+ try {
194
+ // Get base bundle path from current slot
195
+ String slotPath = stateManager.stallionMeta.getCurrentProdSlotPath();
196
+ String baseBundlePath = config.getFilesDirectory()
197
+ + StallionConfigConstants.PROD_DIRECTORY
198
+ + slotPath;
199
+
200
+ // Invoke patch handler with isBundlePatched flag
201
+ StallionPatchHandler.applyPatch(baseBundlePath, downloadPath, isBundlePatchedFlag);
202
+ } catch (Exception e) {
203
+ // Patch application failed, retry with full bundle download
204
+ // Clean up the failed diff download
205
+ StallionFileManager.deleteFileOrFolderSilently(new File(downloadPath));
206
+ isDownloadInProgress.set(false);
207
+ // Retry download with full bundle (no diffUrl)
208
+ downloadNewRelease(newReleaseHash, originalNewReleaseUrl, null, false, null);
209
+ return;
210
+ }
211
+ }
212
+
147
213
  if(publicSigningKey != null && !publicSigningKey.isEmpty()) {
148
214
  if(
149
215
  !StallionSignatureVerification.verifyReleaseSignature(
@@ -158,14 +224,14 @@ public class StallionSyncHandler {
158
224
  }
159
225
  }
160
226
 
161
- stateManager.stallionMeta.setCurrentProdSlot(StallionMetaConstants.SlotStates.NEW_SLOT);
162
- stateManager.stallionMeta.setProdTempHash(newReleaseHash);
163
- String currentProdNewHash = stateManager.stallionMeta.getProdNewHash();
164
- if(currentProdNewHash != null && !currentProdNewHash.isEmpty()) {
165
- StallionSlotManager.stabilizeProd();
166
- }
167
- stateManager.syncStallionMeta();
168
- emitDownloadSuccess(newReleaseHash);
227
+ stateManager.stallionMeta.setCurrentProdSlot(StallionMetaConstants.SlotStates.NEW_SLOT);
228
+ stateManager.stallionMeta.setProdTempHash(newReleaseHash);
229
+ String currentProdNewHash = stateManager.stallionMeta.getProdNewHash();
230
+ if(currentProdNewHash != null && !currentProdNewHash.isEmpty()) {
231
+ StallionSlotManager.stabilizeProd();
232
+ }
233
+ stateManager.syncStallionMeta();
234
+ emitDownloadSuccess(newReleaseHash, bundleDiffIdForEvents);
169
235
  }
170
236
 
171
237
  @Override
@@ -216,10 +282,13 @@ public class StallionSyncHandler {
216
282
  );
217
283
  }
218
284
 
219
- private static void emitDownloadSuccess(String releaseHash) {
285
+ private static void emitDownloadSuccess(String releaseHash, String bundleDiffId) {
220
286
  JSONObject successPayload = new JSONObject();
221
287
  try {
222
288
  successPayload.put("releaseHash", releaseHash);
289
+ if (bundleDiffId != null && !bundleDiffId.isEmpty()) {
290
+ successPayload.put("diffId", bundleDiffId);
291
+ }
223
292
  } catch (Exception ignored) { }
224
293
  StallionEventManager.getInstance().sendEvent(
225
294
  NativeProdEventTypes.DOWNLOAD_COMPLETE_PROD.toString(),
@@ -227,10 +296,13 @@ public class StallionSyncHandler {
227
296
  );
228
297
  }
229
298
 
230
- private static void emitDownloadStarted(String releaseHash, Boolean isResume) {
299
+ private static void emitDownloadStarted(String releaseHash, Boolean isResume, String bundleDiffId) {
231
300
  JSONObject successPayload = new JSONObject();
232
301
  try {
233
302
  successPayload.put("releaseHash", releaseHash);
303
+ if (bundleDiffId != null && !bundleDiffId.isEmpty()) {
304
+ successPayload.put("diffId", bundleDiffId);
305
+ }
234
306
  } catch (Exception ignored) { }
235
307
  StallionEventManager.getInstance().sendEvent(
236
308
  isResume ? NativeProdEventTypes.DOWNLOAD_RESUME_PROD.toString(): NativeProdEventTypes.DOWNLOAD_STARTED_PROD.toString(),
@@ -1,5 +1,6 @@
1
1
  package com.stallion.storage;
2
2
 
3
+ import com.stallion.storage.StallionConfigConstants;
3
4
  import org.json.JSONException;
4
5
  import org.json.JSONObject;
5
6
 
@@ -77,6 +78,17 @@ public class StallionMeta {
77
78
  }
78
79
  }
79
80
 
81
+ public String getCurrentProdSlotPath() {
82
+ switch (this.currentProdSlot) {
83
+ case NEW_SLOT:
84
+ return StallionConfigConstants.NEW_FOLDER_SLOT;
85
+ case STABLE_SLOT:
86
+ return StallionConfigConstants.STABLE_FOLDER_SLOT;
87
+ default:
88
+ return StallionConfigConstants.NEW_FOLDER_SLOT;
89
+ }
90
+ }
91
+
80
92
  public void setCurrentProdSlot(StallionMetaConstants.SlotStates currentProdSlot) {
81
93
  this.currentProdSlot = currentProdSlot;
82
94
  }
@@ -21,6 +21,9 @@ public class StallionStateManager {
21
21
  private boolean isMounted;
22
22
  private String pendingReleaseUrl;
23
23
  private String pendingReleaseHash;
24
+ private String pendingReleaseDiffUrl;
25
+ private boolean pendingReleaseIsBundlePatched;
26
+ private String pendingReleaseBundleDiffId;
24
27
  private boolean isSyncSuccessful;
25
28
 
26
29
  private StallionStateManager(Context context) {
@@ -30,6 +33,9 @@ public class StallionStateManager {
30
33
  this.isMounted = false;
31
34
  this.pendingReleaseUrl = "";
32
35
  this.pendingReleaseHash = "";
36
+ this.pendingReleaseDiffUrl = null;
37
+ this.pendingReleaseIsBundlePatched = false;
38
+ this.pendingReleaseBundleDiffId = null;
33
39
 
34
40
  // Reset mount state on initialization (ensures mount marker file is deleted for new session)
35
41
  setIsMounted(false);
@@ -116,8 +122,15 @@ public class StallionStateManager {
116
122
  }
117
123
 
118
124
  public void setPendingRelease(String pendingReleaseUrl, String pendingReleaseHash) {
125
+ setPendingRelease(pendingReleaseUrl, pendingReleaseHash, null, false, null);
126
+ }
127
+
128
+ public void setPendingRelease(String pendingReleaseUrl, String pendingReleaseHash, String diffUrl, boolean isBundlePatched, String bundleDiffId) {
119
129
  this.pendingReleaseUrl = pendingReleaseUrl;
120
130
  this.pendingReleaseHash = pendingReleaseHash;
131
+ this.pendingReleaseDiffUrl = diffUrl;
132
+ this.pendingReleaseIsBundlePatched = isBundlePatched;
133
+ this.pendingReleaseBundleDiffId = bundleDiffId;
121
134
  }
122
135
 
123
136
  public String getPendingReleaseUrl() {
@@ -128,6 +141,18 @@ public class StallionStateManager {
128
141
  return this.pendingReleaseHash;
129
142
  }
130
143
 
144
+ public String getPendingReleaseDiffUrl() {
145
+ return this.pendingReleaseDiffUrl;
146
+ }
147
+
148
+ public boolean getPendingReleaseIsBundlePatched() {
149
+ return this.pendingReleaseIsBundlePatched;
150
+ }
151
+
152
+ public String getPendingReleaseBundleDiffId() {
153
+ return this.pendingReleaseBundleDiffId;
154
+ }
155
+
131
156
  public boolean getIsSyncSuccessful() { return this.isSyncSuccessful; }
132
157
 
133
158
  public void setIsSyncSuccessful(boolean isSyncSuccessful) { this.isSyncSuccessful = isSyncSuccessful; }
@@ -0,0 +1,30 @@
1
+ package com.stallion.utils;
2
+
3
+ public class StallionBSPatch {
4
+
5
+ static {
6
+ System.loadLibrary("stallion-bspatch");
7
+ }
8
+
9
+ /**
10
+ * Applies a bsdiff patch file to oldFile, writing the result to newFile.
11
+ *
12
+ * @param oldFile Path to the old/base file
13
+ * @param newFile Path where the patched file will be written
14
+ * @param patchFile Path to the patch file
15
+ * @return true if patch was applied successfully, false otherwise
16
+ */
17
+ public static boolean applyPatch(String oldFile, String newFile, String patchFile) {
18
+ int result = nativeApplyPatch(oldFile, newFile, patchFile);
19
+ if (result == 0) {
20
+ return true;
21
+ } else {
22
+ android.util.Log.e("StallionBSPatch", "BSPatch failed with code: " + result +
23
+ " (old: " + oldFile + ", patch: " + patchFile + ")");
24
+ return false;
25
+ }
26
+ }
27
+
28
+ private static native int nativeApplyPatch(String oldFile, String newFile, String patchFile);
29
+ }
30
+
@@ -10,6 +10,7 @@
10
10
  5E555C0D2413F4C50049A1A2 /* Stallion.m in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* Stallion.m */; };
11
11
  F4FF95D7245B92E800C19C63 /* Stallion.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FF95D6245B92E800C19C63 /* Stallion.swift */; };
12
12
  F4FF95DA245B92EB00C19C63 /* StallionDeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4FF95D9245B92EB00C19C63 /* StallionDeviceInfo.swift */; };
13
+ F4FF95E1245B92F000C19C68 /* bspatch.c in Sources */ = {isa = PBXBuildFile; fileRef = F4FF95DF245B92EE00C19C66 /* bspatch.c */; };
13
14
  /* End PBXBuildFile section */
14
15
 
15
16
  /* Begin PBXCopyFilesBuildPhase section */
@@ -31,6 +32,8 @@
31
32
  F4FF95D6245B92E800C19C63 /* Stallion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stallion.swift; sourceTree = "<group>"; };
32
33
  F4FF95D7245B92E900C19C63 /* StallionVersion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "StallionVersion.h"; sourceTree = "<group>"; };
33
34
  F4FF95D9245B92EB00C19C63 /* StallionDeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StallionDeviceInfo.swift"; sourceTree = "<group>"; };
35
+ F4FF95DF245B92EE00C19C66 /* bspatch.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = bspatch.c; sourceTree = "<group>"; };
36
+ F4FF95E0245B92EF00C19C67 /* bspatch_bridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bspatch_bridge.h; sourceTree = "<group>"; };
34
37
  /* End PBXFileReference section */
35
38
 
36
39
  /* Begin PBXFrameworksBuildPhase section */
@@ -57,6 +60,8 @@
57
60
  children = (
58
61
  F4FF95D6245B92E800C19C63 /* Stallion.swift */,
59
62
  F4FF95D9245B92EB00C19C63 /* StallionDeviceInfo.swift */,
63
+ F4FF95DF245B92EE00C19C66 /* bspatch.c */,
64
+ F4FF95E0245B92EF00C19C67 /* bspatch_bridge.h */,
60
65
  B3E7B5891CC2AC0600A0062D /* Stallion.m */,
61
66
  F4FF95D5245B92E700C19C63 /* Stallion-Bridging-Header.h */,
62
67
  F4FF95D7245B92E900C19C63 /* StallionVersion.h */,
@@ -123,6 +128,7 @@
123
128
  files = (
124
129
  F4FF95D7245B92E800C19C63 /* Stallion.swift in Sources */,
125
130
  F4FF95DA245B92EB00C19C63 /* StallionDeviceInfo.swift in Sources */,
131
+ F4FF95E1245B92F000C19C68 /* bspatch.c in Sources */,
126
132
  B3E7B58A1CC2AC0600A0062D /* Stallion.m in Sources */,
127
133
  );
128
134
  runOnlyForDeploymentPostprocessing = 0;
@@ -8,3 +8,4 @@
8
8
  #import <StallionMetaConstants.h>
9
9
  #import <StallionSlotManager.h>
10
10
  #import <StallionExceptionHandler.h>
11
+ #import "bspatch_bridge.h"
@@ -1,5 +1,7 @@
1
1
  #import <React/RCTBridgeModule.h>
2
2
  #import <React/RCTEventEmitter.h>
3
+ #import "StallionStateManager.h"
4
+ #import "StallionMeta.h"
3
5
 
4
6
  @interface RCT_EXTERN_MODULE(Stallion, NSObject)
5
7
 
@@ -40,3 +42,23 @@ RCT_EXTERN_METHOD(restart)
40
42
  }
41
43
 
42
44
  @end
45
+
46
+ // Forward declare the class (implemented in Swift)
47
+ @class Stallion;
48
+
49
+ @interface Stallion (Sync)
50
+ @end
51
+
52
+ @implementation Stallion (Sync)
53
+
54
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getActiveReleaseHash)
55
+ {
56
+ StallionStateManager *stateManager = [StallionStateManager sharedInstance];
57
+ if (stateManager && stateManager.stallionMeta) {
58
+ NSString *hash = [stateManager.stallionMeta getActiveReleaseHash];
59
+ return hash ? hash : @"";
60
+ }
61
+ return @"";
62
+ }
63
+
64
+ @end
@@ -46,9 +46,16 @@ class Stallion: RCTEventEmitter {
46
46
  !pendingReleaseUrl.isEmpty,
47
47
  !pendingReleaseHash.isEmpty else { return }
48
48
 
49
- StallionSyncHandler.downloadNewRelease(newReleaseHash: pendingReleaseHash, newReleaseUrl: pendingReleaseUrl)
49
+ let pendingReleaseDiffUrl = stallionStateManager.pendingReleaseDiffUrl
50
+ let pendingReleaseIsBundlePatched = stallionStateManager.pendingReleaseIsBundlePatched
51
+ let pendingReleaseBundleDiffId = stallionStateManager.pendingReleaseBundleDiffId
52
+
53
+ StallionSyncHandler.downloadNewRelease(newReleaseHash: pendingReleaseHash, newReleaseUrl: pendingReleaseUrl, diffUrl: pendingReleaseDiffUrl, isBundlePatched: pendingReleaseIsBundlePatched, bundleDiffId: pendingReleaseBundleDiffId)
50
54
  stallionStateManager.pendingReleaseUrl = ""
51
55
  stallionStateManager.pendingReleaseHash = ""
56
+ stallionStateManager.pendingReleaseDiffUrl = nil
57
+ stallionStateManager.pendingReleaseIsBundlePatched = false
58
+ stallionStateManager.pendingReleaseBundleDiffId = nil
52
59
  }
53
60
 
54
61
  @objc func getStallionConfig(_ promise: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
@@ -0,0 +1,35 @@
1
+ //
2
+ // StallionBSPatch.swift
3
+ // react-native-stallion
4
+ //
5
+ // Created by Thor963 on 04/11/25.
6
+ //
7
+
8
+ import Foundation
9
+
10
+ class StallionBSPatch {
11
+
12
+ static func applyPatch(oldFile: String, newFile: String, patchFile: String) -> Bool {
13
+ #if os(iOS)
14
+ // Use embedded C implementation on iOS
15
+ // Swift automatically bridges String to const char * for C functions
16
+ let result = oldFile.withCString { oldPath in
17
+ newFile.withCString { newPath in
18
+ patchFile.withCString { patchPath in
19
+ bspatch_apply(oldPath, newPath, patchPath)
20
+ }
21
+ }
22
+ }
23
+
24
+ if result == 0 {
25
+ return true
26
+ } else {
27
+ NSLog("BSPatch failed with code: %d (old: %@, patch: %@)", result, oldFile, patchFile)
28
+ return false
29
+ }
30
+ #else
31
+ NSLog("BSPatch not supported on this platform.")
32
+ return false
33
+ #endif
34
+ }
35
+ }
@@ -35,6 +35,7 @@
35
35
  - (void)setLastRolledBackHashWithTimestamp:(NSString *)lastRolledBackHash;
36
36
  - (void)markSuccessfulLaunch:(NSString *)releaseHash;
37
37
  - (NSInteger)getSuccessfulLaunchCount:(NSString *)releaseHash;
38
+ - (NSString *)getCurrentProdSlotPath;
38
39
  + (instancetype)fromDictionary:(NSDictionary *)dict;
39
40
 
40
41
  @end
@@ -6,6 +6,7 @@
6
6
  //
7
7
 
8
8
  #import "StallionMeta.h"
9
+ #import "StallionObjConstants.h"
9
10
 
10
11
  @implementation StallionMeta
11
12
 
@@ -170,4 +171,15 @@
170
171
  }
171
172
  }
172
173
 
174
+ - (NSString *)getCurrentProdSlotPath {
175
+ switch (self.currentProdSlot) {
176
+ case SlotStateNewSlot:
177
+ return StallionObjConstants.new_folder_slot;
178
+ case SlotStateStableSlot:
179
+ return StallionObjConstants.stable_folder_slot;
180
+ default:
181
+ return nil;
182
+ }
183
+ }
184
+
173
185
  @end
@@ -75,9 +75,9 @@
75
75
 
76
76
  switch (stallionMeta.currentStageSlot) {
77
77
  case SlotStateNewSlot:
78
- return [self resolveBundlePath:[NSString stringWithFormat:@"%@/%@/%@", baseFolderPath, [StallionObjConstants stage_directory], [StallionObjConstants new_folder_slot]]
79
- defaultBundlePath:defaultBundlePath
80
- releaseHash:stallionMeta.stageNewHash isProd:false];
78
+ return [self resolveBundlePath:[NSString stringWithFormat:@"%@/%@/%@", baseFolderPath, [StallionObjConstants stage_directory], [StallionObjConstants new_folder_slot]]
79
+ defaultBundlePath:defaultBundlePath
80
+ releaseHash:stallionMeta.stageNewHash isProd:false];
81
81
  default:
82
82
  return [self getDefaultBundle:defaultBundlePath];
83
83
  }
@@ -0,0 +1,206 @@
1
+ //
2
+ // StallionPatchHandler.swift
3
+ // react-native-stallion
4
+ //
5
+ // Created by Thor963 on 29/11/25.
6
+ //
7
+
8
+ import Foundation
9
+
10
+ class StallionPatchHandler {
11
+
12
+ /**
13
+ * Applies a patch to the base bundle using the diff file.
14
+ *
15
+ * @param baseBundlePath The path to the base bundle folder
16
+ * @param diffPath The path to the downloaded diff folder
17
+ * @param isBundlePatched Whether the bundle file uses bspatch (true) or file-based patching (false)
18
+ * @throws Error if patch application fails at any point
19
+ */
20
+ static func applyPatch(baseBundlePath: String, diffPath: String, isBundlePatched: Bool) throws {
21
+ let baseBundleDir = URL(fileURLWithPath: baseBundlePath)
22
+ let diffDir = URL(fileURLWithPath: diffPath)
23
+
24
+ // Validate inputs
25
+ var isDirectory: ObjCBool = false
26
+ guard FileManager.default.fileExists(atPath: baseBundlePath, isDirectory: &isDirectory),
27
+ isDirectory.boolValue else {
28
+ throw NSError(domain: "StallionPatchHandler", code: -1,
29
+ userInfo: [NSLocalizedDescriptionKey: "Base bundle path does not exist or is not a directory: \(baseBundlePath)"])
30
+ }
31
+
32
+ guard FileManager.default.fileExists(atPath: diffPath, isDirectory: &isDirectory),
33
+ isDirectory.boolValue else {
34
+ throw NSError(domain: "StallionPatchHandler", code: -2,
35
+ userInfo: [NSLocalizedDescriptionKey: "Diff path does not exist or is not a directory: \(diffPath)"])
36
+ }
37
+
38
+ // Path to manifest.json (in diffDir, not in unzip folder based on user's changes)
39
+ let manifestFile = diffDir.appendingPathComponent("manifest.json")
40
+ guard FileManager.default.fileExists(atPath: manifestFile.path) else {
41
+ throw NSError(domain: "StallionPatchHandler", code: -3,
42
+ userInfo: [NSLocalizedDescriptionKey: "Manifest file does not exist: \(manifestFile.path)"])
43
+ }
44
+
45
+ // Create a temporary directory for the patched bundle
46
+ let tempPatchedDir = diffDir.deletingLastPathComponent()
47
+ .appendingPathComponent(diffDir.lastPathComponent + "_patched_temp")
48
+
49
+ // Use defer to ensure cleanup
50
+ defer {
51
+ // Clean up temporary directory
52
+ if FileManager.default.fileExists(atPath: tempPatchedDir.path) {
53
+ try? FileManager.default.removeItem(at: tempPatchedDir)
54
+ }
55
+ }
56
+
57
+ // Copy base bundle to temporary location
58
+ StallionFileManager.copyFileOrDirectory(from: baseBundlePath, to: tempPatchedDir.path)
59
+
60
+ // Verify copy succeeded
61
+ guard FileManager.default.fileExists(atPath: tempPatchedDir.path) else {
62
+ throw NSError(domain: "StallionPatchHandler", code: -8,
63
+ userInfo: [NSLocalizedDescriptionKey: "Failed to copy base bundle to temporary directory"])
64
+ }
65
+
66
+ // Read and parse manifest.json
67
+ let manifest = try readManifest(manifestFile: manifestFile)
68
+
69
+ // Apply manifest changes (diffDir contains the files, not diffUnzipDir)
70
+ try applyManifestChanges(manifest: manifest, diffDir: diffDir, patchedDir: tempPatchedDir, isBundlePatched: isBundlePatched)
71
+
72
+ // Replace diffPath contents with patched result
73
+ // First, clear the diffPath
74
+ let diffDirContents = try FileManager.default.contentsOfDirectory(at: diffDir, includingPropertiesForKeys: nil)
75
+ for file in diffDirContents {
76
+ try? FileManager.default.removeItem(at: file)
77
+ }
78
+
79
+ // Copy patched result to diffPath
80
+ let tempPatchedContents = try FileManager.default.contentsOfDirectory(at: tempPatchedDir, includingPropertiesForKeys: nil)
81
+ for file in tempPatchedContents {
82
+ let destFile = diffDir.appendingPathComponent(file.lastPathComponent)
83
+ StallionFileManager.copyFileOrDirectory(from: file.path, to: destFile.path)
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Reads and parses the manifest.json file.
89
+ */
90
+ private static func readManifest(manifestFile: URL) throws -> [String: Any] {
91
+ let data = try Data(contentsOf: manifestFile)
92
+ guard let manifest = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
93
+ throw NSError(domain: "StallionPatchHandler", code: -4,
94
+ userInfo: [NSLocalizedDescriptionKey: "Failed to parse manifest JSON"])
95
+ }
96
+ return manifest
97
+ }
98
+
99
+ /**
100
+ * Applies the changes specified in the manifest to the patched directory.
101
+ */
102
+ private static func applyManifestChanges(manifest: [String: Any], diffDir: URL, patchedDir: URL, isBundlePatched: Bool) throws {
103
+ // Apply deletions first
104
+ if let deleted = manifest["deleted"] as? [String] {
105
+ for filePath in deleted {
106
+ let fileToDelete = patchedDir.appendingPathComponent(filePath)
107
+ if FileManager.default.fileExists(atPath: fileToDelete.path) {
108
+ try? FileManager.default.removeItem(at: fileToDelete)
109
+ }
110
+ }
111
+ }
112
+
113
+ // Apply modifications (files that already exist)
114
+ if let modified = manifest["modified"] as? [String] {
115
+ for filePath in modified {
116
+ try applyFileFromDiff(diffDir: diffDir, patchedDir: patchedDir, relativeFilePath: filePath, isBundlePatched: isBundlePatched)
117
+ }
118
+ }
119
+
120
+ // Apply additions (new files)
121
+ if let added = manifest["added"] as? [String] {
122
+ for filePath in added {
123
+ try applyFileFromDiff(diffDir: diffDir, patchedDir: patchedDir, relativeFilePath: filePath, isBundlePatched: isBundlePatched)
124
+ }
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Copies a file from the diff directory to the patched directory.
130
+ * If the file is main.jsbundle and isBundlePatched is true, applies bspatch instead of copying.
131
+ */
132
+ private static func applyFileFromDiff(diffDir: URL, patchedDir: URL, relativeFilePath: String, isBundlePatched: Bool) throws {
133
+ let sourceFile = diffDir.appendingPathComponent(relativeFilePath)
134
+ let destFile = patchedDir.appendingPathComponent(relativeFilePath)
135
+
136
+ guard FileManager.default.fileExists(atPath: sourceFile.path) else {
137
+ throw NSError(domain: "StallionPatchHandler", code: -5,
138
+ userInfo: [NSLocalizedDescriptionKey: "Source file does not exist in diff: \(sourceFile.path)"])
139
+ }
140
+
141
+ // Ensure parent directory exists
142
+ let parentDir = destFile.deletingLastPathComponent()
143
+ try? FileManager.default.createDirectory(at: parentDir, withIntermediateDirectories: true, attributes: nil)
144
+
145
+ // Check if this is the main.jsbundle file
146
+ let bundleFileName = StallionObjConstants.bundle_file_name
147
+ let normalizedPath = relativeFilePath.replacingOccurrences(of: "\\", with: "/")
148
+ let isBundleFile = normalizedPath.hasSuffix("/" + (bundleFileName ?? "")) || normalizedPath == bundleFileName
149
+
150
+ if isBundleFile && isBundlePatched {
151
+ // This is the bundle file and it's a binary patch - use bspatch
152
+ // The base bundle file should already exist in the patched directory (from the initial copy)
153
+ let baseBundleFile = destFile
154
+
155
+ guard FileManager.default.fileExists(atPath: baseBundleFile.path) else {
156
+ throw NSError(domain: "StallionPatchHandler", code: -6,
157
+ userInfo: [NSLocalizedDescriptionKey: "Base bundle file does not exist for patching: \(baseBundleFile.path)"])
158
+ }
159
+
160
+ // Create a temporary file for the patched output
161
+ let tempPatchedFile = destFile.deletingLastPathComponent()
162
+ .appendingPathComponent(destFile.lastPathComponent + ".tmp")
163
+
164
+ do {
165
+ // Apply bspatch: patch the base bundle using the diff file
166
+ let success = StallionBSPatch.applyPatch(
167
+ oldFile: baseBundleFile.path,
168
+ newFile: tempPatchedFile.path,
169
+ patchFile: sourceFile.path
170
+ )
171
+
172
+ guard success else {
173
+ throw NSError(domain: "StallionPatchHandler", code: -7,
174
+ userInfo: [NSLocalizedDescriptionKey: "Failed to apply bspatch to \(relativeFilePath)"])
175
+ }
176
+
177
+ // Replace the original file with the patched one
178
+ if FileManager.default.fileExists(atPath: destFile.path) {
179
+ try? FileManager.default.removeItem(at: destFile)
180
+ }
181
+ StallionFileManager.moveFile(from: tempPatchedFile.path, to: destFile.path)
182
+
183
+ // Verify move succeeded
184
+ guard FileManager.default.fileExists(atPath: destFile.path) else {
185
+ throw NSError(domain: "StallionPatchHandler", code: -8,
186
+ userInfo: [NSLocalizedDescriptionKey: "Failed to move patched bundle file"])
187
+ }
188
+ } catch {
189
+ // Clean up temp file on error
190
+ if FileManager.default.fileExists(atPath: tempPatchedFile.path) {
191
+ try? FileManager.default.removeItem(at: tempPatchedFile)
192
+ }
193
+ throw error
194
+ }
195
+ } else {
196
+ // Regular file - copy normally
197
+ StallionFileManager.moveFile(from: sourceFile.path, to: destFile.path)
198
+
199
+ // Verify copy succeeded
200
+ guard FileManager.default.fileExists(atPath: destFile.path) else {
201
+ throw NSError(domain: "StallionPatchHandler", code: -9,
202
+ userInfo: [NSLocalizedDescriptionKey: "Failed to copy file: \(relativeFilePath)"])
203
+ }
204
+ }
205
+ }
206
+ }