Skip to content

Commit

Permalink
Fix edge cases and add tests for +[RCTConvert NSURL:]
Browse files Browse the repository at this point in the history
  • Loading branch information
nicklockwood committed Apr 25, 2015
1 parent bd57364 commit 8a3b0fa
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 140 deletions.
51 changes: 30 additions & 21 deletions React/Base/RCTConvert.m
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ + (NSNumber *)NSNumber:(id)json
});
NSNumber *number = [formatter numberFromString:json];
if (!number) {
RCTLogError(@"JSON String '%@' could not be interpreted as a number", json);
RCTLogConvertError(json, "a number");
}
return number;
} else if (json && json != [NSNull null]) {
RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a number", json, [json classForCoder]);
RCTLogConvertError(json, "a number");
}
return nil;
}
Expand All @@ -66,30 +66,38 @@ + (NSData *)NSData:(id)json

+ (NSURL *)NSURL:(id)json
{
if (!json || json == (id)kCFNull) {
NSString *path = [self NSString:json];
if (!path.length) {
return nil;
}

if (![json isKindOfClass:[NSString class]]) {
RCTLogError(@"Expected NSString for NSURL, received %@: %@", [json classForCoder], json);
return nil;
}
@try { // NSURL has a history of crashing with bad input, so let's be safe

NSString *path = json;
if ([path isAbsolutePath])
{
NSURL *URL = [NSURL URLWithString:path];
if (URL.scheme) { // Was a well-formed absolute URL
return URL;
}

// Check if it has a scheme
if ([path rangeOfString:@"[a-zA-Z][a-zA-Z._-]+:" options:NSRegularExpressionSearch].location == 0) {
path = [path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
URL = [NSURL URLWithString:path];
if (URL) {
return URL;
}
}

// Assume that it's a local path
path = [path stringByRemovingPercentEncoding];
if (![path isAbsolutePath]) {
path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:path];
}
return [NSURL fileURLWithPath:path];
}
else if ([path length])
{
NSURL *URL = [NSURL URLWithString:path relativeToURL:[[NSBundle mainBundle] resourceURL]];
if ([URL isFileURL] && ![[NSFileManager defaultManager] fileExistsAtPath:[URL path]]) {
RCTLogWarn(@"The file '%@' does not exist", URL);
return nil;
}
return URL;
@catch (__unused NSException *e) {
RCTLogConvertError(json, "a valid URL");
return nil;
}
return nil;
}

+ (NSURLRequest *)NSURLRequest:(id)json
Expand All @@ -112,11 +120,12 @@ + (NSDate *)NSDate:(id)json
});
NSDate *date = [formatter dateFromString:json];
if (!date) {
RCTLogError(@"JSON String '%@' could not be interpreted as a date. Expected format: YYYY-MM-DD'T'HH:mm:ss.sssZ", json);
RCTLogError(@"JSON String '%@' could not be interpreted as a date. "
"Expected format: YYYY-MM-DD'T'HH:mm:ss.sssZ", json);
}
return date;
} else if (json && json != [NSNull null]) {
RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a date", json, [json classForCoder]);
RCTLogConvertError(json, "a date");
}
return nil;
}
Expand Down
180 changes: 71 additions & 109 deletions React/Base/RCTJavaScriptLoader.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,47 +10,15 @@
#import "RCTJavaScriptLoader.h"

#import "RCTBridge.h"
#import "RCTInvalidating.h"
#import "RCTLog.h"
#import "RCTRedBox.h"
#import "RCTConvert.h"
#import "RCTSourceCode.h"
#import "RCTUtils.h"

#define NO_REMOTE_MODULE @"Could not fetch module bundle %@. Ensure node server is running.\n\nIf it timed out, try reloading."
#define NO_LOCAL_BUNDLE @"Could not load local bundle %@. Ensure file exists."

#define CACHE_DIR @"RCTJSBundleCache"

#pragma mark - Application Engine

/**
* TODO:
* - Add window resize rotation events matching the DOM API.
* - Device pixel ration hooks.
* - Source maps.
*/
@implementation RCTJavaScriptLoader
{
__weak RCTBridge *_bridge;
}

