Fix edge cases and add tests for +[RCTConvert NSURL:]

This commit is contained in:
Nick Lockwood
2015-04-25 14:52:18 -07:00
parent bd5736414a
commit 8a3b0fa9e8
3 changed files with 98 additions and 137 deletions

View File

@@ -49,11 +49,11 @@ RCT_CONVERTER(NSString *, NSString, description)
}); });
NSNumber *number = [formatter numberFromString:json]; NSNumber *number = [formatter numberFromString:json];
if (!number) { if (!number) {
RCTLogError(@"JSON String '%@' could not be interpreted as a number", json); RCTLogConvertError(json, "a number");
} }
return number; return number;
} else if (json && json != [NSNull null]) { } 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; return nil;
} }
@@ -66,30 +66,38 @@ RCT_CONVERTER(NSString *, NSString, description)
+ (NSURL *)NSURL:(id)json + (NSURL *)NSURL:(id)json
{ {
if (!json || json == (id)kCFNull) { NSString *path = [self NSString:json];
if (!path.length) {
return nil; return nil;
} }
if (![json isKindOfClass:[NSString class]]) { @try { // NSURL has a history of crashing with bad input, so let's be safe
RCTLogError(@"Expected NSString for NSURL, received %@: %@", [json classForCoder], json);
return nil;
}
NSString *path = json; NSURL *URL = [NSURL URLWithString:path];
if ([path isAbsolutePath]) 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]; return [NSURL fileURLWithPath:path];
} }
else if ([path length]) @catch (__unused NSException *e) {
{ RCTLogConvertError(json, "a valid URL");
NSURL *URL = [NSURL URLWithString:path relativeToURL:[[NSBundle mainBundle] resourceURL]]; return nil;
if ([URL isFileURL] && ![[NSFileManager defaultManager] fileExistsAtPath:[URL path]]) {
RCTLogWarn(@"The file '%@' does not exist", URL);
return nil;
}
return URL;
} }
return nil;
} }
+ (NSURLRequest *)NSURLRequest:(id)json + (NSURLRequest *)NSURLRequest:(id)json
@@ -112,11 +120,12 @@ RCT_CONVERTER(NSString *, NSString, description)
}); });
NSDate *date = [formatter dateFromString:json]; NSDate *date = [formatter dateFromString:json];
if (!date) { 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; return date;
} else if (json && json != [NSNull null]) { } 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; return nil;
} }

View File

@@ -10,47 +10,15 @@
#import "RCTJavaScriptLoader.h" #import "RCTJavaScriptLoader.h"
#import "RCTBridge.h" #import "RCTBridge.h"
#import "RCTInvalidating.h" #import "RCTConvert.h"
#import "RCTLog.h"
#import "RCTRedBox.h"
#import "RCTSourceCode.h" #import "RCTSourceCode.h"
#import "RCTUtils.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 @implementation RCTJavaScriptLoader
{ {
__weak RCTBridge *_bridge; __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 - (instancetype)initWithBridge:(RCTBridge *)bridge
{ {
if ((self = [super init])) { if ((self = [super init])) {
@@ -61,92 +29,86 @@
- (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(void (^)(NSError *))onComplete - (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:@{ 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); onComplete(error);
return; 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: NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:scriptURL completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) { ^(NSData *data, NSURLResponse *response, NSError *error) {
// Handle general request errors // Handle general request errors
if (error) { if (error) {
if ([[error domain] isEqualToString:NSURLErrorDomain]) { 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]]; 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 = @{ NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: desc, NSLocalizedDescriptionKey: desc,
NSLocalizedFailureReasonErrorKey: [error localizedDescription], NSLocalizedFailureReasonErrorKey: [error localizedDescription],
NSUnderlyingErrorKey: error, NSUnderlyingErrorKey: error,
}; };
error = [NSError errorWithDomain:@"JSServer" error = [NSError errorWithDomain:@"JSServer"
code:error.code code:error.code
userInfo:userInfo]; userInfo:userInfo];
} }
onComplete(error); onComplete(error);
return; return;
} }
// Parse response as text // Parse response as text
NSStringEncoding encoding = NSUTF8StringEncoding; NSStringEncoding encoding = NSUTF8StringEncoding;
if (response.textEncodingName != nil) { if (response.textEncodingName != nil) {
CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
if (cfEncoding != kCFStringEncodingInvalidId) { if (cfEncoding != kCFStringEncodingInvalidId) {
encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
} }
} }
NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding]; NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding];
// Handle HTTP errors // Handle HTTP errors
if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) { if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) {
NSDictionary *userInfo; NSDictionary *userInfo;
NSDictionary *errorDetails = RCTJSONParse(rawText, nil); NSDictionary *errorDetails = RCTJSONParse(rawText, nil);
if ([errorDetails isKindOfClass:[NSDictionary class]] && if ([errorDetails isKindOfClass:[NSDictionary class]] &&
[errorDetails[@"errors"] isKindOfClass:[NSArray class]]) { [errorDetails[@"errors"] isKindOfClass:[NSArray class]]) {
NSMutableArray *fakeStack = [[NSMutableArray alloc] init]; NSMutableArray *fakeStack = [[NSMutableArray alloc] init];
for (NSDictionary *err in errorDetails[@"errors"]) { for (NSDictionary *err in errorDetails[@"errors"]) {
[fakeStack addObject: @{ [fakeStack addObject: @{
@"methodName": err[@"description"] ?: @"", @"methodName": err[@"description"] ?: @"",
@"file": err[@"filename"] ?: @"", @"file": err[@"filename"] ?: @"",
@"lineNumber": err[@"lineNumber"] ?: @0 @"lineNumber": err[@"lineNumber"] ?: @0
}]; }];
} }
userInfo = @{ userInfo = @{
NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided", NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided",
@"stack": fakeStack, @"stack": fakeStack,
}; };
} else { } else {
userInfo = @{NSLocalizedDescriptionKey: rawText}; userInfo = @{NSLocalizedDescriptionKey: rawText};
} }
error = [NSError errorWithDomain:@"JSServer" error = [NSError errorWithDomain:@"JSServer"
code:[(NSHTTPURLResponse *)response statusCode] code:[(NSHTTPURLResponse *)response statusCode]
userInfo:userInfo]; userInfo:userInfo];
onComplete(error); onComplete(error);
return; return;
} }
RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])];
sourceCodeModule.scriptURL = scriptURL; sourceCodeModule.scriptURL = scriptURL;
sourceCodeModule.scriptText = rawText; sourceCodeModule.scriptText = rawText;
[_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *scriptError) { [_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *scriptError) {
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
onComplete(scriptError); onComplete(scriptError);
}); });
}]; }];
}]; }];
[task resume]; [task resume];
} }

View File

@@ -23,16 +23,6 @@
#import "RCTWebViewExecutor.h" #import "RCTWebViewExecutor.h"
#import "UIView+React.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) @interface RCTUIManager (RCTRootView)
- (NSNumber *)allocateRootTag; - (NSNumber *)allocateRootTag;