2015-04-19 19:55:46 +00:00
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
2018-02-17 02:24:55 +00:00
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
2015-04-19 19:55:46 +00:00
*/
2015-04-02 14:33:21 +00:00
#import "RCTJavaScriptLoader.h"
2016-11-23 19:33:44 +00:00
#import <sys/stat.h>
#import <cxxreact/JSBundleType.h>
#import <jschelpers/JavaScriptCore.h>
2015-04-02 14:33:21 +00:00
#import "RCTBridge.h"
2015-04-25 21:52:18 +00:00
#import "RCTConvert.h"
2016-10-11 19:14:37 +00:00
#import "RCTMultipartDataTask.h"
2016-11-23 19:33:44 +00:00
#import "RCTPerformanceLogger.h"
#import "RCTUtils.h"
2016-03-17 17:34:46 +00:00
2016-07-12 12:13:32 +00:00
NSString *const RCTJavaScriptLoaderErrorDomain = @"RCTJavaScriptLoaderErrorDomain";
2017-08-30 13:15:56 +00:00
@interface RCTSource()
{
@public
NSURL *_url;
NSData *_data;
NSUInteger _length;
2017-08-31 12:25:19 +00:00
NSInteger _filesChangedCount;
2017-08-30 13:15:56 +00:00
}
@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;
2017-08-31 12:25:19 +00:00
source->_filesChangedCount = RCTSourceFilesChangedCountNotBuiltByBundler;
2017-08-30 13:15:56 +00:00
return source;
}
@end
2016-10-13 18:42:30 +00:00
@implementation RCTLoadingProgress
- (NSString *)description
{
NSMutableString *desc = [NSMutableString new];
[desc appendString:_status ?: @"Loading"];
2017-01-03 18:23:08 +00:00
if ([_total integerValue] > 0) {
2016-10-13 18:42:30 +00:00
[desc appendFormat:@" %ld%% (%@/%@)", (long)(100 * [_done integerValue] / [_total integerValue]), _done, _total];
}
2016-11-23 14:31:43 +00:00
[desc appendString:@"\u2026"];
2016-10-13 18:42:30 +00:00
return desc;
}
@end
2015-04-02 14:33:21 +00:00
@implementation RCTJavaScriptLoader
2015-08-24 10:14:33 +00:00
RCT_NOT_IMPLEMENTED(- (instancetype)init)
2015-06-15 14:53:45 +00:00
2016-10-13 18:42:30 +00:00
+ (void)loadBundleAtURL:(NSURL *)scriptURL onProgress:(RCTSourceLoadProgressBlock)onProgress onComplete:(RCTSourceLoadBlock)onComplete
2016-07-12 12:13:32 +00:00
{
int64_t sourceLength;
NSError *error;
NSData *data = [self attemptSynchronousLoadOfBundleAtURL:scriptURL
2016-11-11 13:21:37 +00:00
runtimeBCVersion:JSNoBytecodeFileFormatVersion
2016-07-12 12:13:32 +00:00
sourceLength:&sourceLength
error:&error];
if (data) {
2017-08-30 13:15:56 +00:00
onComplete(nil, RCTSourceCreate(scriptURL, data, sourceLength));
2016-07-12 12:13:32 +00:00
return;
}
const BOOL isCannotLoadSyncError =
[error.domain isEqualToString:RCTJavaScriptLoaderErrorDomain]
&& error.code == RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously;
if (isCannotLoadSyncError) {
2016-10-13 18:42:30 +00:00
attemptAsynchronousLoadOfBundleAtURL(scriptURL, onProgress, onComplete);
2016-07-12 12:13:32 +00:00
} else {
2017-08-30 13:15:56 +00:00
onComplete(error, nil);
2016-07-12 12:13:32 +00:00
}
}
+ (NSData *)attemptSynchronousLoadOfBundleAtURL:(NSURL *)scriptURL
2016-11-11 13:21:37 +00:00
runtimeBCVersion:(int32_t)runtimeBCVersion
2016-07-12 12:13:32 +00:00
sourceLength:(int64_t *)sourceLength
error:(NSError **)error
2015-04-02 14:33:21 +00:00
{
2016-05-23 18:03:17 +00:00
NSString *unsanitizedScriptURLString = scriptURL.absoluteString;
2015-04-25 21:52:18 +00:00
// Sanitize the script URL
2016-07-12 12:13:32 +00:00
scriptURL = sanitizeURL(scriptURL);
2015-04-25 21:52:18 +00:00
2015-10-08 19:28:38 +00:00
if (!scriptURL) {
2016-07-12 12:13:32 +00:00
if (error) {
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
code:RCTJavaScriptLoaderErrorNoScriptURL
userInfo:@{NSLocalizedDescriptionKey:
2016-08-02 15:05:33 +00:00
[NSString stringWithFormat:@"No script URL provided. Make sure the packager is "
2017-06-14 12:20:47 +00:00
@"running or you have embedded a JS bundle in your application bundle.\n\n"
@"unsanitizedScriptURLString = %@", unsanitizedScriptURLString]}];
2016-07-12 12:13:32 +00:00
}
return nil;
2015-04-11 22:08:00 +00:00
}
2015-10-08 19:28:38 +00:00
// Load local script file
2016-07-12 12:13:32 +00:00
if (!scriptURL.fileURL) {
if (error) {
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
code:RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously
userInfo:@{NSLocalizedDescriptionKey:
2016-07-13 23:39:12 +00:00
[NSString stringWithFormat:@"Cannot load %@ URLs synchronously",
scriptURL.scheme]}];
2016-07-07 20:31:21 +00:00
}
2016-07-12 12:13:32 +00:00
return nil;
}
2016-07-07 20:31:21 +00:00
2016-07-12 12:13:32 +00:00
// 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]}];
2016-07-07 20:31:21 +00:00
}
2016-07-12 12:13:32 +00:00
return nil;
}
2016-07-07 20:31:21 +00:00
Simplifying Struct definition.
Summary:
Since we are reading from a file, we should make sure this struct is packed, just in case we change it down the line and the compiler decides it might want to introduce padding, we're now protected against that.
There was also a discussion about the fact that people might use `ptr += sizeof(BundleHeader)` as an idiom in their code, which would currently be incorrect, if padding was introduced at the end of the file. Actually, it remains incorrect to do that now, because a RAM bundle header is a different size to a BC Bundle header. If people are properly testing their code, they should spot this pretty quickly, because it will always be an incorrect thing to do with a RAM bundle, so this isn't as bad as previously thought: where the code only succeeds when the compiler deigns to not pad the struct at the end.
This diff also cleans up how headers are initialised. `BundleHeader` has a constructor that explicitly zero-initialises it so we can rely on the default initializer to do the right thing now.
Reviewed By: mhorowitz
Differential Revision: D4572032
fbshipit-source-id: 7dc50cfa9438dfdfb9f842dc39d8f15334813c63
2017-02-20 12:28:32 +00:00
facebook::react::BundleHeader header;
2016-11-11 13:21:37 +00:00
size_t readResult = fread(&header, sizeof(header), 1, bundle);
2016-07-12 12:13:32 +00:00
fclose(bundle);
if (readResult != 1) {
if (error) {
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
code:RCTJavaScriptLoaderErrorFailedReadingFile
userInfo:@{NSLocalizedDescriptionKey:
[NSString stringWithFormat:@"Error reading bundle %@", scriptURL.path]}];
2016-07-07 20:31:21 +00:00
}
2016-07-12 12:13:32 +00:00
return nil;
2015-10-08 19:28:38 +00:00
}
2016-11-23 14:31:43 +00:00
facebook::react::ScriptTag tag = facebook::react::parseTypeFromHeader(header);
2016-11-11 13:21:37 +00:00
switch (tag) {
2016-11-23 14:31:43 +00:00
case facebook::react::ScriptTag::RAMBundle:
2016-11-11 13:21:37 +00:00
break;
2017-09-18 14:52:04 +00:00
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
2016-07-12 12:13:32 +00:00
if (error) {
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
code:RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously
userInfo:@{NSLocalizedDescriptionKey:
2016-11-01 17:14:00 +00:00
@"Cannot load text/javascript files synchronously"}];
2015-04-25 21:52:18 +00:00
}
2016-07-12 12:13:32 +00:00
return nil;
2017-09-18 14:52:04 +00:00
#endif
}
2016-11-23 14:31:43 +00:00
case facebook::react::ScriptTag::BCBundle:
2017-01-12 06:28:07 +00:00
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;
}
Simplifying Struct definition.
Summary:
Since we are reading from a file, we should make sure this struct is packed, just in case we change it down the line and the compiler decides it might want to introduce padding, we're now protected against that.
There was also a discussion about the fact that people might use `ptr += sizeof(BundleHeader)` as an idiom in their code, which would currently be incorrect, if padding was introduced at the end of the file. Actually, it remains incorrect to do that now, because a RAM bundle header is a different size to a BC Bundle header. If people are properly testing their code, they should spot this pretty quickly, because it will always be an incorrect thing to do with a RAM bundle, so this isn't as bad as previously thought: where the code only succeeds when the compiler deigns to not pad the struct at the end.
This diff also cleans up how headers are initialised. `BundleHeader` has a constructor that explicitly zero-initialises it so we can rely on the default initializer to do the right thing now.
Reviewed By: mhorowitz
Differential Revision: D4572032
fbshipit-source-id: 7dc50cfa9438dfdfb9f842dc39d8f15334813c63
2017-02-20 12:28:32 +00:00
else if ((uint32_t)runtimeBCVersion != header.version) {
2016-11-11 13:21:37 +00:00
if (error) {
NSString *errDesc =
2017-01-12 06:28:07 +00:00
[NSString stringWithFormat:@"BC Version Mismatch. Expect: %d, Actual: %u",
Simplifying Struct definition.
Summary:
Since we are reading from a file, we should make sure this struct is packed, just in case we change it down the line and the compiler decides it might want to introduce padding, we're now protected against that.
There was also a discussion about the fact that people might use `ptr += sizeof(BundleHeader)` as an idiom in their code, which would currently be incorrect, if padding was introduced at the end of the file. Actually, it remains incorrect to do that now, because a RAM bundle header is a different size to a BC Bundle header. If people are properly testing their code, they should spot this pretty quickly, because it will always be an incorrect thing to do with a RAM bundle, so this isn't as bad as previously thought: where the code only succeeds when the compiler deigns to not pad the struct at the end.
This diff also cleans up how headers are initialised. `BundleHeader` has a constructor that explicitly zero-initialises it so we can rely on the default initializer to do the right thing now.
Reviewed By: mhorowitz
Differential Revision: D4572032
fbshipit-source-id: 7dc50cfa9438dfdfb9f842dc39d8f15334813c63
2017-02-20 12:28:32 +00:00
runtimeBCVersion, header.version];
2016-11-11 13:21:37 +00:00
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
code:RCTJavaScriptLoaderErrorBCVersion
userInfo:@{NSLocalizedDescriptionKey: errDesc}];
}
return nil;
}
break;
2016-07-12 12:13:32 +00:00
}
2015-04-25 21:52:18 +00:00
2016-07-12 12:13:32 +00:00
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]}];
2015-04-25 21:52:18 +00:00
}
2016-07-12 12:13:32 +00:00
return nil;
}
if (sourceLength) {
*sourceLength = statInfo.st_size;
}
2016-11-11 13:21:37 +00:00
return [NSData dataWithBytes:&header length:sizeof(header)];
2016-07-12 12:13:32 +00:00
}
2017-08-31 12:25:19 +00:00
static void parseHeaders(NSDictionary *headers, RCTSource *source) {
source->_filesChangedCount = [headers[@"X-Metro-Files-Changed-Count"] integerValue];
}
2016-10-13 18:42:30 +00:00
static void attemptAsynchronousLoadOfBundleAtURL(NSURL *scriptURL, RCTSourceLoadProgressBlock onProgress, RCTSourceLoadBlock onComplete)
2016-07-12 12:13:32 +00:00
{
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];
2017-08-30 13:15:56 +00:00
onComplete(error, RCTSourceCreate(scriptURL, source, source.length));
2016-07-12 12:13:32 +00:00
});
return;
}
2015-04-02 14:33:21 +00:00
2016-10-11 19:14:37 +00:00
RCTMultipartDataTask *task = [[RCTMultipartDataTask alloc] initWithURL:scriptURL partHandler:^(NSInteger statusCode, NSDictionary *headers, NSData *data, NSError *error, BOOL done) {
if (!done) {
2016-10-13 18:42:30 +00:00
if (onProgress) {
onProgress(progressEventFromData(data));
}
2016-10-11 19:14:37 +00:00
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"
2017-05-31 03:31:25 +00:00
"- Node server URL is correctly set in AppDelegate\n"
"- WiFi is enabled and connected to the same network as the Node Server\n\n"
2016-10-11 19:14:37 +00:00
"URL: " stringByAppendingString:scriptURL.absoluteString],
NSLocalizedFailureReasonErrorKey: error.localizedDescription,
NSUnderlyingErrorKey: error,
}];
}
2017-08-30 13:15:56 +00:00
onComplete(error, nil);
2016-10-11 19:14:37 +00:00
return;
}
// For multipart responses packager sets X-Http-Status header in case HTTP status code
// is different from 200 OK
2017-02-15 11:41:32 +00:00
NSString *statusCodeHeader = headers[@"X-Http-Status"];
2016-10-11 19:14:37 +00:00
if (statusCodeHeader) {
statusCode = [statusCodeHeader integerValue];
}
if (statusCode != 200) {
error = [NSError errorWithDomain:@"JSServer"
code:statusCode
userInfo:userInfoForRawResponse([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding])];
2017-08-30 13:15:56 +00:00
onComplete(error, nil);
2016-10-11 19:14:37 +00:00
return;
}
2017-07-20 01:42:49 +00:00
// Validate that the packager actually returned javascript.
NSString *contentType = headers[@"Content-Type"];
2017-09-20 02:45:52 +00:00
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];
2017-07-20 01:42:49 +00:00
error = [NSError errorWithDomain:@"JSServer"
code:NSURLErrorCannotParseResponse
userInfo:@{
2017-07-26 18:39:07 +00:00
NSLocalizedDescriptionKey: description,
2017-07-20 01:42:49 +00:00
@"headers": headers,
@"data": data
2017-09-20 02:45:52 +00:00
}];
2017-08-30 13:15:56 +00:00
onComplete(error, nil);
2017-07-20 01:42:49 +00:00
return;
}
2017-08-31 12:25:19 +00:00
RCTSource *source = RCTSourceCreate(scriptURL, data, data.length);
parseHeaders(headers, source);
onComplete(nil, source);
2017-08-14 17:54:33 +00:00
} 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));
}
2016-10-11 19:14:37 +00:00
}];
[task startTask];
2015-04-02 14:33:21 +00:00
}
2016-07-12 12:13:32 +00:00
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];
}
2016-10-13 18:42:30 +00:00
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];
2017-02-15 11:41:32 +00:00
progress.status = info[@"status"];
progress.done = info[@"done"];
progress.total = info[@"total"];
2016-10-13 18:42:30 +00:00
return progress;
}
2017-08-14 17:54:33 +00:00
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;
}
2016-07-12 12:13:32 +00:00
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<NSDictionary *> *fakeStack = [NSMutableArray new];
for (NSDictionary *err in errors) {
2017-02-15 11:41:32 +00:00
[fakeStack addObject: @{
2016-07-12 12:13:32 +00:00
@"methodName": err[@"description"] ?: @"",
@"file": err[@"filename"] ?: @"",
@"lineNumber": err[@"lineNumber"] ?: @0
2017-02-15 11:41:32 +00:00
}];
2016-07-12 12:13:32 +00:00
}
return @{NSLocalizedDescriptionKey: parsedResponse[@"message"] ?: @"No message provided", @"stack": [fakeStack copy]};
}
2015-04-02 14:33:21 +00:00
@end