From 90dd7a13f0a3997517f8c560bc53e213efc88049 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Mon, 13 Jul 2015 10:33:39 -0700 Subject: [PATCH] Added support for URLs pointing to files inside application home --- .../RCTConvert_NSURLTests.m | 14 +++- React/Base/RCTConvert.m | 65 ++++++++++++------- React/Base/RCTUtils.h | 6 +- React/Base/RCTUtils.m | 44 ++++++++----- 4 files changed, 83 insertions(+), 46 deletions(-) diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_NSURLTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_NSURLTests.m index ac98f184e..781a13d60 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_NSURLTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_NSURLTests.m @@ -15,6 +15,7 @@ #import #import "RCTConvert.h" +#import "RCTUtils.h" @interface RCTConvert_NSURLTests : XCTestCase @@ -42,7 +43,7 @@ TEST_PATH(name, _input, [[[NSBundle mainBundle] bundlePath] stringByAppendingPat TEST_URL(basic, @"http://example.com", @"http://example.com") TEST_URL(null, (id)kCFNull, nil) -// Local files +// Resource files TEST_PATH(fileURL, @"file:///blah/hello.jsbundle", @"/blah/hello.jsbundle") TEST_BUNDLE_PATH(filePath, @"blah/hello.jsbundle", @"blah/hello.jsbundle") TEST_BUNDLE_PATH(filePathWithSpaces, @"blah blah/hello.jsbundle", @"blah blah/hello.jsbundle") @@ -50,6 +51,9 @@ TEST_BUNDLE_PATH(filePathWithEncodedSpaces, @"blah%20blah/hello.jsbundle", @"bla TEST_BUNDLE_PATH(imageAt2XPath, @"images/foo@2x.jpg", @"images/foo@2x.jpg") TEST_BUNDLE_PATH(imageFile, @"foo.jpg", @"foo.jpg") +// User documents +TEST_PATH(documentsFolder, @"~/Documents", [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]) + // Remote files TEST_URL(fullURL, @"http://example.com/blah/hello.jsbundle", @"http://example.com/blah/hello.jsbundle") TEST_URL(urlWithSpaces, @"http://example.com/blah blah/foo", @"http://example.com/blah%20blah/foo") @@ -57,4 +61,12 @@ TEST_URL(urlWithEncodedSpaces, @"http://example.com/blah%20blah/foo", @"http://e TEST_URL(imageURL, @"http://example.com/foo@2x.jpg", @"http://example.com/foo@2x.jpg") TEST_URL(imageURLWithSpaces, @"http://example.com/blah foo@2x.jpg", @"http://example.com/blah%20foo@2x.jpg") +// Data URLs +- (void)testDataURL +{ + NSURL *expectedURL = RCTDataURL(@"text/plain", [@"abcde" dataUsingEncoding:NSUTF8StringEncoding]); + NSURL *testURL = [NSURL URLWithString:@"data:text/plain;base64,YWJjZGU="]; + XCTAssertEqualObjects([testURL absoluteString], [expectedURL absoluteString]); +} + @end diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index e548c3956..06e080546 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -107,7 +107,11 @@ RCT_CONVERTER(NSString *, NSString, description) // Assume that it's a local path path = [path stringByRemovingPercentEncoding]; - if (![path isAbsolutePath]) { + if ([path hasPrefix:@"~"]) { + // Path is inside user directory + path = [path stringByExpandingTildeInPath]; + } else if (![path isAbsolutePath]) { + // Assume it's a resource path path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:path]; } return [NSURL fileURLWithPath:path]; @@ -652,43 +656,54 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[ return nil; } - if (RCT_DEBUG && ![json isKindOfClass:[NSString class]] && ![json isKindOfClass:[NSDictionary class]]) { - RCTLogConvertError(json, "an image"); - return nil; - } - UIImage *image; NSString *path; CGFloat scale = 0.0; if ([json isKindOfClass:[NSString class]]) { - if ([json length] == 0) { - return nil; - } path = json; - } else { + } else if ([json isKindOfClass:[NSDictionary class]]) { path = [self NSString:json[@"uri"]]; scale = [self CGFloat:json[@"scale"]]; + } else { + RCTLogConvertError(json, "an image"); } - if ([path hasPrefix:@"data:"]) { - NSURL *url = [NSURL URLWithString:path]; - NSData *imageData = [NSData dataWithContentsOfURL:url]; - image = [UIImage imageWithData:imageData]; - } else if ([path isAbsolutePath] || [path hasPrefix:@"~"]) { - image = [UIImage imageWithContentsOfFile:path.stringByExpandingTildeInPath]; - } else { - image = [UIImage imageNamed:path]; - if (!image) { - image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:path ofType:nil]]; + NSURL *URL = [self NSURL:path]; + NSString *scheme = [URL.scheme lowercaseString]; + if ([scheme isEqualToString:@"file"]) { + + if ([NSThread currentThread] == [NSThread mainThread]) { + // Image may reside inside a .car file, in which case we have no choice + // but to use +[UIImage imageNamed] - but this method isn't thread safe + image = [UIImage imageNamed:path]; } + + if (!image) { + // Attempt to load from the file system + if ([path pathExtension].length == 0) { + path = [path stringByAppendingPathExtension:@"png"]; + } + image = [UIImage imageWithContentsOfFile:path]; + } + + // We won't warn about nil images because there are legitimate cases + // where we find out if a string is an image by using this method, but + // we do enforce thread-safe API usage with the following check + if (RCT_DEBUG && !image && [UIImage imageNamed:path]) { + RCTAssertMainThread(); + } + + } else if ([scheme isEqualToString:@"data"]) { + image = [UIImage imageWithData:[NSData dataWithContentsOfURL:URL]]; + } else { + RCTLogConvertError(json, "an image. Only local files or data URIs are supported"); } - + if (scale > 0) { - image = [UIImage imageWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation]; + image = [UIImage imageWithCGImage:image.CGImage + scale:scale + orientation:image.imageOrientation]; } - - // NOTE: we don't warn about nil images because there are legitimate - // case where we find out if a string is an image by using this method return image; } diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index 8bdd91ccf..e21a07222 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -47,6 +47,7 @@ RCT_EXTERN BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector); // Creates a standardized error object RCT_EXTERN NSDictionary *RCTMakeError(NSString *message, id toStringify, NSDictionary *extraData); RCT_EXTERN NSDictionary *RCTMakeAndLogError(NSString *message, id toStringify, NSDictionary *extraData); +RCT_EXTERN NSDictionary *RCTJSErrorFromNSError(NSError *error); // Returns YES if React is running in a test environment RCT_EXTERN BOOL RCTRunningInTestEnvironment(void); @@ -58,7 +59,8 @@ RCT_EXTERN BOOL RCTImageHasAlpha(CGImageRef image); RCT_EXTERN NSError *RCTErrorWithMessage(NSString *message); // Convert nil values to NSNull, and vice-versa -RCT_EXTERN id RCTNullIfNil(id value); RCT_EXTERN id RCTNilIfNull(id value); +RCT_EXTERN id RCTNullIfNil(id value); -RCT_EXTERN NSDictionary *RCTJSErrorFromNSError(NSError *error); +// Convert data to a Base64-encoded data URL +RCT_EXTERN NSURL *RCTDataURL(NSString *mimeType, NSData *data); diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index 0b7ae89c7..7fc620bc0 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -238,6 +238,27 @@ NSDictionary *RCTMakeAndLogError(NSString *message, id toStringify, NSDictionary return error; } +// TODO: Can we just replace RCTMakeError with this function instead? +NSDictionary *RCTJSErrorFromNSError(NSError *error) +{ + NSString *errorMessage; + NSArray *stackTrace = [NSThread callStackSymbols]; + NSMutableDictionary *errorInfo = + [NSMutableDictionary dictionaryWithObject:stackTrace forKey:@"nativeStackIOS"]; + + if (error) { + errorMessage = error.localizedDescription ?: @"Unknown error from a native module"; + errorInfo[@"domain"] = error.domain ?: RCTErrorDomain; + errorInfo[@"code"] = @(error.code); + } else { + errorMessage = @"Unknown error from a native module"; + errorInfo[@"domain"] = RCTErrorDomain; + errorInfo[@"code"] = @-1; + } + + return RCTMakeError(errorMessage, nil, errorInfo); +} + BOOL RCTRunningInTestEnvironment(void) { static BOOL isTestEnvironment = NO; @@ -277,23 +298,10 @@ id RCTNilIfNull(id value) return value == (id)kCFNull ? nil : value; } -// TODO: Can we just replace RCTMakeError with this function instead? -NSDictionary *RCTJSErrorFromNSError(NSError *error) +NSURL *RCTDataURL(NSString *mimeType, NSData *data) { - NSString *errorMessage; - NSArray *stackTrace = [NSThread callStackSymbols]; - NSMutableDictionary *errorInfo = - [NSMutableDictionary dictionaryWithObject:stackTrace forKey:@"nativeStackIOS"]; - - if (error) { - errorMessage = error.localizedDescription ?: @"Unknown error from a native module"; - errorInfo[@"domain"] = error.domain ?: RCTErrorDomain; - errorInfo[@"code"] = @(error.code); - } else { - errorMessage = @"Unknown error from a native module"; - errorInfo[@"domain"] = RCTErrorDomain; - errorInfo[@"code"] = @-1; - } - - return RCTMakeError(errorMessage, nil, errorInfo); + return [NSURL URLWithString: + [NSString stringWithFormat:@"data:%@;base64,%@", mimeType, + [data base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]]]; } +