@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.
@@ -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
  }
@@ -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
@@ -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
- // 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];
148
- if ([[NSFileManager defaultManager] fileExistsAtPath:candidate]) {
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
- // 2. Fallback to builtin resource path
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;AAqBlD,eAAeA,mBAAmB,CAACC,YAAY,CAAO,mBAAmB,CAAC","ignoreList":[]}
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;CAClB;;AAED,wBAA2E"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-split-bundle-loader",
3
- "version": "1.1.50",
3
+ "version": "1.1.52",
4
4
  "description": "react-native-split-bundle-loader",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -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');