/* * 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 "RCTBridge.h" #import "RCTConvert.h" #import "RCTMultipartDataTask.h" #import "RCTPerformanceLogger.h" #import "RCTUtils.h" NSString *const RCTJavaScriptLoaderErrorDomain = @"RCTJavaScriptLoaderErrorDomain"; const UInt32 RCT_BYTECODE_ALIGNMENT = 4; UInt32 RCTReadUInt32LE(NSData *script, UInt32 offset) { return [script length] < offset + 4 ? 0 : CFSwapInt32LittleToHost(*(((uint32_t *)[script bytes]) + offset / 4)); } bool RCTIsBytecodeBundle(NSData *script) { static const UInt32 BYTECODE_BUNDLE_MAGIC_NUMBER = 0xffe7c3c3; return ( [script length] > 8 && RCTReadUInt32LE(script, 0) == BYTECODE_BUNDLE_MAGIC_NUMBER && RCTReadUInt32LE(script, 4) > 0); } @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; // Multipart responses may give us an unaligned view into the buffer. This ensures memory is aligned. source->_data = (RCTIsBytecodeBundle(data) && ((long)[data bytes] % RCT_BYTECODE_ALIGNMENT)) ? [[NSData alloc] initWithData:data] : data; source->_length = length; source->_filesChangedCount = RCTSourceFilesChangedCountNotBuiltByBundler; return source; } @end @implementation RCTLoadingProgress - (NSString *)description { NSMutableString *desc = [NSMutableString new]; [desc appendString:_status ?: @"Bundling"]; if ([_total integerValue] > 0 && [_done integerValue] > [_total integerValue]) { [desc appendFormat:@" %ld%%", (long)100]; } else if ([_total integerValue] > 0) { [desc appendFormat:@" %ld%%", (long)(100 * [_done integerValue] / [_total integerValue])]; } else { [desc appendFormat:@" %ld%%", (long)0]; } [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 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 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 } } 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"] && ![mimeType isEqualToString:@"application/x-metro-bytecode-bundle"]) { NSString *description; if ([mimeType isEqualToString:@"application/json"]) { NSError *parseError; NSDictionary *jsonError = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError]; if (!parseError && [jsonError isKindOfClass:[NSDictionary class]] && [[jsonError objectForKey:@"message"] isKindOfClass:[NSString class]] && [[jsonError objectForKey:@"message"] length]) { description = [jsonError objectForKey:@"message"]; } else { description = [NSString stringWithFormat:@"Unknown error fetching '%@'.", scriptURL.absoluteString]; } } else { 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"] || [headers[@"Content-Type"] isEqualToString:@"application/x-metro-bytecode-bundle"]) { 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"; // 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