Handle bad JSON data without crashing

Summary:
public
NSJSONSerialization throws an exception when it encounters bad JSON data, including NaN values, which may not be a programming error.

This diff adds code to catch those exceptions and convert to an error. Also, if no error handling is in place, RCTJSONStringify will now display a redbox, and attempt to recover by sanitizing the JSON data and retrying.

Reviewed By: javache

Differential Revision: D2854778

fb-gh-sync-id: 18e6990af0d91083496d6a0b75c31a94ed9454a5
This commit is contained in:
Nick Lockwood 2016-01-26 06:12:48 -08:00 committed by facebook-github-bot-3
parent 1edcf4c6ac
commit 7419a82bd7
5 changed files with 88 additions and 14 deletions

View File

@ -78,4 +78,35 @@
XCTAssertEqualObjects(obj, RCTJSONParse(json, NULL));
}
- (void)testNotJSONSerializable
{
NSDictionary<NSString *, id> *obj = @{@"foo": [NSDate date]};
NSString *json = @"{\"foo\":null}";
XCTAssertEqualObjects(json, RCTJSONStringify(obj, NULL));
}
- (void)testNaN
{
NSDictionary<NSString *, id> *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<NSString *, id> *obj = @{@"foo": string};
NSString *json = @"{\"foo\":null}";
XCTAssertEqualObjects(json, RCTJSONStringify(obj, NULL));
}
- (void)testErrorPointer
{
NSDictionary<NSString *, id> *obj = @{@"foo": [NSDate date]};
NSError *error;
XCTAssertNil(RCTJSONStringify(obj, &error));
XCTAssertNotNil(error);
}
@end

View File

@ -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

View File

@ -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<Class> *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;
}

View File

@ -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"];

View File

@ -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++;