1/* 2 * Copyright (c) Meta Platforms, Inc. and affiliates. 3 * 4 * This source code is licensed under the MIT license found in the 5 * LICENSE file in the root directory of this source tree. 6 */ 7 8#import "ABI47_0_0RCTJavaScriptLoader.h" 9 10#import <sys/stat.h> 11 12#import <ABI47_0_0cxxreact/ABI47_0_0JSBundleType.h> 13 14#import "ABI47_0_0RCTBridge.h" 15#import "ABI47_0_0RCTConvert.h" 16#import "ABI47_0_0RCTMultipartDataTask.h" 17#import "ABI47_0_0RCTPerformanceLogger.h" 18#import "ABI47_0_0RCTUtils.h" 19 20NSString *const ABI47_0_0RCTJavaScriptLoaderErrorDomain = @"ABI47_0_0RCTJavaScriptLoaderErrorDomain"; 21 22const uint32_t ABI47_0_0RCT_BYTECODE_ALIGNMENT = 4; 23 24@interface ABI47_0_0RCTSource () { 25 @public 26 NSURL *_url; 27 NSData *_data; 28 NSUInteger _length; 29 NSInteger _filesChangedCount; 30} 31 32@end 33 34@implementation ABI47_0_0RCTSource 35 36static ABI47_0_0RCTSource *ABI47_0_0RCTSourceCreate(NSURL *url, NSData *data, int64_t length) NS_RETURNS_RETAINED 37{ 38 using ABI47_0_0facebook::ABI47_0_0React::ScriptTag; 39 ABI47_0_0facebook::ABI47_0_0React::BundleHeader header; 40 [data getBytes:&header length:sizeof(header)]; 41 42 ABI47_0_0RCTSource *source = [ABI47_0_0RCTSource new]; 43 source->_url = url; 44 // Multipart responses may give us an unaligned view into the buffer. This ensures memory is aligned. 45 if (parseTypeFromHeader(header) == ScriptTag::MetroHBCBundle && ((long)[data bytes] % ABI47_0_0RCT_BYTECODE_ALIGNMENT)) { 46 source->_data = [[NSData alloc] initWithData:data]; 47 } else { 48 source->_data = data; 49 } 50 source->_length = length; 51 source->_filesChangedCount = ABI47_0_0RCTSourceFilesChangedCountNotBuiltByBundler; 52 return source; 53} 54 55@end 56 57@implementation ABI47_0_0RCTLoadingProgress 58 59- (NSString *)description 60{ 61 NSMutableString *desc = [NSMutableString new]; 62 [desc appendString:_status ?: @"Bundling"]; 63 64 if ([_total integerValue] > 0 && [_done integerValue] > [_total integerValue]) { 65 [desc appendFormat:@" %ld%%", (long)100]; 66 } else if ([_total integerValue] > 0) { 67 [desc appendFormat:@" %ld%%", (long)(100 * [_done integerValue] / [_total integerValue])]; 68 } else { 69 [desc appendFormat:@" %ld%%", (long)0]; 70 } 71 [desc appendString:@"\u2026"]; 72 return desc; 73} 74 75@end 76 77@implementation ABI47_0_0RCTJavaScriptLoader 78 79ABI47_0_0RCT_NOT_IMPLEMENTED(-(instancetype)init) 80 81+ (void)loadBundleAtURL:(NSURL *)scriptURL 82 onProgress:(ABI47_0_0RCTSourceLoadProgressBlock)onProgress 83 onComplete:(ABI47_0_0RCTSourceLoadBlock)onComplete 84{ 85 int64_t sourceLength; 86 NSError *error; 87 NSData *data = [self attemptSynchronousLoadOfBundleAtURL:scriptURL sourceLength:&sourceLength error:&error]; 88 if (data) { 89 onComplete(nil, ABI47_0_0RCTSourceCreate(scriptURL, data, sourceLength)); 90 return; 91 } 92 93 const BOOL isCannotLoadSyncError = [error.domain isEqualToString:ABI47_0_0RCTJavaScriptLoaderErrorDomain] && 94 error.code == ABI47_0_0RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously; 95 96 if (isCannotLoadSyncError) { 97 attemptAsynchronousLoadOfBundleAtURL(scriptURL, onProgress, onComplete); 98 } else { 99 onComplete(error, nil); 100 } 101} 102 103+ (NSData *)attemptSynchronousLoadOfBundleAtURL:(NSURL *)scriptURL 104 sourceLength:(int64_t *)sourceLength 105 error:(NSError **)error 106{ 107 NSString *unsanitizedScriptURLString = scriptURL.absoluteString; 108 // Sanitize the script URL 109 scriptURL = sanitizeURL(scriptURL); 110 111 if (!scriptURL) { 112 if (error) { 113 *error = [NSError 114 errorWithDomain:ABI47_0_0RCTJavaScriptLoaderErrorDomain 115 code:ABI47_0_0RCTJavaScriptLoaderErrorNoScriptURL 116 userInfo:@{ 117 NSLocalizedDescriptionKey : [NSString 118 stringWithFormat:@"No script URL provided. Make sure the packager is " 119 @"running or you have embedded a JS bundle in your application bundle.\n\n" 120 @"unsanitizedScriptURLString = %@", 121 unsanitizedScriptURLString] 122 }]; 123 } 124 return nil; 125 } 126 127 // Load local script file 128 if (!scriptURL.fileURL) { 129 if (error) { 130 *error = [NSError errorWithDomain:ABI47_0_0RCTJavaScriptLoaderErrorDomain 131 code:ABI47_0_0RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously 132 userInfo:@{ 133 NSLocalizedDescriptionKey : 134 [NSString stringWithFormat:@"Cannot load %@ URLs synchronously", scriptURL.scheme] 135 }]; 136 } 137 return nil; 138 } 139 140 // Load the first 4 bytes to check if the bundle is regular or RAM ("Random Access Modules" bundle). 141 // The RAM bundle has a magic number in the 4 first bytes `(0xFB0BD1E5)`. 142 // The benefit of RAM bundle over a regular bundle is that we can lazily inject 143 // modules into JSC as they're required. 144 FILE *bundle = fopen(scriptURL.path.UTF8String, "r"); 145 if (!bundle) { 146 if (error) { 147 *error = [NSError 148 errorWithDomain:ABI47_0_0RCTJavaScriptLoaderErrorDomain 149 code:ABI47_0_0RCTJavaScriptLoaderErrorFailedOpeningFile 150 userInfo:@{ 151 NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error opening bundle %@", scriptURL.path] 152 }]; 153 } 154 return nil; 155 } 156 157 ABI47_0_0facebook::ABI47_0_0React::BundleHeader header; 158 size_t readResult = fread(&header, sizeof(header), 1, bundle); 159 fclose(bundle); 160 if (readResult != 1) { 161 if (error) { 162 *error = [NSError 163 errorWithDomain:ABI47_0_0RCTJavaScriptLoaderErrorDomain 164 code:ABI47_0_0RCTJavaScriptLoaderErrorFailedReadingFile 165 userInfo:@{ 166 NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error reading bundle %@", scriptURL.path] 167 }]; 168 } 169 return nil; 170 } 171 172 ABI47_0_0facebook::ABI47_0_0React::ScriptTag tag = ABI47_0_0facebook::ABI47_0_0React::parseTypeFromHeader(header); 173 switch (tag) { 174 case ABI47_0_0facebook::ABI47_0_0React::ScriptTag::MetroHBCBundle: 175 case ABI47_0_0facebook::ABI47_0_0React::ScriptTag::RAMBundle: 176 break; 177 178 case ABI47_0_0facebook::ABI47_0_0React::ScriptTag::String: { 179#if ABI47_0_0RCT_ENABLE_INSPECTOR 180 NSData *source = [NSData dataWithContentsOfFile:scriptURL.path options:NSDataReadingMappedIfSafe error:error]; 181 if (sourceLength && source != nil) { 182 *sourceLength = source.length; 183 } 184 return source; 185#else 186 if (error) { 187 *error = 188 [NSError errorWithDomain:ABI47_0_0RCTJavaScriptLoaderErrorDomain 189 code:ABI47_0_0RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously 190 userInfo:@{NSLocalizedDescriptionKey : @"Cannot load text/javascript files synchronously"}]; 191 } 192 return nil; 193#endif 194 } 195 } 196 197 struct stat statInfo; 198 if (stat(scriptURL.path.UTF8String, &statInfo) != 0) { 199 if (error) { 200 *error = [NSError 201 errorWithDomain:ABI47_0_0RCTJavaScriptLoaderErrorDomain 202 code:ABI47_0_0RCTJavaScriptLoaderErrorFailedStatingFile 203 userInfo:@{ 204 NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error stating bundle %@", scriptURL.path] 205 }]; 206 } 207 return nil; 208 } 209 if (sourceLength) { 210 *sourceLength = statInfo.st_size; 211 } 212 return [NSData dataWithBytes:&header length:sizeof(header)]; 213} 214 215static void parseHeaders(NSDictionary *headers, ABI47_0_0RCTSource *source) 216{ 217 source->_filesChangedCount = [headers[@"X-Metro-Files-Changed-Count"] integerValue]; 218} 219 220static void attemptAsynchronousLoadOfBundleAtURL( 221 NSURL *scriptURL, 222 ABI47_0_0RCTSourceLoadProgressBlock onProgress, 223 ABI47_0_0RCTSourceLoadBlock onComplete) 224{ 225 scriptURL = sanitizeURL(scriptURL); 226 227 if (scriptURL.fileURL) { 228 // Reading in a large bundle can be slow. Dispatch to the background queue to do it. 229 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 230 NSError *error = nil; 231 NSData *source = [NSData dataWithContentsOfFile:scriptURL.path options:NSDataReadingMappedIfSafe error:&error]; 232 onComplete(error, ABI47_0_0RCTSourceCreate(scriptURL, source, source.length)); 233 }); 234 return; 235 } 236 237 ABI47_0_0RCTMultipartDataTask *task = [[ABI47_0_0RCTMultipartDataTask alloc] initWithURL:scriptURL 238 partHandler:^(NSInteger statusCode, NSDictionary *headers, NSData *data, NSError *error, BOOL done) { 239 if (!done) { 240 if (onProgress) { 241 onProgress(progressEventFromData(data)); 242 } 243 return; 244 } 245 246 // Handle general request errors 247 if (error) { 248 if ([error.domain isEqualToString:NSURLErrorDomain]) { 249 error = [NSError 250 errorWithDomain:ABI47_0_0RCTJavaScriptLoaderErrorDomain 251 code:ABI47_0_0RCTJavaScriptLoaderErrorURLLoadFailed 252 userInfo:@{ 253 NSLocalizedDescriptionKey : 254 [@"Could not connect to development server.\n\n" 255 "Ensure the following:\n" 256 "- Node server is running and available on the same network - run 'npm start' from ABI47_0_0React-native root\n" 257 "- Node server URL is correctly set in AppDelegate\n" 258 "- WiFi is enabled and connected to the same network as the Node Server\n\n" 259 "URL: " stringByAppendingString:scriptURL.absoluteString], 260 NSLocalizedFailureReasonErrorKey : error.localizedDescription, 261 NSUnderlyingErrorKey : error, 262 }]; 263 } 264 onComplete(error, nil); 265 return; 266 } 267 268 // For multipart responses packager sets X-Http-Status header in case HTTP status code 269 // is different from 200 OK 270 NSString *statusCodeHeader = headers[@"X-Http-Status"]; 271 if (statusCodeHeader) { 272 statusCode = [statusCodeHeader integerValue]; 273 } 274 275 if (statusCode != 200) { 276 error = 277 [NSError errorWithDomain:@"JSServer" 278 code:statusCode 279 userInfo:userInfoForRawResponse([[NSString alloc] initWithData:data 280 encoding:NSUTF8StringEncoding])]; 281 onComplete(error, nil); 282 return; 283 } 284 285 // Validate that the packager actually returned javascript. 286 NSString *contentType = headers[@"Content-Type"]; 287 NSString *mimeType = [[contentType componentsSeparatedByString:@";"] firstObject]; 288 if (![mimeType isEqualToString:@"application/javascript"] && ![mimeType isEqualToString:@"text/javascript"] && 289 ![mimeType isEqualToString:@"application/x-metro-bytecode-bundle"]) { 290 NSString *description; 291 if ([mimeType isEqualToString:@"application/json"]) { 292 NSError *parseError; 293 NSDictionary *jsonError = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError]; 294 if (!parseError && [jsonError isKindOfClass:[NSDictionary class]] && 295 [[jsonError objectForKey:@"message"] isKindOfClass:[NSString class]] && 296 [[jsonError objectForKey:@"message"] length]) { 297 description = [jsonError objectForKey:@"message"]; 298 } else { 299 description = [NSString stringWithFormat:@"Unknown error fetching '%@'.", scriptURL.absoluteString]; 300 } 301 } else { 302 description = [NSString 303 stringWithFormat: 304 @"Expected MIME-Type to be 'application/javascript' or 'text/javascript', but got '%@'.", mimeType]; 305 } 306 307 error = [NSError 308 errorWithDomain:@"JSServer" 309 code:NSURLErrorCannotParseResponse 310 userInfo:@{NSLocalizedDescriptionKey : description, @"headers" : headers, @"data" : data}]; 311 onComplete(error, nil); 312 return; 313 } 314 315 ABI47_0_0RCTSource *source = ABI47_0_0RCTSourceCreate(scriptURL, data, data.length); 316 parseHeaders(headers, source); 317 onComplete(nil, source); 318 } 319 progressHandler:^(NSDictionary *headers, NSNumber *loaded, NSNumber *total) { 320 // Only care about download progress events for the javascript bundle part. 321 if ([headers[@"Content-Type"] isEqualToString:@"application/javascript"] || 322 [headers[@"Content-Type"] isEqualToString:@"application/x-metro-bytecode-bundle"]) { 323 onProgress(progressEventFromDownloadProgress(loaded, total)); 324 } 325 }]; 326 327 [task startTask]; 328} 329 330static NSURL *sanitizeURL(NSURL *url) 331{ 332 // Why we do this is lost to time. We probably shouldn't; passing a valid URL is the caller's responsibility not ours. 333 return [ABI47_0_0RCTConvert NSURL:url.absoluteString]; 334} 335 336static ABI47_0_0RCTLoadingProgress *progressEventFromData(NSData *rawData) 337{ 338 NSString *text = [[NSString alloc] initWithData:rawData encoding:NSUTF8StringEncoding]; 339 id info = ABI47_0_0RCTJSONParse(text, nil); 340 if (!info || ![info isKindOfClass:[NSDictionary class]]) { 341 return nil; 342 } 343 344 ABI47_0_0RCTLoadingProgress *progress = [ABI47_0_0RCTLoadingProgress new]; 345 progress.status = info[@"status"]; 346 progress.done = info[@"done"]; 347 progress.total = info[@"total"]; 348 return progress; 349} 350 351static ABI47_0_0RCTLoadingProgress *progressEventFromDownloadProgress(NSNumber *total, NSNumber *done) 352{ 353 ABI47_0_0RCTLoadingProgress *progress = [ABI47_0_0RCTLoadingProgress new]; 354 progress.status = @"Downloading"; 355 // Progress values are in bytes transform them to kilobytes for smaller numbers. 356 progress.done = done != nil ? @([done integerValue] / 1024) : nil; 357 progress.total = total != nil ? @([total integerValue] / 1024) : nil; 358 return progress; 359} 360 361static NSDictionary *userInfoForRawResponse(NSString *rawText) 362{ 363 NSDictionary *parsedResponse = ABI47_0_0RCTJSONParse(rawText, nil); 364 if (![parsedResponse isKindOfClass:[NSDictionary class]]) { 365 return @{NSLocalizedDescriptionKey : rawText}; 366 } 367 NSArray *errors = parsedResponse[@"errors"]; 368 if (![errors isKindOfClass:[NSArray class]]) { 369 return @{NSLocalizedDescriptionKey : rawText}; 370 } 371 NSMutableArray<NSDictionary *> *fakeStack = [NSMutableArray new]; 372 for (NSDictionary *err in errors) { 373 [fakeStack addObject:@{ 374 @"methodName" : err[@"description"] ?: @"", 375 @"file" : err[@"filename"] ?: @"", 376 @"lineNumber" : err[@"lineNumber"] ?: @0 377 }]; 378 } 379 return 380 @{NSLocalizedDescriptionKey : parsedResponse[@"message"] ?: @"No message provided", @"stack" : [fakeStack copy]}; 381} 382 383@end 384