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.
- package/android/src/main/cpp/CMakeLists.txt +41 -0
- package/android/src/main/cpp/bspatch_bridge.c +268 -0
- package/android/src/main/cpp/bspatch_bridge.h +26 -0
- package/android/src/main/cpp/bzip2/blocksort.c +1094 -0
- package/android/src/main/cpp/bzip2/bzlib.c +1572 -0
- package/android/src/main/cpp/bzip2/bzlib.h +282 -0
- package/android/src/main/cpp/bzip2/bzlib_private.h +509 -0
- package/android/src/main/cpp/bzip2/compress.c +672 -0
- package/android/src/main/cpp/bzip2/crctable.c +104 -0
- package/android/src/main/cpp/bzip2/decompress.c +652 -0
- package/android/src/main/cpp/bzip2/huffman.c +205 -0
- package/android/src/main/cpp/bzip2/randtable.c +84 -0
- package/android/src/main/cpp/stallion_bspatch_jni.cpp +39 -0
- package/android/src/main/java/com/stallion/StallionModule.java +13 -1
- package/android/src/main/java/com/stallion/networkmanager/StallionPatchHandler.java +222 -0
- package/android/src/main/java/com/stallion/networkmanager/StallionSyncHandler.java +88 -16
- package/android/src/main/java/com/stallion/storage/StallionMeta.java +12 -0
- package/android/src/main/java/com/stallion/storage/StallionStateManager.java +25 -0
- package/android/src/main/java/com/stallion/utils/StallionBSPatch.java +30 -0
- package/ios/Stallion.xcodeproj/project.pbxproj +6 -0
- package/ios/main/Stallion-Bridging-Header.h +1 -0
- package/ios/main/Stallion.m +22 -0
- package/ios/main/Stallion.swift +8 -1
- package/ios/main/StallionBSPatch.swift +35 -0
- package/ios/main/StallionMeta.h +1 -0
- package/ios/main/StallionMeta.m +12 -0
- package/ios/main/StallionModule.m +3 -3
- package/ios/main/StallionPatchHandler.swift +206 -0
- package/ios/main/StallionSignatureVerification.swift +1 -1
- package/ios/main/StallionStateManager.h +3 -0
- package/ios/main/StallionStateManager.m +3 -0
- package/ios/main/StallionSyncHandler.swift +86 -10
- package/ios/main/bspatch.c +270 -0
- package/ios/main/bspatch_bridge.h +25 -0
- package/package.json +1 -1
- package/react-native-stallion.podspec +3 -1
- package/src/index.js +2 -0
- package/src/index.js.map +1 -1
- package/src/main/utils/StallionNativeUtils.js +3 -0
- package/src/main/utils/StallionNativeUtils.js.map +1 -1
- package/types/index.d.ts +1 -0
- package/types/index.d.ts.map +1 -1
- package/types/main/utils/StallionNativeUtils.d.ts +2 -1
- package/types/main/utils/StallionNativeUtils.d.ts.map +1 -1
- package/types/types/utils.types.d.ts +1 -0
- 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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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;
|
package/ios/main/Stallion.m
CHANGED
|
@@ -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
|
package/ios/main/Stallion.swift
CHANGED
|
@@ -46,9 +46,16 @@ class Stallion: RCTEventEmitter {
|
|
|
46
46
|
!pendingReleaseUrl.isEmpty,
|
|
47
47
|
!pendingReleaseHash.isEmpty else { return }
|
|
48
48
|
|
|
49
|
-
|
|
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
|
+
}
|
package/ios/main/StallionMeta.h
CHANGED
|
@@ -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
|
package/ios/main/StallionMeta.m
CHANGED
|
@@ -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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
+
}
|