diff --git a/ChangeLog b/ChangeLog index 9460b371c..bdc00218e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,9 @@ +2014-02-17 Larry Campbell + + * Source/NSJSONSerialization.m: + * Tests/base/NSJSONSerialization/tests00.m: + Fixes for bug #41628 + 2014-07-12 Yavor Doganov * Tests/GNUmakefile: diff --git a/Source/NSJSONSerialization.m b/Source/NSJSONSerialization.m index 83d6b299f..0e5b83983 100644 --- a/Source/NSJSONSerialization.m +++ b/Source/NSJSONSerialization.m @@ -154,23 +154,58 @@ updateStreamBuffer(ParserState* state) { case NSUTF8StringEncoding: { - int i = -1; - // Read one UTF8 character from the stream - do + NSUInteger i = 0; + NSInteger n; + + n = [stream read: &bytes[0] maxLength: 1]; + if (n != 1) { - [stream read: &bytes[++i] maxLength: 1]; + state->error = [stream streamError]; + if (state->error == nil) + { + state->error + = [NSError errorWithDomain: NSCocoaErrorDomain + code: 0 + userInfo: nil]; + } + break; + } + else + { + if ((bytes[0] & 0xC0) == 0xC0) + { + for (i = 1; i <= 5; i++) + if ((bytes[0] & (0x40 >> i)) == 0) + break; + } } - while (bytes[i] & 0xf); if (0 == i) { state->buffer[0] = bytes[0]; } else { - str = [[NSString alloc] initWithUTF8String: (char*)bytes]; - [str getCharacters: state->buffer range: NSMakeRange(0,1)]; - [str release]; + n = [stream read: &bytes[1] maxLength: i]; + if (n == i) + { + str = [[NSString alloc] initWithUTF8String: (char*)bytes]; + [str getCharacters: state->buffer + range: NSMakeRange(0,1)]; + [str release]; + } + else + { + state->error = [stream streamError]; + if (state->error == nil) + { + state->error + = [NSError errorWithDomain: NSCocoaErrorDomain + code: 0 + userInfo: nil]; + } + break; + } } break; } @@ -209,6 +244,7 @@ updateStreamBuffer(ParserState* state) state->sourceIndex = -1; state->bufferIndex = 0; state->bufferLength = 1; + return; } // Use an NSString to do the character set conversion. We could do this more // efficiently. We could also reuse the string. @@ -243,7 +279,7 @@ consumeChar(ParserState *state) { state->sourceIndex++; state->bufferIndex++; - if (state->bufferIndex >= BUFFER_SIZE) + if (state->bufferIndex >= state->bufferLength) { state->updateBuffer(state); } diff --git a/Tests/base/NSJSONSerialization/tests00.m b/Tests/base/NSJSONSerialization/tests00.m new file mode 100644 index 000000000..43a76dc4a --- /dev/null +++ b/Tests/base/NSJSONSerialization/tests00.m @@ -0,0 +1,346 @@ +// +// Created by Larry Campbell on 12/12/08. +// + +#import +#import "ObjectTesting.h" + + +@interface GnustepBaseTests : NSObject { + unsigned _successes; + unsigned _failures; +} +@end + +@implementation GnustepBaseTests + +- (BOOL) performTest: (NSString *)name +{ + NSAutoreleasePool *pool = [NSAutoreleasePool new]; + BOOL result; + + NSLog(@"Performing test %@", name); + NS_DURING + { + [self performSelector: NSSelectorFromString(name)]; + _successes++; + result = YES; + } + NS_HANDLER + { + NSLog(@"Test %@ failed: %@", name, [localException reason]); + _failures++; + result = NO; + } + NS_ENDHANDLER + + [pool release]; + return result; +} + +- (unsigned) successes +{ + return _successes; +} + +- (unsigned) failures +{ + return _failures; +} + +@end + +@interface GnustepBaseTests(TheTests) +@end + +@implementation GnustepBaseTests(TheTests) + +- (void) cr39118 +{ + int x = 1999999999; + NSString *s = @"1999999999"; + NSAssert([s intValue] == x, @"intValue botch"); +} + +// verify that distinct strings get distinct hashes even if strings are very long +- (void) cr48439 +{ + NSMutableSet *hashes = [NSMutableSet set]; + NSArray *strings = [NSArray arrayWithObjects: + @"", + @"a", + @"aa", + @"123456789012345678901234567890123456789012345678901234567890123", + @"123456789012345678901234567890123456789012345678901234567890123a", + @"123456789012345678901234567890123456789012345678901234567890123b", + @"123456789012345678901234567890123456789012345678901234567890123c", + @"123456789012345678901234567890123456789012345678901234567890123d", + @"1234567890123456789012345678901234567890123456789012345678901234", + @"1234567890123456789012345678901234567890123456789012345678901234a", + @"1234567890123456789012345678901234567890123456789012345678901234b", + @"1234567890123456789012345678901234567890123456789012345678901234c", + @"1234567890123456789012345678901234567890123456789012345678901234d", + @"12345678901234567890123456789012345678901234567890123456789012345", + @"12345678901234567890123456789012345678901234567890123456789012345a", + @"12345678901234567890123456789012345678901234567890123456789012345b", + @"12345678901234567890123456789012345678901234567890123456789012345c", + @"12345678901234567890123456789012345678901234567890123456789012345d", + @"123456789012345678901234567890123456789012345678901234567890123456789012345", + @"123456789012345678901234567890123456789012345678901234567890123456789012345a", + @"123456789012345678901234567890123456789012345678901234567890123456789012345b", + @"123456789012345678901234567890123456789012345678901234567890123456789012345c", + @"123456789012345678901234567890123456789012345678901234567890123456789012345d", + nil]; + NSString *s; + NSEnumerator *e; + + e = [strings objectEnumerator]; + while ((s = [e nextObject]) != nil) + [hashes addObject:[NSNumber numberWithUnsignedInt:[s hash]]]; + + NSAssert([hashes count] == [strings count], @"hash botch"); +} + +// this also covers CR 150661 +- (void)cr153594 +{ + NSDate *t1 = [NSDate date]; + NSDate *t2; + NSData *d; + + d = [NSKeyedArchiver archivedDataWithRootObject: t1]; + t2 = [NSKeyedUnarchiver unarchiveObjectWithData: d]; + NSAssert([t1 isEqual:t2], @"equality botch"); +} + +- (void)cr1524466 +{ + NSURL *u = [[[NSURL alloc] initWithScheme: @"http" host: @"1.2.3.4" path: @"/a?b;foo"] autorelease]; + NSString *s = [[u absoluteURL] description]; + NSAssert([s isEqual: @"http://1.2.3.4/a?b;foo"], @"NSURL encoding botch"); +} + +- (void)cr2096767 +{ + static struct { + NSString *inputString; + NSTimeInterval increment; + NSString *expectedOutput; + } tests[] = { + { @"2006-04-22 22:22:22:901", 0.0, @"2006-04-22 22:22:22:901" }, // gnustep bug https://savannah.gnu.org/bugs/?func=detailitem&item_id=16426 + { @"2006-04-22 22:22:22:999", 0.0, @"2006-04-22 22:22:22:999" }, + { @"2006-04-22 22:22:22:999", 0.0005, @"2006-04-22 22:22:22:999" }, + { @"2006-04-22 22:22:22:999", 0.0006, @"2006-04-22 22:22:22:999" }, + { @"2006-04-22 22:22:22:999", 0.0007, @"2006-04-22 22:22:22:999" }, + { @"2006-04-22 22:22:22:999", 0.0008, @"2006-04-22 22:22:22:999" }, + { @"2006-04-22 22:22:22:999", 0.00089, @"2006-04-22 22:22:22:999" }, + { @"2006-04-22 22:22:22:999", 0.000899, @"2006-04-22 22:22:22:999" }, + { @"2006-04-22 22:22:22:999", 0.0008999, @"2006-04-22 22:22:22:999" }, + { @"2006-04-22 22:22:22:999", 0.00089999, @"2006-04-22 22:22:23:000" }, + { @"2006-04-22 22:22:22:999", 0.0009, @"2006-04-22 22:22:23:000" }, // CR https://bugzilla.akamai.com/show_bug.cgi?id=2096767 + }; + unsigned i; + NSString *fmt = [NSString stringWithFormat: @"%%Y-%%m-%%d %%H:%%M:%%S:%%F"]; + + for (i = 0; i < sizeof(tests)/sizeof(tests[0]); i++) + { + NSString *inputString = tests[i].inputString; + NSString *expectedOutput = tests[i].expectedOutput; + NSCalendarDate *d1 = [NSCalendarDate dateWithString:inputString calendarFormat:fmt]; + NSCalendarDate *d2 = [[[NSCalendarDate alloc] initWithTimeInterval:tests[i].increment sinceDate:d1] autorelease]; + NSString *result = [d2 descriptionWithCalendarFormat:fmt]; + NSLog(@"input string: %@, increment: %.4f", inputString, tests[i].increment); + NSLog(@"input date: %@", d1); + NSLog(@"output date: %@", result); + NSLog(@"expect: %@", expectedOutput); + NSAssert([result isEqualToString:expectedOutput], @"mismatch"); + } +} + +- (void)cr2549370 +{ + NSString *path = @"/tmp/json.data"; + NSString *string = @"Hello garçon! ¡Ola! A little more coöperation please!"; + NSMutableDictionary *dict = [NSMutableDictionary dictionary]; + NSMutableArray *array = [NSMutableArray array]; + NSData *d; + NSInputStream *stream; + + [array addObject:[NSDictionary dictionaryWithObjectsAndKeys:string, @"nickname", nil]]; + [dict setObject:array forKey: @"datacenters"]; + + d = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:0]; + [d writeToFile:path atomically:NO]; + + stream = [NSInputStream inputStreamWithFileAtPath:path]; + [stream open]; + dict = [NSJSONSerialization JSONObjectWithStream:stream options:0 error:0]; + dict = [[dict objectForKey: @"datacenters"] objectAtIndex:0]; + NSAssert(dict != nil, @"JSON returned nil"); + NSAssert([string isEqualToString:[dict objectForKey: @"nickname"]], @"data mismatch"); +} + +void checkstr(NSString *expected, NSString *got) +{ + if ([expected caseInsensitiveCompare:got] != NSOrderedSame) { + NSLog(@"expected %@", expected); + NSLog(@" got %@", got); + [NSException raise: @"error" format: @"test failed"]; + } +} + +// This is copied from servermonitor. We want to use the same quoting rules it does. +// We use this only for the username and password; we assume the path/query/fragment +// is already escaped. +static NSString *percentQuoted(NSString *s) +{ + NSMutableString *retval = [NSMutableString stringWithCapacity:[s length]]; + const unsigned char *p = (const unsigned char *)[s UTF8String]; + unsigned char c; + const char *allowedNonalnumChars = "-_,.'~!$&*();"; + + while ((c = *p++) != '\0') { + if (isalnum(c) || strchr(allowedNonalnumChars, c) != 0) + [retval appendFormat: @"%c", c]; + else + [retval appendFormat: @"%%%02x", c]; + } + return retval; +} + +- (void)urlTest +{ + static NSString *schemes[] = { @"ftp", @"http", @"https" }; + static NSString *hosts[] = { @"www.akamai.com", @"10.0.1.1", @"localhost" }; + static NSString *usernames[] = { nil, @"username", @"foo", @"a_username" }; + static NSString *passwords[] = { nil, @"password", @"M@nx2y#" }; + + // The path/query/fragment part here must be percent-quoted + // as it would have to be if pasted into a curl command. + struct { + NSString *pqf; // path, query, fragment + NSString *path; // expected path returned from NSURL (nil if same as pqf) + } pqfs[] = { + { @"/", nil }, + { @"/foo/bar/zot", nil }, + { @"/a%20dillar%20a%20dollar", nil }, + { @"/foo:bar:zot", nil }, + { @"/a?b;foo", @"/a" }, + { @"/a%25b", nil }, + { @"/MHcwdTBOMEwwSjAJBgUrDgMCGgUABBScDhI23ZEqCpe2zGqbkoJVvUaDTgQUf/ZMNigUrs0eN6/eWvJbw6CsK/4CEQCbL3Mm0kXS8q7t0jI7E5gdoiMwITAfBgkrBgEFBQcwAQIEEgQQi7kTQMz7QK60KI2PnRR3mg==", + nil }, + { @"/interface/6/?mode=rc3&outputType=json&url=http%3A%2F%2Fwww.ehow.com%2Fhow_2324118_take-care-paper-cut.html&category=Health&subCategory=Family%20Health&subSubCategory=General%20Family%20Health&pageLocations=PLATFORM_TL|0&flushPage=1", + @"/interface/6" }, + { @"/d/search/p/enom/xml/domain/multiset/v4/?url=http%3A%2F%2Fnuseek.com&Partner=enom_internal_d2r_derp&config=1234567890&affilData=ip%3d127.0.0.1%26ua%3dIE|6.0|WinNT%26ur%3d&maxListings=10&maxRT=20&maxRTL=20&maxWeb=4&serveUrl=http://parkingweb30/default.pk", + @"/d/search/p/enom/xml/domain/multiset/v4" }, + }; + unsigned i, j, k, l, m; + + for (i = 0; i < sizeof(schemes)/sizeof(schemes[0]); i++) + { + NSString *scheme = schemes[i]; + + for (j = 0; j < sizeof(hosts)/sizeof(hosts[0]); j++) + { + NSString *host = hosts[j]; + + for (k = 0; k < sizeof(usernames)/sizeof(usernames[0]); k++) + { + NSString *username = usernames[k]; + + for (l = 0; l < sizeof(passwords)/sizeof(passwords[0]); l++) + { + NSString *password = passwords[l]; + + for (m = 0; m < sizeof(pqfs)/sizeof(pqfs[0]); m++) + { + NSMutableString *hostpart = [NSMutableString string]; + NSString *pqf = pqfs[m].pqf; + NSString *expectedPath = [(pqfs[m].path == nil ? pqf : pqfs[m].path) stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + NSString *urlstring; + NSURL *url; + + expectedPath = [(pqfs[m].path == nil ? pqf : pqfs[m].path) stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding]; + + if (username != nil) + { + if (password != nil) + { + [hostpart appendFormat: @"%@:%@@", + percentQuoted(username), + percentQuoted(password)]; + } + else + { + [hostpart appendFormat: @"%@@", + percentQuoted(username)]; + } + } + [hostpart appendString:host]; + + urlstring = [NSString stringWithFormat: @"%@://%@%@", + scheme, hostpart, pqf]; + url = [[[NSURL alloc] initWithString: urlstring] + autorelease]; + + // work around Apple bug, which rejects + // objects containing vertical bar (turns out + // gnustep is bug-for-bug compatible here, + // so no need to make this Apple-only) + if (url == nil && [pqf rangeOfString: @"|"].length != 0) + { + NSMutableString *fixedObject = [[pqf mutableCopy] autorelease]; + unsigned n; + do { + n = [fixedObject replaceOccurrencesOfString: @"|" withString: @"%7C" options:0 range:NSMakeRange(0, [fixedObject length])]; + } while (n != 0); + urlstring = [NSString stringWithFormat: @"%@://%@%@", scheme, hostpart, fixedObject]; + url = [[[NSURL alloc] initWithString:urlstring] autorelease]; + } + + NSAssert(url != nil, @"URL creation failed"); + + NSAssert([[url scheme] isEqual:schemes[i]], @"scheme botch"); + NSAssert((username == nil) == ([url user] == nil), @"username existence botch"); + if (username != nil) { + NSAssert([[url user] isEqual:username], @"username botch"); + NSAssert((password == nil) == ([url password] == nil), @"password existence botch"); + if (password != nil) { + NSString *urlpassword = [[url password] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + NSAssert([urlpassword isEqual:password], @"password botch"); + } + } + checkstr(expectedPath, [url path]); + checkstr(urlstring, [[url absoluteURL] description]); + NSAssert([[url host] isEqual:host], @"host botch"); + NSAssert([[url path] isEqual:expectedPath], + @"path botch"); + } + } + } + } + } +} + +@end + + +int main(int argc, char *argv[]) +{ + int status = 0; + NSAutoreleasePool *pool = [NSAutoreleasePool new]; + GnustepBaseTests *gtb = [GnustepBaseTests new]; + + PASS([gtb performTest: @"cr39118"], "cr39118"); + PASS([gtb performTest: @"cr48439"], "cr48439"); + PASS([gtb performTest: @"cr153594"], "cr153594"); + PASS([gtb performTest: @"urlTest"], "urlTest"); + PASS([gtb performTest: @"cr1524466"], "cr1524466"); + PASS([gtb performTest: @"cr2096767"], "cr2096767"); + PASS([gtb performTest: @"cr2549370"], "cr2549370"); + + [gtb release]; + + [pool release]; + return status; +}