mirror of
https://github.com/Tencent/Hippy.git
synced 2026-10-28 21:48:13 +08:00
257 lines
10 KiB
Objective-C
257 lines
10 KiB
Objective-C
/*!
|
|
* iOS SDK
|
|
*
|
|
* Tencent is pleased to support the open source community by making
|
|
* Hippy available.
|
|
*
|
|
* Copyright (C) 2019 THL A29 Limited, a Tencent company.
|
|
* All rights reserved.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#import <CommonCrypto/CommonCrypto.h>
|
|
|
|
#import "HippyUtils.h"
|
|
#import "HPAsserts.h"
|
|
#import "HPLog.h"
|
|
|
|
#include <objc/message.h>
|
|
|
|
NSString *const HippyErrorUnspecified = @"HippyErrorUnspecified";
|
|
|
|
static NSString *__nullable _HPJSONStringifyNoRetry(id __nullable jsonObject, NSError **error) {
|
|
if (!jsonObject) {
|
|
return nil;
|
|
}
|
|
|
|
static SEL JSONKitSelector = NULL;
|
|
static NSSet<Class> *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];
|
|
}
|
|
});
|
|
|
|
@try {
|
|
// 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;
|
|
} @catch (NSException *exception) {
|
|
// Convert exception to error
|
|
if (error) {
|
|
*error = [NSError errorWithDomain:HPErrorDomain code:0 userInfo:@ { NSLocalizedDescriptionKey: exception.description ?: @"" }];
|
|
}
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
NSString *__nullable HippyJSONStringify(id __nullable jsonObject, NSError **error) {
|
|
if (error) {
|
|
return _HPJSONStringifyNoRetry(jsonObject, error);
|
|
} else {
|
|
NSError *localError;
|
|
NSString *json = _HPJSONStringifyNoRetry(jsonObject, &localError);
|
|
if (localError) {
|
|
HPLogError(@"HPJSONStringify() encountered the following error: %@", localError.localizedDescription);
|
|
// Sanitize the data, then retry. This is slow, but it prevents uncaught
|
|
// data issues from crashing in production
|
|
return _HPJSONStringifyNoRetry(HippyJSONClean(jsonObject), NULL);
|
|
}
|
|
return json;
|
|
}
|
|
}
|
|
|
|
static id __nullable _HPJSONParse(NSString *__nullable 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) {
|
|
HPLogWarn(@"HPJSONParse received the following string, which could "
|
|
"not be losslessly converted to UTF8 data: '%@'",
|
|
jsonString);
|
|
} else {
|
|
NSString *errorMessage = @"HPJSONParse received invalid UTF8 data";
|
|
if (error) {
|
|
*error = HPErrorWithMessage(errorMessage);
|
|
} else {
|
|
HPLogError(@"%@", errorMessage);
|
|
}
|
|
return nil;
|
|
}
|
|
}
|
|
NSJSONReadingOptions options = NSJSONReadingAllowFragments;
|
|
if (mutable) {
|
|
options |= NSJSONReadingMutableContainers;
|
|
}
|
|
return [NSJSONSerialization JSONObjectWithData:jsonData options:options error:error];
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
id __nullable HippyJSONParse(NSString *__nullable jsonString, NSError **error) {
|
|
return _HPJSONParse(jsonString, NO, error);
|
|
}
|
|
|
|
id __nullable HippyJSONParseMutable(NSString *__nullable jsonString, NSError **error) {
|
|
return _HPJSONParse(jsonString, YES, error);
|
|
}
|
|
|
|
id HippyJSONClean(id object) {
|
|
static dispatch_once_t onceToken;
|
|
static NSSet<Class> *validLeafTypes;
|
|
dispatch_once(&onceToken, ^{
|
|
validLeafTypes = [[NSSet alloc] initWithArray:@[
|
|
[NSString class],
|
|
[NSMutableString class],
|
|
[NSNumber class],
|
|
[NSNull class],
|
|
]];
|
|
});
|
|
|
|
if ([validLeafTypes containsObject:[object classForCoder]]) {
|
|
if ([object isKindOfClass:[NSNumber class]]) {
|
|
return @(HPZeroIfNaN([object doubleValue]));
|
|
}
|
|
if ([object isKindOfClass:[NSString class]]) {
|
|
if ([object UTF8String] == NULL) {
|
|
return (id)kCFNull;
|
|
}
|
|
}
|
|
return object;
|
|
}
|
|
|
|
if ([object isKindOfClass:[NSDictionary class]]) {
|
|
__block BOOL copy = NO;
|
|
NSMutableDictionary<NSString *, id> *values = [[NSMutableDictionary alloc] initWithCapacity:[object count]];
|
|
[object enumerateKeysAndObjectsUsingBlock:^(NSString *key, id item, __unused BOOL *stop) {
|
|
id value = HippyJSONClean(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 = HippyJSONClean(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 *HPMD5Hash(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]];
|
|
}
|
|
|
|
NSDictionary<NSString *, id> *HippyMakeError(NSString *message, id __nullable toStringify, NSDictionary<NSString *, id> *__nullable extraData) {
|
|
if (toStringify) {
|
|
message = [message stringByAppendingString:[toStringify description]];
|
|
}
|
|
|
|
NSMutableDictionary<NSString *, id> *error = [extraData mutableCopy] ?: [NSMutableDictionary new];
|
|
error[@"message"] = message;
|
|
return error;
|
|
}
|
|
|
|
NSDictionary<NSString *, id> *HippyMakeAndLogError(NSString *message, id __nullable toStringify, NSDictionary<NSString *, id> *__nullable extraData) {
|
|
NSDictionary<NSString *, id> *error = HippyMakeError(message, toStringify, extraData);
|
|
HPLogError(@"\nError: %@", error);
|
|
return error;
|
|
}
|
|
|
|
NSDictionary<NSString *, id> *HippyJSErrorFromNSError(NSError *error) {
|
|
NSString *codeWithDomain = [NSString stringWithFormat:@"E%@%ld", error.domain.uppercaseString, (long)error.code];
|
|
return HippyJSErrorFromCodeMessageAndNSError(codeWithDomain, error.localizedDescription, error);
|
|
}
|
|
|
|
NSDictionary<NSString *, id> *HippyJSErrorFromCodeMessageAndNSError(NSString *code, NSString *message, NSError *__nullable error) {
|
|
NSString *errorMessage;
|
|
NSArray<NSString *> *stackTrace = [NSThread callStackSymbols];
|
|
NSMutableDictionary<NSString *, id> *errorInfo = [NSMutableDictionary dictionaryWithObject:stackTrace forKey:@"nativeStackIOS"];
|
|
|
|
if (error) {
|
|
errorMessage = error.localizedDescription ?: @"Unknown error from a native module";
|
|
errorInfo[@"domain"] = error.domain ?: HPErrorDomain;
|
|
} else {
|
|
errorMessage = @"Unknown error from a native module";
|
|
errorInfo[@"domain"] = HPErrorDomain;
|
|
}
|
|
errorInfo[@"code"] = code ?: HippyErrorUnspecified;
|
|
errorInfo[@"userInfo"] = HPNullIfNil(error.userInfo);
|
|
|
|
// Allow for explicit overriding of the error message
|
|
errorMessage = message ?: errorMessage;
|
|
|
|
return HippyMakeError(errorMessage, nil, errorInfo);
|
|
}
|