diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTJSONTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTJSONTests.m index fd8343c08..acacc3a34 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTJSONTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTJSONTests.m @@ -78,4 +78,35 @@ XCTAssertEqualObjects(obj, RCTJSONParse(json, NULL)); } +- (void)testNotJSONSerializable +{ + NSDictionary *obj = @{@"foo": [NSDate date]}; + NSString *json = @"{\"foo\":null}"; + XCTAssertEqualObjects(json, RCTJSONStringify(obj, NULL)); +} + +- (void)testNaN +{ + NSDictionary *obj = @{@"foo": @(NAN)}; + NSString *json = @"{\"foo\":0}"; + XCTAssertEqualObjects(json, RCTJSONStringify(obj, NULL)); +} + +- (void)testNotUTF8Convertible +{ + //see https://gist.github.com/0xced/56035d2f57254cf518b5 + NSString *string = [[NSString alloc] initWithBytes:"\xd8\x00" length:2 encoding:NSUTF16StringEncoding]; + NSDictionary *obj = @{@"foo": string}; + NSString *json = @"{\"foo\":null}"; + XCTAssertEqualObjects(json, RCTJSONStringify(obj, NULL)); +} + +- (void)testErrorPointer +{ + NSDictionary *obj = @{@"foo": [NSDate date]}; + NSError *error; + XCTAssertNil(RCTJSONStringify(obj, &error)); + XCTAssertNotNil(error); +} + @end diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index 30febd5a3..8f7a4858b 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -23,7 +23,7 @@ RCT_EXTERN NSString *__nullable RCTJSONStringify(id __nullable jsonObject, NSErr RCT_EXTERN id __nullable RCTJSONParse(NSString *__nullable jsonString, NSError **error); RCT_EXTERN id __nullable RCTJSONParseMutable(NSString *__nullable jsonString, NSError **error); -// Strip non JSON-safe values from an object graph +// Santize a JSON string by stripping invalid objects and/or NaN values RCT_EXTERN id RCTJSONClean(id object); // Get MD5 hash of a string diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index 7b69da74b..f49e117e2 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -23,8 +23,12 @@ NSString *const RCTErrorUnspecified = @"EUNSPECIFIED"; -NSString *__nullable RCTJSONStringify(id __nullable jsonObject, NSError **error) +static NSString *__nullable _RCTJSONStringifyNoRetry(id __nullable jsonObject, NSError **error) { + if (!jsonObject) { + return nil; + } + static SEL JSONKitSelector = NULL; static NSSet *collectionTypes; static dispatch_once_t onceToken; @@ -38,17 +42,48 @@ NSString *__nullable RCTJSONStringify(id __nullable jsonObject, NSError **error) } }); - // 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); - } + @try { - // Use Foundation JSON method - NSData *jsonData = jsonObject ? [NSJSONSerialization - dataWithJSONObject:jsonObject - options:(NSJSONWritingOptions)NSJSONReadingAllowFragments - error:error] : nil; - return jsonData ? [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] : 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; + } + @catch (NSException *exception) { + + // Convert exception to error + if (error) { + *error = [NSError errorWithDomain:RCTErrorDomain code:0 userInfo:@{ + NSLocalizedDescriptionKey: exception.description ?: @"" + }]; + } + return nil; + } +} + +NSString *__nullable RCTJSONStringify(id __nullable jsonObject, NSError **error) +{ + if (error) { + return _RCTJSONStringifyNoRetry(jsonObject, error); + } else { + NSError *localError; + NSString *json = _RCTJSONStringifyNoRetry(jsonObject, &localError); + if (localError) { + RCTLogError(@"RCTJSONStringify() 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 _RCTJSONStringifyNoRetry(RCTJSONClean(jsonObject), NULL); + } + return json; + } } static id __nullable _RCTJSONParse(NSString *__nullable jsonString, BOOL mutable, NSError **error) @@ -134,6 +169,14 @@ id RCTJSONClean(id object) }); if ([validLeafTypes containsObject:[object classForCoder]]) { + if ([object isKindOfClass:[NSNumber class]]) { + return @(RCTZeroIfNaN([object doubleValue])); + } + if ([object isKindOfClass:[NSString class]]) { + if ([object UTF8String] == NULL) { + return (id)kCFNull; + } + } return object; } diff --git a/React/Modules/RCTRedBox.m b/React/Modules/RCTRedBox.m index 6379c1164..b8717b150 100644 --- a/React/Modules/RCTRedBox.m +++ b/React/Modules/RCTRedBox.m @@ -91,7 +91,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)openStackFrameInEditor:(NSDictionary *)stackFrame { - NSData *stackFrameJSON = [RCTJSONStringify(stackFrame, nil) dataUsingEncoding:NSUTF8StringEncoding]; + NSData *stackFrameJSON = [RCTJSONStringify(stackFrame, NULL) dataUsingEncoding:NSUTF8StringEncoding]; NSString *postLength = [NSString stringWithFormat:@"%tu", stackFrameJSON.length]; NSMutableURLRequest *request = [NSMutableURLRequest new]; request.URL = [RCTConvert NSURL:@"http://localhost:8081/open-stack-frame"]; diff --git a/React/Profiler/RCTProfile.m b/React/Profiler/RCTProfile.m index 8bcf2a56e..42cf74c63 100644 --- a/React/Profiler/RCTProfile.m +++ b/React/Profiler/RCTProfile.m @@ -81,7 +81,7 @@ static systrace_arg_t *RCTProfileSystraceArgsFromNSDictionary(NSDictionary *args systrace_args[i].key = keyc; systrace_args[i].key_len = (int)strlen(keyc); - const char *valuec = RCTJSONStringify(value, nil).UTF8String; + const char *valuec = RCTJSONStringify(value, NULL).UTF8String; systrace_args[i].value = valuec; systrace_args[i].value_len = (int)strlen(valuec); i++;