/**
* `CADisplayLink` code copied from Ejecta but we've placed the JavaScriptCore
* engine in its own dedicated thread.
*
* TODO: Try adding to the `RCTJavaScriptExecutor`'s thread runloop. Removes one
* additional GCD dispatch per frame and likely makes it so that other UIThread
* operations don't delay the dispatch (so we can begin working in JS much
* faster.) Event handling must still be sent via a GCD dispatch, of course.
*
* We must add the display link to two runloops in order to get setTimeouts to
* fire during scrolling. (`NSDefaultRunLoopMode` and `UITrackingRunLoopMode`)
* TODO: We can invent a `requestAnimationFrame` and
* `requestAvailableAnimationFrame` to control if callbacks can be fired during
* an animation.
* http://stackoverflow.com/questions/12622800/why-does-uiscrollview-pause-my-cadisplaylink
*
*/
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if ((self = [super init])) {
Expand All @@ -61,92 +29,86 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge

- (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(void (^)(NSError *))onComplete
{
if (scriptURL == nil) {
// Sanitize the script URL
scriptURL = [RCTConvert NSURL:scriptURL.absoluteString];

if (!scriptURL ||
([scriptURL isFileURL] && ![[NSFileManager defaultManager] fileExistsAtPath:scriptURL.path])) {
NSError *error = [NSError errorWithDomain:@"JavaScriptLoader" code:1 userInfo:@{
NSLocalizedDescriptionKey: @"No script URL provided"
NSLocalizedDescriptionKey: scriptURL ? [NSString stringWithFormat:@"Script at '%@' could not be found.", scriptURL] : @"No script URL provided"
}];
onComplete(error);
return;
}

if ([scriptURL isFileURL]) {
NSString *bundlePath = [[NSBundle bundleForClass:[self class]] resourcePath];
NSString *localPath = [scriptURL.absoluteString substringFromIndex:@"file://".length];

if (![localPath hasPrefix:bundlePath]) {
NSString *absolutePath = [NSString stringWithFormat:@"%@/%@", bundlePath, localPath];
scriptURL = [NSURL fileURLWithPath:absolutePath];
}
}

NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:scriptURL completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {

// Handle general request errors
if (error) {
if ([[error domain] isEqualToString:NSURLErrorDomain]) {
NSString *desc = [@"Could not connect to development server. Ensure node server is running and available on the same network - run 'npm start' from react-native root\n\nURL: " stringByAppendingString:[scriptURL absoluteString]];
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: desc,
NSLocalizedFailureReasonErrorKey: [error localizedDescription],
NSUnderlyingErrorKey: error,
};
error = [NSError errorWithDomain:@"JSServer"
code:error.code
userInfo:userInfo];
}
onComplete(error);
return;
}

// Parse response as text
NSStringEncoding encoding = NSUTF8StringEncoding;
if (response.textEncodingName != nil) {
CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
if (cfEncoding != kCFStringEncodingInvalidId) {
encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
}
}
NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding];

// Handle HTTP errors
if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) {
NSDictionary *userInfo;
NSDictionary *errorDetails = RCTJSONParse(rawText, nil);
if ([errorDetails isKindOfClass:[NSDictionary class]] &&
[errorDetails[@"errors"] isKindOfClass:[NSArray class]]) {
NSMutableArray *fakeStack = [[NSMutableArray alloc] init];
for (NSDictionary *err in errorDetails[@"errors"]) {
[fakeStack addObject: @{
@"methodName": err[@"description"] ?: @"",
@"file": err[@"filename"] ?: @"",
@"lineNumber": err[@"lineNumber"] ?: @0
}];
}
userInfo = @{
NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided",
@"stack": fakeStack,
};
} else {
userInfo = @{NSLocalizedDescriptionKey: rawText};
}
error = [NSError errorWithDomain:@"JSServer"
code:[(NSHTTPURLResponse *)response statusCode]
userInfo:userInfo];

onComplete(error);
return;
}
RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])];
sourceCodeModule.scriptURL = scriptURL;
sourceCodeModule.scriptText = rawText;
// Handle general request errors
if (error) {
if ([[error domain] isEqualToString:NSURLErrorDomain]) {
NSString *desc = [@"Could not connect to development server. Ensure node server is running and available on the same network - run 'npm start' from react-native root\n\nURL: " stringByAppendingString:[scriptURL absoluteString]];
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: desc,
NSLocalizedFailureReasonErrorKey: [error localizedDescription],
NSUnderlyingErrorKey: error,
};
error = [NSError errorWithDomain:@"JSServer"
code:error.code
userInfo:userInfo];
}
onComplete(error);
return;
}

[_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *scriptError) {
dispatch_async(dispatch_get_main_queue(), ^{
onComplete(scriptError);
});
}];
}];
// Parse response as text
NSStringEncoding encoding = NSUTF8StringEncoding;
if (response.textEncodingName != nil) {
CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
if (cfEncoding != kCFStringEncodingInvalidId) {
encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
}
}
NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding];

// Handle HTTP errors
if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) {
NSDictionary *userInfo;
NSDictionary *errorDetails = RCTJSONParse(rawText, nil);
if ([errorDetails isKindOfClass:[NSDictionary class]] &&
[errorDetails[@"errors"] isKindOfClass:[NSArray class]]) {
NSMutableArray *fakeStack = [[NSMutableArray alloc] init];
for (NSDictionary *err in errorDetails[@"errors"]) {
[fakeStack addObject: @{
@"methodName": err[@"description"] ?: @"",
@"file": err[@"filename"] ?: @"",
@"lineNumber": err[@"lineNumber"] ?: @0
}];
}
userInfo = @{
NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided",
@"stack": fakeStack,
};
} else {
userInfo = @{NSLocalizedDescriptionKey: rawText};
}
error = [NSError errorWithDomain:@"JSServer"
code:[(NSHTTPURLResponse *)response statusCode]
userInfo:userInfo];

onComplete(error);
return;
}
RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])];
sourceCodeModule.scriptURL = scriptURL;
sourceCodeModule.scriptText = rawText;

[_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *scriptError) {
dispatch_async(dispatch_get_main_queue(), ^{
onComplete(scriptError);
});
}];
}];

[task resume];
}
Expand Down
10 changes: 0 additions & 10 deletions React/Base/RCTRootView.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,6 @@
#import "RCTWebViewExecutor.h"
#import "UIView+React.h"

/**
* HACK(t6568049) This should be removed soon, hiding to prevent people from
* relying on it
*/
@interface RCTBridge (RCTRootView)

- (void)setJavaScriptExecutor:(id<RCTJavaScriptExecutor>)executor;

@end

@interface RCTUIManager (RCTRootView)

- (NSNumber *)allocateRootTag;
Expand Down

0 comments on commit 8a3b0fa

Please sign in to comment.