react-native/React/Base/RCTUtils.m

496 lines
15 KiB
Objective-C

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "RCTUtils.h"
#import <mach/mach_time.h>
#import <objc/message.h>
#import <UIKit/UIKit.h>
#import <CommonCrypto/CommonCrypto.h>
#import <zlib.h>
#import <dlfcn.h>
#import "RCTLog.h"
NSString *RCTJSONStringify(id jsonObject, NSError **error)
{
static SEL JSONKitSelector = NULL;
static NSSet *collectionTypes;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL selector = NSSelectorFromString(@"JSONStringWithOptions:error:");
if ([NSDictionary instancesRespondToSelector:selector]) {
JSONKitSelector = selector;
collectionTypes = [NSSet setWithObjects:
[NSArray class], [NSMutableArray class],
[NSDictionary class], [NSMutableDictionary class], nil];
}
});
// Use JSONKit if available and object is not a fragment
if (JSONKitSelector && [collectionTypes containsObject:[jsonObject classForCoder]]) {
return ((NSString *(*)(id, SEL, int, NSError **))objc_msgSend)(jsonObject, JSONKitSelector, 0, error);
}
// Use Foundation JSON method
NSData *jsonData = [NSJSONSerialization
dataWithJSONObject:jsonObject
options:(NSJSONWritingOptions)NSJSONReadingAllowFragments
error:error];
return jsonData ? [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] : nil;
}
static id _RCTJSONParse(NSString *jsonString, BOOL mutable, NSError **error)
{
static SEL JSONKitSelector = NULL;
static SEL JSONKitMutableSelector = NULL;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL selector = NSSelectorFromString(@"objectFromJSONStringWithParseOptions:error:");
if ([NSString instancesRespondToSelector:selector]) {
JSONKitSelector = selector;
JSONKitMutableSelector = NSSelectorFromString(@"mutableObjectFromJSONStringWithParseOptions:error:");
}
});
if (jsonString) {
// Use JSONKit if available and string is not a fragment
if (JSONKitSelector) {
NSInteger length = jsonString.length;
for (NSInteger i = 0; i < length; i++) {
unichar c = [jsonString characterAtIndex:i];
if (strchr("{[", c)) {
static const int options = (1 << 2); // loose unicode
SEL selector = mutable ? JSONKitMutableSelector : JSONKitSelector;
return ((id (*)(id, SEL, int, NSError **))objc_msgSend)(jsonString, selector, options, error);
}
if (!strchr(" \r\n\t", c)) {
break;
}
}
}
// Use Foundation JSON method
NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
if (!jsonData) {
jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
if (jsonData) {
RCTLogWarn(@"RCTJSONParse received the following string, which could "
"not be losslessly converted to UTF8 data: '%@'", jsonString);
} else {
NSString *errorMessage = @"RCTJSONParse received invalid UTF8 data";
if (error) {
*error = RCTErrorWithMessage(errorMessage);
} else {
RCTLogError(@"%@", errorMessage);
}
return nil;
}
}
NSJSONReadingOptions options = NSJSONReadingAllowFragments;
if (mutable) {
options |= NSJSONReadingMutableContainers;
}
return [NSJSONSerialization JSONObjectWithData:jsonData
options:options
error:error];
}
return nil;
}
id RCTJSONParse(NSString *jsonString, NSError **error)
{
return _RCTJSONParse(jsonString, NO, error);
}
id RCTJSONParseMutable(NSString *jsonString, NSError **error)
{
return _RCTJSONParse(jsonString, YES, error);
}
id RCTJSONClean(id object)
{
static dispatch_once_t onceToken;
static NSSet *validLeafTypes;
dispatch_once(&onceToken, ^{
validLeafTypes = [[NSSet alloc] initWithArray:@[
[NSString class],
[NSMutableString class],
[NSNumber class],
[NSNull class],
]];
});
if ([validLeafTypes containsObject:[object classForCoder]]) {
return object;
}
if ([object isKindOfClass:[NSDictionary class]]) {
__block BOOL copy = NO;
NSMutableDictionary *values = [[NSMutableDictionary alloc] initWithCapacity:[object count]];
[object enumerateKeysAndObjectsUsingBlock:^(NSString *key, id item, __unused BOOL *stop) {
id value = RCTJSONClean(item);
values[key] = value;
copy |= value != item;
}];
return copy ? values : object;
}
if ([object isKindOfClass:[NSArray class]]) {
__block BOOL copy = NO;
__block NSArray *values = object;
[object enumerateObjectsUsingBlock:^(id item, NSUInteger idx, __unused BOOL *stop) {
id value = RCTJSONClean(item);
if (copy) {
[(NSMutableArray *)values addObject:value];
} else if (value != item) {
// Converted value is different, so we'll need to copy the array
values = [[NSMutableArray alloc] initWithCapacity:values.count];
for (NSUInteger i = 0; i < idx; i++) {
[(NSMutableArray *)values addObject:object[i]];
}
[(NSMutableArray *)values addObject:value];
copy = YES;
}
}];
return values;
}
return (id)kCFNull;
}
NSString *RCTMD5Hash(NSString *string)
{
const char *str = string.UTF8String;
unsigned char result[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), result);
return [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
result[0], result[1], result[2], result[3],
result[4], result[5], result[6], result[7],
result[8], result[9], result[10], result[11],
result[12], result[13], result[14], result[15]
];
}
CGFloat RCTScreenScale()
{
static CGFloat scale;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (![NSThread isMainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{
scale = [UIScreen mainScreen].scale;
});
} else {
scale = [UIScreen mainScreen].scale;
}
});
return scale;
}
CGSize RCTScreenSize()
{
static CGSize size;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (![NSThread isMainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{
size = [UIScreen mainScreen].bounds.size;
});
} else {
size = [UIScreen mainScreen].bounds.size;
}
});
return size;
}
CGFloat RCTRoundPixelValue(CGFloat value)
{
CGFloat scale = RCTScreenScale();
return round(value * scale) / scale;
}
CGFloat RCTCeilPixelValue(CGFloat value)
{
CGFloat scale = RCTScreenScale();
return ceil(value * scale) / scale;
}
CGFloat RCTFloorPixelValue(CGFloat value)
{
CGFloat scale = RCTScreenScale();
return floor(value * scale) / scale;
}
void RCTSwapClassMethods(Class cls, SEL original, SEL replacement)
{
Method originalMethod = class_getClassMethod(cls, original);
IMP originalImplementation = method_getImplementation(originalMethod);
const char *originalArgTypes = method_getTypeEncoding(originalMethod);
Method replacementMethod = class_getClassMethod(cls, replacement);
IMP replacementImplementation = method_getImplementation(replacementMethod);
const char *replacementArgTypes = method_getTypeEncoding(replacementMethod);
if (class_addMethod(cls, original, replacementImplementation, replacementArgTypes)) {
class_replaceMethod(cls, replacement, originalImplementation, originalArgTypes);
} else {
method_exchangeImplementations(originalMethod, replacementMethod);
}
}
void RCTSwapInstanceMethods(Class cls, SEL original, SEL replacement)
{
Method originalMethod = class_getInstanceMethod(cls, original);
IMP originalImplementation = method_getImplementation(originalMethod);
const char *originalArgTypes = method_getTypeEncoding(originalMethod);
Method replacementMethod = class_getInstanceMethod(cls, replacement);
IMP replacementImplementation = method_getImplementation(replacementMethod);
const char *replacementArgTypes = method_getTypeEncoding(replacementMethod);
if (class_addMethod(cls, original, replacementImplementation, replacementArgTypes)) {
class_replaceMethod(cls, replacement, originalImplementation, originalArgTypes);
} else {
method_exchangeImplementations(originalMethod, replacementMethod);
}
}
BOOL RCTClassOverridesClassMethod(Class cls, SEL selector)
{
return RCTClassOverridesInstanceMethod(object_getClass(cls), selector);
}
BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector)
{
unsigned int numberOfMethods;
Method *methods = class_copyMethodList(cls, &numberOfMethods);
for (unsigned int i = 0; i < numberOfMethods; i++) {
if (method_getName(methods[i]) == selector) {
free(methods);
return YES;
}
}
free(methods);
return NO;
}
NSDictionary *RCTMakeError(NSString *message, id toStringify, NSDictionary *extraData)
{
if (toStringify) {
message = [message stringByAppendingString:[toStringify description]];
}
NSMutableDictionary *error = [NSMutableDictionary dictionaryWithDictionary:extraData];
error[@"message"] = message;
return error;
}
NSDictionary *RCTMakeAndLogError(NSString *message, id toStringify, NSDictionary *extraData)
{
id error = RCTMakeError(message, toStringify, extraData);
RCTLogError(@"\nError: %@", error);
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;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
isTestEnvironment = NSClassFromString(@"SenTestCase") || NSClassFromString(@"XCTest");
});
return isTestEnvironment;
}
BOOL RCTRunningInAppExtension(void)
{
return [[[[NSBundle mainBundle] bundlePath] pathExtension] isEqualToString:@"appex"];
}
id RCTSharedApplication(void)
{
if (RCTRunningInAppExtension()) {
return nil;
}
return [[UIApplication class] performSelector:@selector(sharedApplication)];
}
id RCTAlertView(NSString *title, NSString *message, id delegate, NSString *cancelButtonTitle, NSArray *otherButtonTitles)
{
if (RCTRunningInAppExtension()) {
RCTLogError(@"RCTAlertView is unavailable when running in an app extension");
return nil;
}
UIAlertView *alertView = [[UIAlertView alloc] init];
alertView.title = title;
alertView.message = message;
alertView.delegate = delegate;
if (cancelButtonTitle != nil) {
[alertView addButtonWithTitle:cancelButtonTitle];
alertView.cancelButtonIndex = 0;
}
for (NSString *buttonTitle in otherButtonTitles)
{
[alertView addButtonWithTitle:buttonTitle];
}
return alertView;
}
BOOL RCTImageHasAlpha(CGImageRef image)
{
switch (CGImageGetAlphaInfo(image)) {
case kCGImageAlphaNone:
case kCGImageAlphaNoneSkipLast:
case kCGImageAlphaNoneSkipFirst:
return NO;
default:
return YES;
}
}
NSError *RCTErrorWithMessage(NSString *message)
{
NSDictionary *errorInfo = @{NSLocalizedDescriptionKey: message};
NSError *error = [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo];
return error;
}
id RCTNullIfNil(id value)
{
return value ?: (id)kCFNull;
}
id RCTNilIfNull(id value)
{
return value == (id)kCFNull ? nil : value;
}
NSURL *RCTDataURL(NSString *mimeType, NSData *data)
{
return [NSURL URLWithString:
[NSString stringWithFormat:@"data:%@;base64,%@", mimeType,
[data base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]]];
}
BOOL RCTIsGzippedData(NSData *); // exposed for unit testing purposes
BOOL RCTIsGzippedData(NSData *data)
{
UInt8 *bytes = (UInt8 *)data.bytes;
return (data.length >= 2 && bytes[0] == 0x1f && bytes[1] == 0x8b);
}
NSData *RCTGzipData(NSData *input, float level)
{
if (input.length == 0 || RCTIsGzippedData(input)) {
return input;
}
void *libz = dlopen("/usr/lib/libz.dylib", RTLD_LAZY);
int (*deflateInit2_)(z_streamp, int, int, int, int, int, const char *, int) = dlsym(libz, "deflateInit2_");
int (*deflate)(z_streamp, int) = dlsym(libz, "deflate");
int (*deflateEnd)(z_streamp) = dlsym(libz, "deflateEnd");
z_stream stream;
stream.zalloc = Z_NULL;
stream.zfree = Z_NULL;
stream.opaque = Z_NULL;
stream.avail_in = (uint)input.length;
stream.next_in = (Bytef *)input.bytes;
stream.total_out = 0;
stream.avail_out = 0;
static const NSUInteger RCTGZipChunkSize = 16384;
NSMutableData *output = nil;
int compression = (level < 0.0f)? Z_DEFAULT_COMPRESSION: (int)(roundf(level * 9));
if (deflateInit2(&stream, compression, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY) == Z_OK) {
output = [NSMutableData dataWithLength:RCTGZipChunkSize];
while (stream.avail_out == 0) {
if (stream.total_out >= output.length) {
output.length += RCTGZipChunkSize;
}
stream.next_out = (uint8_t *)output.mutableBytes + stream.total_out;
stream.avail_out = (uInt)(output.length - stream.total_out);
deflate(&stream, Z_FINISH);
}
deflateEnd(&stream);
output.length = stream.total_out;
}
dlclose(libz);
return output;
}
NSString *RCTBundlePathForURL(NSURL *URL)
{
if (!URL.fileURL) {
// Not a file path
return nil;
}
NSString *path = URL.path;
NSString *bundlePath = [[NSBundle mainBundle] resourcePath];
if (![path hasPrefix:bundlePath]) {
// Not a bundle-relative file
return nil;
}
return [path substringFromIndex:bundlePath.length + 1];
}
BOOL RCTIsXCAssetURL(NSURL *imageURL)
{
NSString *name = RCTBundlePathForURL(imageURL);
if (name.pathComponents.count != 1) {
// URL is invalid, or is a file path, not an XCAsset identifier
return NO;
}
NSString *extension = [name pathExtension];
if (extension.length && ![extension isEqualToString:@"png"]) {
// Not a png
return NO;
}
extension = extension.length ? nil : @"png";
if ([[NSBundle mainBundle] pathForResource:name ofType:extension]) {
// File actually exists in bundle, so is not an XCAsset
return NO;
}
return YES;
}