/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "RCTJavaScriptLoader.h" #import #import #import #import "RCTBridge.h" #import "RCTConvert.h" #import "RCTMultipartDataTask.h" #import "RCTPerformanceLogger.h" #import "RCTUtils.h" NSString *const RCTJavaScriptLoaderErrorDomain = @"RCTJavaScriptLoaderErrorDomain"; @interface RCTSource() { @public NSURL *_url; NSData *_data; NSUInteger _length; NSInteger _filesChangedCount; } @end @implementation RCTSource static RCTSource *RCTSourceCreate(NSURL *url, NSData *data, int64_t length) NS_RETURNS_RETAINED { RCTSource *source = [RCTSource new]; source->_url = url; source->_data = data; source->_length = length; source->_filesChangedCount = RCTSourceFilesChangedCountNotBuiltByBundler; return source; } @end @implementation RCTLoadingProgress - (NSString *)description { NSMutableString *desc = [NSMutableString new]; [desc appendString:_status ?: @"Loading"]; if ([_total integerValue] > 0) { [desc appendFormat:@" %ld%% (%@/%@)", (long)(100 * [_done integerValue] / [_total integerValue]), _done, _total]; } [desc appendString:@"\u2026"]; return desc; } @end @implementation RCTJavaScriptLoader RCT_NOT_IMPLEMENTED(- (instancetype)init) + (void)loadBundleAtURL:(NSURL *)scriptURL onProgress:(RCTSourceLoadProgressBlock)onProgress onComplete:(RCTSourceLoadBlock)onComplete { int64_t sourceLength; NSError *error; NSData *data = [self attemptSynchronousLoadOfBundleAtURL:scriptURL runtimeBCVersion:JSNoBytecodeFileFormatVersion sourceLength:&sourceLength error:&error]; if (data) { onComplete(nil, RCTSourceCreate(scriptURL, data, sourceLength)); return; } const BOOL isCannotLoadSyncError = [error.domain isEqualToString:RCTJavaScriptLoaderErrorDomain] && error.code == RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously; if (isCannotLoadSyncError) { attemptAsynchronousLoadOfBundleAtURL(scriptURL, onProgress, onComplete); } else { onComplete(error, nil); } } + (NSData *)attemptSynchronousLoadOfBundleAtURL:(NSURL *)scriptURL runtimeBCVersion:(int32_t)runtimeBCVersion sourceLength:(int64_t *)sourceLength error:(NSError **)error { NSString *unsanitizedScriptURLString = scriptURL.absoluteString; // Sanitize the script URL scriptURL = sanitizeURL(scriptURL); if (!scriptURL) { if (error) { *error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain code:RCTJavaScriptLoaderErrorNoScriptURL userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"No script URL provided. Make sure the packager is " @"running or you have embedded a JS bundle in your application bundle.\n\n" @"unsanitizedScriptURLString = %@", unsanitizedScriptURLString]}]; } return nil; } // Load local script file if (!scriptURL.fileURL) { if (error) { *error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain code:RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Cannot load %@ URLs synchronously", scriptURL.scheme]}]; } return nil; } // Load the first 4 bytes to check if the bundle is regular or RAM ("Random Access Modules" bundle). // The RAM bundle has a magic number in the 4 first bytes `(0xFB0BD1E5)`. // The benefit of RAM bundle over a regular bundle is that we can lazily inject // modules into JSC as they're required. FILE *bundle = fopen(scriptURL.path.UTF8String, "r"); if (!bundle) { if (error) { *error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain code:RCTJavaScriptLoaderErrorFailedOpeningFile userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error opening bundle %@", scriptURL.path]}]; } return nil; } facebook::react::BundleHeader header; size_t readResult = fread(&header, sizeof(header), 1, bundle); fclose(bundle); if (readResult != 1) { if (error) { *error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain code:RCTJavaScriptLoaderErrorFailedReadingFile userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error reading bundle %@", scriptURL.path]}]; } return nil; } facebook::react::ScriptTag tag = facebook::react::parseTypeFromHeader(header); switch (tag) { case facebook::react::ScriptTag::RAMBundle: break; case facebook::react::ScriptTag::String: { #if RCT_ENABLE_INSPECTOR NSData *source = [NSData dataWithContentsOfFile:scriptURL.path options:NSDataReadingMappedIfSafe error:error]; if (sourceLength && source != nil) { *sourceLength = source.length; } return source; #else if (error) { *error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain code:RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously userInfo:@{NSLocalizedDescriptionKey: @"Cannot load text/javascript files synchronously"}]; } return nil; #endif } case facebook::react::ScriptTag::BCBundle: if (runtimeBCVersion == JSNoBytecodeFileFormatVersion || runtimeBCVersion < 0) { if (error) { *error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain code:RCTJavaScriptLoaderErrorBCNotSupported userInfo:@{NSLocalizedDescriptionKey: @"Bytecode bundles are not supported by this runtime."}]; } return nil; } else if ((uint32_t)runtimeBCVersion != header.version) { if (error) { NSString *errDesc = [NSString stringWithFormat:@"BC Version Mismatch. Expect: %d, Actual: %u", runtimeBCVersion, header.version]; *error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain code:RCTJavaScriptLoaderErrorBCVersion userInfo:@{NSLocalizedDescriptionKey: errDesc}]; } return nil; } break; } struct stat statInfo; if (stat(scriptURL.path.UTF8String, &statInfo) != 0) { if (error) { *error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain code:RCTJavaScriptLoaderErrorFailedStatingFile userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error stating bundle %@", scriptURL.path]}]; } return nil; } if (sourceLength) { *sourceLength = statInfo.st_size; } return [NSData dataWithBytes:&header length:sizeof(header)]; } static void parseHeaders(NSDictionary *headers, RCTSource *source) { source->_filesChangedCount = [headers[@"X-Metro-Files-Changed-Count"] integerValue]; } static void attemptAsynchronousLoadOfBundleAtURL(NSURL *scriptURL, RCTSourceLoadProgressBlock onProgress, RCTSourceLoadBlock onComplete) { scriptURL = sanitizeURL(scriptURL); if (scriptURL.fileURL) { // Reading in a large bundle can be slow. Dispatch to the background queue to do it. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSError *error = nil; NSData *source = [NSData dataWithContentsOfFile:scriptURL.path options:NSDataReadingMappedIfSafe error:&error]; onComplete(error, RCTSourceCreate(scriptURL, source, source.length)); }); return; } RCTMultipartDataTask *task = [[RCTMultipartDataTask alloc] initWithURL:scriptURL partHandler:^(NSInteger statusCode, NSDictionary *headers, NSData *data, NSError *error, BOOL done) { if (!done) { if (onProgress) { onProgress(progressEventFromData(data)); } return; } // Handle general request errors if (error) { if ([error.domain isEqualToString:NSURLErrorDomain]) { error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain code:RCTJavaScriptLoaderErrorURLLoadFailed userInfo: @{ NSLocalizedDescriptionKey: [@"Could not connect to development server.\n\n" "Ensure the following:\n" "- Node server is running and available on the same network - run 'npm start' from react-native root\n" "- Node server URL is correctly set in AppDelegate\n" "- WiFi is enabled and connected to the same network as the Node Server\n\n" "URL: " stringByAppendingString:scriptURL.absoluteString], NSLocalizedFailureReasonErrorKey: error.localizedDescription, NSUnderlyingErrorKey: error, }]; } onComplete(error, nil); return; } // For multipart responses packager sets X-Http-Status header in case HTTP status code // is different from 200 OK NSString *statusCodeHeader = headers[@"X-Http-Status"]; if (statusCodeHeader) { statusCode = [statusCodeHeader integerValue]; } if (statusCode != 200) { error = [NSError errorWithDomain:@"JSServer" code:statusCode userInfo:userInfoForRawResponse([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding])]; onComplete(error, nil); return; } // Validate that the packager actually returned javascript. NSString *contentType = headers[@"Content-Type"]; NSString *mimeType = [[contentType componentsSeparatedByString:@";"] firstObject]; if (![mimeType isEqualToString:@"application/javascript"] && ![mimeType isEqualToString:@"text/javascript"]) { NSString *description = [NSString stringWithFormat:@"Expected MIME-Type to be 'application/javascript' or 'text/javascript', but got '%@'.", mimeType]; error = [NSError errorWithDomain:@"JSServer" code:NSURLErrorCannotParseResponse userInfo:@{ NSLocalizedDescriptionKey: description, @"headers": headers, @"data": data }]; onComplete(error, nil); return; } RCTSource *source = RCTSourceCreate(scriptURL, data, data.length); parseHeaders(headers, source); onComplete(nil, source); } progressHandler:^(NSDictionary *headers, NSNumber *loaded, NSNumber *total) { // Only care about download progress events for the javascript bundle part. if ([headers[@"Content-Type"] isEqualToString:@"application/javascript"]) { onProgress(progressEventFromDownloadProgress(loaded, total)); } }]; [task startTask]; } static NSURL *sanitizeURL(NSURL *url) { // Why we do this is lost to time. We probably shouldn't; passing a valid URL is the caller's responsibility not ours. return [RCTConvert NSURL:url.absoluteString]; } static RCTLoadingProgress *progressEventFromData(NSData *rawData) { NSString *text = [[NSString alloc] initWithData:rawData encoding:NSUTF8StringEncoding]; id info = RCTJSONParse(text, nil); if (!info || ![info isKindOfClass:[NSDictionary class]]) { return nil; } RCTLoadingProgress *progress = [RCTLoadingProgress new]; progress.status = info[@"status"]; progress.done = info[@"done"]; progress.total = info[@"total"]; return progress; } static RCTLoadingProgress *progressEventFromDownloadProgress(NSNumber *total, NSNumber *done) { RCTLoadingProgress *progress = [RCTLoadingProgress new]; progress.status = @"Downloading JavaScript bundle"; // Progress values are in bytes transform them to kilobytes for smaller numbers. progress.done = done != nil ? @([done integerValue] / 1024) : nil; progress.total = total != nil ? @([total integerValue] / 1024) : nil; return progress; } static NSDictionary *userInfoForRawResponse(NSString *rawText) { NSDictionary *parsedResponse = RCTJSONParse(rawText, nil); if (![parsedResponse isKindOfClass:[NSDictionary class]]) { return @{NSLocalizedDescriptionKey: rawText}; } NSArray *errors = parsedResponse[@"errors"]; if (![errors isKindOfClass:[NSArray class]]) { return @{NSLocalizedDescriptionKey: rawText}; } NSMutableArray *fakeStack = [NSMutableArray new]; for (NSDictionary *err in errors) { [fakeStack addObject: @{ @"methodName": err[@"description"] ?: @"", @"file": err[@"filename"] ?: @"", @"lineNumber": err[@"lineNumber"] ?: @0 }]; } return @{NSLocalizedDescriptionKey: parsedResponse[@"message"] ?: @"No message provided", @"stack": [fakeStack copy]}; } @end