@onekeyfe/react-native-split-bundle-loader 1.1.50 → 1.1.52
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/java/com/splitbundleloader/SplitBundleLoaderModule.kt +40 -1
- package/ios/SplitBundleLoader.h +14 -0
- package/ios/SplitBundleLoader.mm +109 -17
- package/lib/module/NativeSplitBundleLoader.js.map +1 -1
- package/lib/typescript/src/NativeSplitBundleLoader.d.ts +1 -0
- package/lib/typescript/src/NativeSplitBundleLoader.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeSplitBundleLoader.ts +1 -0
|
@@ -85,6 +85,26 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// -----------------------------------------------------------------------
|
|
89
|
+
// resolveSegmentPath (Phase 3)
|
|
90
|
+
// -----------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
override fun resolveSegmentPath(relativePath: String, sha256: String, promise: Promise) {
|
|
93
|
+
try {
|
|
94
|
+
val absolutePath = resolveSegmentPath(relativePath, sha256)
|
|
95
|
+
if (absolutePath != null) {
|
|
96
|
+
promise.resolve(absolutePath)
|
|
97
|
+
} else {
|
|
98
|
+
promise.reject(
|
|
99
|
+
"SPLIT_BUNDLE_NOT_FOUND",
|
|
100
|
+
"Segment file not found: $relativePath"
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
} catch (e: Exception) {
|
|
104
|
+
promise.reject("SPLIT_BUNDLE_RESOLVE_ERROR", e.message, e)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
88
108
|
// -----------------------------------------------------------------------
|
|
89
109
|
// loadSegment
|
|
90
110
|
// -----------------------------------------------------------------------
|
|
@@ -96,6 +116,10 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
|
|
|
96
116
|
sha256: String,
|
|
97
117
|
promise: Promise
|
|
98
118
|
) {
|
|
119
|
+
// NOTE (#44): sha256 param is not verified at load time by design.
|
|
120
|
+
// Per §6.4.1, runtime trusts that OTA install has already verified
|
|
121
|
+
// segment integrity. Builtin segments are signed as part of the APK/IPA.
|
|
122
|
+
// If runtime SHA-256 verification is needed, add it here.
|
|
99
123
|
try {
|
|
100
124
|
val segId = segmentId.toInt()
|
|
101
125
|
|
|
@@ -157,14 +181,29 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
|
|
|
157
181
|
// Path resolution helpers
|
|
158
182
|
// -----------------------------------------------------------------------
|
|
159
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Verify resolved path stays within the expected root directory (#45).
|
|
186
|
+
* Prevents path traversal via ".." components in relativePath.
|
|
187
|
+
*/
|
|
188
|
+
private fun isPathWithinRoot(root: File, resolved: File): Boolean {
|
|
189
|
+
return resolved.canonicalPath.startsWith(root.canonicalPath + File.separator) ||
|
|
190
|
+
resolved.canonicalPath == root.canonicalPath
|
|
191
|
+
}
|
|
192
|
+
|
|
160
193
|
private fun resolveSegmentPath(relativePath: String, expectedSha256: String): String? {
|
|
194
|
+
// Path traversal guard (#45)
|
|
195
|
+
if (relativePath.contains("..")) {
|
|
196
|
+
SBLLogger.warn("Path traversal rejected: $relativePath")
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
199
|
+
|
|
161
200
|
// 1. Try OTA bundle directory first
|
|
162
201
|
val otaBundlePath = getOtaBundlePath()
|
|
163
202
|
if (!otaBundlePath.isNullOrEmpty()) {
|
|
164
203
|
val otaRoot = File(otaBundlePath).parentFile
|
|
165
204
|
if (otaRoot != null) {
|
|
166
205
|
val candidate = File(otaRoot, relativePath)
|
|
167
|
-
if (candidate.exists()) {
|
|
206
|
+
if (candidate.exists() && isPathWithinRoot(otaRoot, candidate)) {
|
|
168
207
|
return candidate.absolutePath
|
|
169
208
|
}
|
|
170
209
|
}
|
package/ios/SplitBundleLoader.h
CHANGED
|
@@ -10,5 +10,19 @@
|
|
|
10
10
|
sha256:(NSString *)sha256
|
|
11
11
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
12
12
|
reject:(RCTPromiseRejectBlock)reject;
|
|
13
|
+
- (void)resolveSegmentPath:(NSString *)relativePath
|
|
14
|
+
sha256:(NSString *)sha256
|
|
15
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
16
|
+
reject:(RCTPromiseRejectBlock)reject;
|
|
17
|
+
|
|
18
|
+
/// Evaluate a JS bundle file inside the given RCTHost's runtime.
|
|
19
|
+
/// Used for the common + entry split-bundle loading strategy:
|
|
20
|
+
/// 1. RCTHost boots with common.jsbundle (polyfills + shared modules)
|
|
21
|
+
/// 2. After the runtime is ready, this evaluates the entry-specific bundle
|
|
22
|
+
/// (main.jsbundle or background.bundle) via jsi::Runtime::evaluateJavaScript.
|
|
23
|
+
///
|
|
24
|
+
/// @param bundlePath Absolute filesystem path to the bundle file.
|
|
25
|
+
/// @param host The RCTHost whose runtime should evaluate the bundle.
|
|
26
|
+
+ (void)loadEntryBundle:(NSString *)bundlePath inHost:(id)host;
|
|
13
27
|
|
|
14
28
|
@end
|
package/ios/SplitBundleLoader.mm
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#import "SplitBundleLoader.h"
|
|
2
2
|
#import "SBLLogger.h"
|
|
3
3
|
#import <React/RCTBridge.h>
|
|
4
|
+
#import <objc/runtime.h>
|
|
5
|
+
#include <jsi/jsi.h>
|
|
4
6
|
|
|
5
7
|
// Bridgeless (New Architecture) support: RCTHost segment registration
|
|
6
8
|
@interface RCTHost (SplitBundle)
|
|
@@ -66,6 +68,11 @@
|
|
|
66
68
|
|
|
67
69
|
/// Registers a segment with the current runtime, supporting both legacy bridge
|
|
68
70
|
/// and bridgeless (RCTHost) architectures (#13).
|
|
71
|
+
///
|
|
72
|
+
/// Thread safety (#57): This method is called from the TurboModule (JS thread).
|
|
73
|
+
/// RCTBridge.registerSegmentWithId:path: internally registers the segment with
|
|
74
|
+
/// the Hermes runtime on the JS thread, which is the correct calling context.
|
|
75
|
+
/// No queue dispatch is needed.
|
|
69
76
|
+ (BOOL)registerSegment:(int)segmentId path:(NSString *)path error:(NSError **)outError
|
|
70
77
|
{
|
|
71
78
|
// Try legacy bridge first
|
|
@@ -127,6 +134,101 @@
|
|
|
127
134
|
}
|
|
128
135
|
}
|
|
129
136
|
|
|
137
|
+
// MARK: - Path resolution helper
|
|
138
|
+
|
|
139
|
+
/// Resolves a relative segment path to an absolute path, checking OTA then builtin.
|
|
140
|
+
/// Returns nil if the segment file is not found.
|
|
141
|
+
+ (nullable NSString *)resolveAbsolutePath:(NSString *)relativePath
|
|
142
|
+
{
|
|
143
|
+
// 1. Try OTA bundle root first
|
|
144
|
+
NSString *otaPath = [SplitBundleLoader otaBundlePath];
|
|
145
|
+
if (otaPath) {
|
|
146
|
+
NSString *otaRoot = [otaPath stringByDeletingLastPathComponent];
|
|
147
|
+
NSString *candidate = [[otaRoot stringByAppendingPathComponent:relativePath] stringByStandardizingPath];
|
|
148
|
+
if ([candidate hasPrefix:otaRoot] &&
|
|
149
|
+
[[NSFileManager defaultManager] fileExistsAtPath:candidate]) {
|
|
150
|
+
return candidate;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 2. Fallback to builtin resource path
|
|
155
|
+
NSString *builtinRoot = [[NSBundle mainBundle] resourcePath];
|
|
156
|
+
NSString *candidate = [[builtinRoot stringByAppendingPathComponent:relativePath] stringByStandardizingPath];
|
|
157
|
+
if ([candidate hasPrefix:builtinRoot] &&
|
|
158
|
+
[[NSFileManager defaultManager] fileExistsAtPath:candidate]) {
|
|
159
|
+
return candidate;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return nil;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// MARK: - resolveSegmentPath (Phase 3)
|
|
166
|
+
|
|
167
|
+
- (void)resolveSegmentPath:(NSString *)relativePath
|
|
168
|
+
sha256:(NSString *)sha256
|
|
169
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
170
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
171
|
+
{
|
|
172
|
+
@try {
|
|
173
|
+
if ([relativePath containsString:@".."]) {
|
|
174
|
+
reject(@"SPLIT_BUNDLE_INVALID_PATH",
|
|
175
|
+
[NSString stringWithFormat:@"Path traversal rejected: %@", relativePath],
|
|
176
|
+
nil);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
NSString *absolutePath = [SplitBundleLoader resolveAbsolutePath:relativePath];
|
|
181
|
+
if (absolutePath) {
|
|
182
|
+
resolve(absolutePath);
|
|
183
|
+
} else {
|
|
184
|
+
reject(@"SPLIT_BUNDLE_NOT_FOUND",
|
|
185
|
+
[NSString stringWithFormat:@"Segment file not found: %@", relativePath],
|
|
186
|
+
nil);
|
|
187
|
+
}
|
|
188
|
+
} @catch (NSException *exception) {
|
|
189
|
+
reject(@"SPLIT_BUNDLE_RESOLVE_ERROR", exception.reason, nil);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// MARK: - loadEntryBundle (common + entry split loading)
|
|
194
|
+
|
|
195
|
+
+ (void)loadEntryBundle:(NSString *)bundlePath inHost:(id)host
|
|
196
|
+
{
|
|
197
|
+
if (!host || bundlePath.length == 0) {
|
|
198
|
+
[SBLLogger warn:[NSString stringWithFormat:@"loadEntryBundle: invalid arguments (host=%@, path=%@)", host, bundlePath]];
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
Ivar ivar = class_getInstanceVariable([host class], "_instance");
|
|
203
|
+
if (!ivar) {
|
|
204
|
+
[SBLLogger warn:[NSString stringWithFormat:@"loadEntryBundle: _instance ivar not found on %@", [host class]]];
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
id instance = object_getIvar(host, ivar);
|
|
209
|
+
if (!instance) {
|
|
210
|
+
[SBLLogger warn:@"loadEntryBundle: _instance is nil"];
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
NSData *data = [NSData dataWithContentsOfFile:bundlePath];
|
|
215
|
+
if (!data || data.length == 0) {
|
|
216
|
+
[SBLLogger warn:[NSString stringWithFormat:@"loadEntryBundle: failed to read bundle at %@", bundlePath]];
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
NSString *sourceURL = bundlePath.lastPathComponent ?: bundlePath;
|
|
221
|
+
[SBLLogger info:[NSString stringWithFormat:@"loadEntryBundle: evaluating %@ (%lu bytes)", sourceURL, (unsigned long)data.length]];
|
|
222
|
+
|
|
223
|
+
[instance callFunctionOnBufferedRuntimeExecutor:^(facebook::jsi::Runtime &runtime) {
|
|
224
|
+
@autoreleasepool {
|
|
225
|
+
auto buffer = std::make_shared<facebook::jsi::StringBuffer>(
|
|
226
|
+
std::string(static_cast<const char *>(data.bytes), data.length));
|
|
227
|
+
runtime.evaluateJavaScript(std::move(buffer), [sourceURL UTF8String]);
|
|
228
|
+
}
|
|
229
|
+
}];
|
|
230
|
+
}
|
|
231
|
+
|
|
130
232
|
// MARK: - loadSegment
|
|
131
233
|
|
|
132
234
|
- (void)loadSegment:(double)segmentId
|
|
@@ -138,26 +240,16 @@
|
|
|
138
240
|
{
|
|
139
241
|
@try {
|
|
140
242
|
int segId = (int)segmentId;
|
|
141
|
-
NSString *absolutePath = nil;
|
|
142
243
|
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
absolutePath = candidate;
|
|
150
|
-
}
|
|
244
|
+
// Path traversal guard (#45)
|
|
245
|
+
if ([relativePath containsString:@".."]) {
|
|
246
|
+
reject(@"SPLIT_BUNDLE_INVALID_PATH",
|
|
247
|
+
[NSString stringWithFormat:@"Path traversal rejected: %@", relativePath],
|
|
248
|
+
nil);
|
|
249
|
+
return;
|
|
151
250
|
}
|
|
152
251
|
|
|
153
|
-
|
|
154
|
-
if (!absolutePath) {
|
|
155
|
-
NSString *builtinRoot = [[NSBundle mainBundle] resourcePath];
|
|
156
|
-
NSString *candidate = [builtinRoot stringByAppendingPathComponent:relativePath];
|
|
157
|
-
if ([[NSFileManager defaultManager] fileExistsAtPath:candidate]) {
|
|
158
|
-
absolutePath = candidate;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
252
|
+
NSString *absolutePath = [SplitBundleLoader resolveAbsolutePath:relativePath];
|
|
161
253
|
|
|
162
254
|
if (!absolutePath) {
|
|
163
255
|
reject(@"SPLIT_BUNDLE_NOT_FOUND",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeSplitBundleLoader.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAAQ,cAAc;
|
|
1
|
+
{"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeSplitBundleLoader.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAAQ,cAAc;AAsBlD,eAAeA,mBAAmB,CAACC,YAAY,CAAO,mBAAmB,CAAC","ignoreList":[]}
|
|
@@ -9,6 +9,7 @@ export interface Spec extends TurboModule {
|
|
|
9
9
|
bundleVersion?: string;
|
|
10
10
|
}>;
|
|
11
11
|
loadSegment(segmentId: number, segmentKey: string, relativePath: string, sha256: string): Promise<void>;
|
|
12
|
+
resolveSegmentPath(relativePath: string, sha256: string): Promise<string>;
|
|
12
13
|
}
|
|
13
14
|
declare const _default: Spec;
|
|
14
15
|
export default _default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NativeSplitBundleLoader.d.ts","sourceRoot":"","sources":["../../../src/NativeSplitBundleLoader.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,uBAAuB,IAAI,OAAO,CAAC;QACjC,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC,CAAC;IACH,WAAW,CACT,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"NativeSplitBundleLoader.d.ts","sourceRoot":"","sources":["../../../src/NativeSplitBundleLoader.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,uBAAuB,IAAI,OAAO,CAAC;QACjC,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC,CAAC;IACH,WAAW,CACT,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3E;;AAED,wBAA2E"}
|
package/package.json
CHANGED
|
@@ -17,6 +17,7 @@ export interface Spec extends TurboModule {
|
|
|
17
17
|
relativePath: string,
|
|
18
18
|
sha256: string,
|
|
19
19
|
): Promise<void>;
|
|
20
|
+
resolveSegmentPath(relativePath: string, sha256: string): Promise<string>;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export default TurboModuleRegistry.getEnforcing<Spec>('SplitBundleLoader');
|