diff --git a/Frameworks/CoreFoundationAdditions/_NSCFString.mm b/Frameworks/CoreFoundationAdditions/_NSCFString.mm index 79dc8a89ca..b08864b2c4 100644 --- a/Frameworks/CoreFoundationAdditions/_NSCFString.mm +++ b/Frameworks/CoreFoundationAdditions/_NSCFString.mm @@ -20,6 +20,11 @@ #include "_NSCFString.h" #include +@interface _NSCFTemporaryRootObject (NSString) +- (void)_raiseBoundsExceptionForSelector:(SEL)selector andIndex:(NSUInteger)index; +- (void)_raiseBoundsExceptionForSelector:(SEL)selector andRange:(NSRange)range; +@end + // ignore bridge cast warnings here. _NSCFString will be a subclass of NSString. It just // doesn't realize it yet. #pragma clang diagnostic push @@ -59,19 +64,40 @@ + (instancetype)allocWithZone:(NSZone*)zone { // NSString overrides - (NSUInteger)length { - return CFStringGetLength(static_cast(self)); + return _CFStringGetLength2(static_cast(self)); +} + +- (NSUInteger)hash { + return __CFStringHash(static_cast(self)); } - (unichar)characterAtIndex:(NSUInteger)index { - return CFStringGetCharacterAtIndex(static_cast(self), index); + unichar ch = 0; + int err = _CFStringCheckAndGetCharacterAtIndex(static_cast(self), index, &ch); + if (err == _CFStringErrBounds) { + [self _raiseBoundsExceptionForSelector:_cmd andIndex:index]; + return 0; + } + return ch; } - (void)getCharacters:(unichar*)buffer range:(NSRange)range { - return CFStringGetCharacters(static_cast(self), CFRange{ range.location, range.length }, buffer); + int err = _CFStringCheckAndGetCharacters(static_cast(self), CFRange{ range.location, range.length }, buffer); + if (err == _CFStringErrBounds) { + [self _raiseBoundsExceptionForSelector:_cmd andRange:range]; + } } - (void)replaceCharactersInRange:(NSRange)range withString:(NSString*)replacement { - CFStringReplace(static_cast(self), CFRange{ range.location, range.length }, static_cast(replacement)); + int err = __CFStringCheckAndReplace(static_cast(self), CFRange{ range.location, range.length }, static_cast(replacement)); + switch (err) { + case _CFStringErrBounds: + [self _raiseBoundsExceptionForSelector:_cmd andRange:range]; + break; + case _CFStringErrNotMutable: + [self doesNotRecognizeSelector:_cmd]; + break; + } } - (instancetype)copyWithZone:(NSZone*)zone { diff --git a/Frameworks/CoreFoundationAdditions/_NSCFTemporaryRootObject.mm b/Frameworks/CoreFoundationAdditions/_NSCFTemporaryRootObject.mm index 3af722c167..617c75d698 100644 --- a/Frameworks/CoreFoundationAdditions/_NSCFTemporaryRootObject.mm +++ b/Frameworks/CoreFoundationAdditions/_NSCFTemporaryRootObject.mm @@ -15,5 +15,19 @@ //****************************************************************************** #include "_NSCFTemporaryRootObject.h" +#include + @implementation _NSCFTemporaryRootObject -@end \ No newline at end of file +- (void)doesNotRecognizeSelector:(SEL)selector { + // According to the reference platform documentation, this method must never return. + EXCEPTION_RECORD record{ + EXCEPTION_NONCONTINUABLE_EXCEPTION, + EXCEPTION_NONCONTINUABLE, + nullptr, + nullptr, + 0, + {0} + }; + RaiseFailFastException(&record, nullptr, FAIL_FAST_GENERATE_EXCEPTION_ADDRESS); +} +@end diff --git a/Frameworks/Foundation/NSCFMutableString.mm b/Frameworks/Foundation/NSCFMutableString.mm index 83c2949142..be4e45c1fd 100644 --- a/Frameworks/Foundation/NSCFMutableString.mm +++ b/Frameworks/Foundation/NSCFMutableString.mm @@ -45,7 +45,7 @@ - (instancetype)initWithUTF8String:(const char*)utf8str { - (instancetype)initWithFormat:(id)formatStr arguments:(va_list)pReader { CFMutableStringRef mutableRef = CFStringCreateMutable(kCFAllocatorDefault, 0); - CFStringAppendFormatAndArguments(mutableRef, nullptr, static_cast(formatStr), pReader); + _CFStringAppendFormatAndArgumentsAux(mutableRef, &_NSCFStringCopyDescription, nullptr, static_cast(formatStr), pReader); return reinterpret_cast(static_cast(mutableRef)); } diff --git a/Frameworks/Foundation/NSCFString.h b/Frameworks/Foundation/NSCFString.h index 5268e29f7e..26fd2ca0be 100644 --- a/Frameworks/Foundation/NSCFString.h +++ b/Frameworks/Foundation/NSCFString.h @@ -21,4 +21,6 @@ @end @interface NSMutableStringPrototype : NSMutableString -@end \ No newline at end of file +@end + +CFStringRef _NSCFStringCopyDescription(void* cfTypeRef, const void* locInfo); \ No newline at end of file diff --git a/Frameworks/Foundation/NSCFString.mm b/Frameworks/Foundation/NSCFString.mm index 898b636a8f..f2d72ba09b 100644 --- a/Frameworks/Foundation/NSCFString.mm +++ b/Frameworks/Foundation/NSCFString.mm @@ -23,6 +23,10 @@ #include "BridgeHelpers.h" #include "ForFoundationOnly.h" +CFStringRef _NSCFStringCopyDescription(void* cfTypeRef, const void* locInfo) { + return (CFStringRef)[[static_cast(cfTypeRef) description] copy]; +} + @implementation NSStringPrototype PROTOTYPE_CLASS_REQUIRED_IMPLS(NSObject) @@ -45,7 +49,7 @@ - (instancetype)initWithUTF8String:(const char*)utf8str { - (instancetype)initWithFormat:(id)formatStr arguments:(va_list)pReader { return reinterpret_cast(static_cast( - CFStringCreateWithFormatAndArguments(kCFAllocatorDefault, nullptr, static_cast(formatStr), pReader))); + _CFStringCreateWithFormatAndArgumentsAux(kCFAllocatorDefault, &_NSCFStringCopyDescription, nullptr, static_cast(formatStr), pReader))); } - (instancetype)initWithBytes:(const void*)bytes length:(NSUInteger)length encoding:(NSStringEncoding)encoding { @@ -89,12 +93,12 @@ - (instancetype)initWithFormat:(NSString*)format locale:(id)locale arguments:(va CFStringRef str; if (locale == nil) { - str = CFStringCreateWithFormatAndArguments(nullptr, nullptr, static_cast(format), argList); + str = _CFStringCreateWithFormatAndArgumentsAux(nullptr, &_NSCFStringCopyDescription, nullptr, static_cast(format), argList); } else if ([locale isKindOfClass:[NSLocale class]] || [locale isKindOfClass:[NSDictionary class]]) { - str = CFStringCreateWithFormatAndArguments(kCFAllocatorDefault, - static_cast(locale), - static_cast(format), - argList); + str = _CFStringCreateWithFormatAndArgumentsAux(kCFAllocatorDefault, &_NSCFStringCopyDescription, + static_cast(locale), + static_cast(format), + argList); } else { [NSException raise:NSInvalidArgumentException format:@"Locale parameter must be a NSLocale or a NSDictionary."]; } diff --git a/Frameworks/Foundation/NSCFURL.mm b/Frameworks/Foundation/NSCFURL.mm index 2a02acdb27..1910fe7057 100644 --- a/Frameworks/Foundation/NSCFURL.mm +++ b/Frameworks/Foundation/NSCFURL.mm @@ -141,7 +141,7 @@ - (NSString*)path { ret = [ret stringByRemovingPercentEncoding]; // Remove slashes from before drives - if (([ret hasPrefix:_NSGetSlashStr()]) && (_isLetter([ret characterAtIndex:1])) && ([ret characterAtIndex:2] == ':')) { + if ([ret length] >= 3 && ([ret hasPrefix:_NSGetSlashStr()]) && (_isLetter([ret characterAtIndex:1])) && ([ret characterAtIndex:2] == ':')) { ret = [ret substringFromIndex:1]; } diff --git a/Frameworks/Foundation/NSString.mm b/Frameworks/Foundation/NSString.mm index de87254476..35595e514e 100644 --- a/Frameworks/Foundation/NSString.mm +++ b/Frameworks/Foundation/NSString.mm @@ -56,20 +56,6 @@ NSString* const NSParseErrorException = @"NSParseErrorException"; NSString* const NSCharacterConversionException = @"NSCharacterConversionException"; -id error(id obj, char* buf, const char* error, ...) { - TraceError(TAG, L"propertyListFromStrings error: %hs", buf); - - return nil; -} - -static unichar SwapWord(unichar c) { - return (c >> 8) | ((c & 0xFF) << 8); -} - -static unichar PickWord(unichar c) { - return c; -} - typedef enum { NEW_LINE = 0x0a, CARRIAGE_RETURN = 0x0d, @@ -82,6 +68,24 @@ @implementation NSString */ BASE_CLASS_REQUIRED_IMPLS(NSString, NSStringPrototype, CFStringGetTypeID); +// Exception methods for _NSCFString +- (void)_raiseBoundsExceptionForSelector:(SEL)selector andIndex:(NSUInteger)index { + [NSException raise:NSRangeException + format:@"-[NSString %@]: Index %lu out of bounds; string length %lu", + NSStringFromSelector(selector), + (unsigned long)index, + (unsigned long)self.length]; +} + +- (void)_raiseBoundsExceptionForSelector:(SEL)selector andRange:(NSRange)range { + [NSException raise:NSRangeException + format:@"-[NSString %@]: Range {%lu, %lu} out of bounds; string length %lu", + NSStringFromSelector(selector), + (unsigned long)range.location, + (unsigned long)range.length, + (unsigned long)self.length]; +} + /** @Status Interoperable */ @@ -533,6 +537,10 @@ - (BOOL)getBytes:(void*)buffer @Status Interoperable */ - (void)getCharacters:(unichar*)dest range:(NSRange)range { + if (range.location + range.length > self.length) { + [self _raiseBoundsExceptionForSelector:_cmd andRange:range]; + } + for (unsigned int i = 0; i < range.length; i++) { dest[i] = [self characterAtIndex:(i + range.location)]; } @@ -1089,7 +1097,18 @@ - (NSRange)rangeOfString:(NSString*)searchString options:(NSStringCompareOptions @Status Interoperable */ - (BOOL)isAbsolutePath { - return ([self hasPrefix:_NSGetSlashStr()]) || ((_isLetter([self characterAtIndex:0])) && ([self characterAtIndex:1] == ':')); + if ([self hasPrefix:_NSGetSlashStr()]) { + return YES; + } + + if (self.length >= 2) { + // TODO: This will require some investigation when we design a cohesive path story. + // Currently, we'll report @"C:Hello.txt" as absolute, when it is in fact relative + // to the drive-specific CWD on C:. + return _isLetter([self characterAtIndex:0]) && [self characterAtIndex:1] == ':'; + } + + return NO; } /** @@ -1261,254 +1280,14 @@ - (Class)classForCoder { @Status Interoperable */ - (NSDictionary*)propertyListFromStringsFileFormat { - NSMutableDictionary* ret = [NSMutableDictionary new]; - NSUInteger length = [self length]; - - NSString* key; - id value; - - unsigned int index, c, strSize = 0, strMax = 2048; - std::unique_ptr strBuf(static_cast(IwMalloc(strMax)), IwFree); - - enum { - STATE_WHITESPACE, - STATE_COMMENT_SLASH, - STATE_COMMENT_EOL, - STATE_COMMENT, - STATE_COMMENT_STAR, - STATE_STRING, - STATE_STRING_KEY, - STATE_STRING_SLASH, - STATE_STRING_SLASH_X00, - STATE_STRING_SLASH_XX0 - } state = STATE_WHITESPACE; - enum { EXPECT_KEY, EXPECT_EQUAL_SEMI, EXPECT_VAL, EXPECT_SEMI } expect = EXPECT_KEY; - - unichar (*mapUC)(unichar); - if ([self characterAtIndex:0] == 0xFFFE) { - // reverse endianness - mapUC = SwapWord; - index = 1; - } else if ([self characterAtIndex:0] == 0xFEFF) { - // native endianness - mapUC = PickWord; - index = 1; - } else { - // no BOM, assume native endianness - mapUC = PickWord; - index = 0; - } - - if (mapUC([self characterAtIndex:(length - 1)]) == 0x0A) - length--; - - for (; index < length; index++) { - c = mapUC([self characterAtIndex:index]); - switch (state) { - case STATE_WHITESPACE: - if (c == '/') { - state = STATE_COMMENT_SLASH; - } else if (c == '=') { - if (expect == EXPECT_EQUAL_SEMI) { - expect = EXPECT_VAL; - } else { - return error(ret, strBuf.get(), "unexpected character %02X '%c' at %d", c, c, index); - } - } else if (c == ';') { - if (expect == EXPECT_SEMI) { - [ret setValue:value forKey:key]; - value = nil; - key = nil; - expect = EXPECT_KEY; - } else if (expect == EXPECT_EQUAL_SEMI) { - expect = EXPECT_KEY; - assert(0); - //[array addObject:[array lastObject]]; - } else { - return error(ret, strBuf.get(), "unexpected character %02X '%c' at %d", c, c, index); - } - } else if (c == '\"') { - if (expect != EXPECT_KEY && expect != EXPECT_VAL) { - return error(ret, strBuf.get(), "unexpected character %02X '%c' at %d", c, c, index); - } - - strSize = 0; - state = STATE_STRING; - } else if (c > ' ') { - if (expect != EXPECT_KEY) { - return error(ret, strBuf.get(), "unexpected character %02X '%c' at %d", c, c, index); - } - - strBuf[0] = c; - strSize = 1; - state = STATE_STRING_KEY; - } - break; - - case STATE_COMMENT_SLASH: - if (c == '*') { - state = STATE_COMMENT; - } else if (c == '/') { - state = STATE_COMMENT_EOL; - } else { - return error(ret, strBuf.get(), "unexpected character %02X '%c',after /", c, c); - } - break; - - case STATE_COMMENT_EOL: - if (c == 0x0A) { - state = STATE_WHITESPACE; - } - - case STATE_COMMENT: - if (c == '*') { - state = STATE_COMMENT_STAR; - } - break; - - case STATE_COMMENT_STAR: - if (c == '/') { - state = STATE_WHITESPACE; - } else if (c != '*') { - state = STATE_COMMENT; - } - break; - - case STATE_STRING_KEY: - switch (c) { - case '\"': - return error(ret, strBuf.get(), "unexpected character %02X '%c' at %d", c, c, index); - case '=': - index -= 2; - case ' ': - c = '\"'; - } - - case STATE_STRING: - if (c == '\"') { - strBuf[strSize] = '\0'; - - NSString* string = [NSString stringWithUTF8String:strBuf.get()]; - if (expect == EXPECT_KEY) { - key = string; - } else { - value = string; - } - - state = STATE_WHITESPACE; - - if (expect == EXPECT_KEY) { - expect = EXPECT_EQUAL_SEMI; - } else { - expect = EXPECT_SEMI; - } - } else { - if (strSize >= strMax) { - strMax *= 2; - char* oldPtr = strBuf.release(); - strBuf.reset((char*)IwRealloc(oldPtr, strMax)); - } - if (c == '\\') { - state = STATE_STRING_SLASH; - } else { - // [NOTE: Convert to UTF8 here!] - strBuf[strSize] = c; - strSize++; - } - } - break; - - case STATE_STRING_SLASH: - switch (c) { - case 'a': - strBuf[strSize++] = '\a'; - state = STATE_STRING; - break; - case 'b': - strBuf[strSize++] = '\b'; - state = STATE_STRING; - break; - case 'f': - strBuf[strSize++] = '\f'; - state = STATE_STRING; - break; - case 'n': - strBuf[strSize++] = '\n'; - state = STATE_STRING; - break; - case 'r': - strBuf[strSize++] = '\r'; - state = STATE_STRING; - break; - case 't': - strBuf[strSize++] = '\t'; - state = STATE_STRING; - break; - case 'v': - strBuf[strSize++] = '\v'; - state = STATE_STRING; - break; - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - strBuf[strSize++] = c - '0'; - state = STATE_STRING_SLASH_X00; - break; - - default: - strBuf[strSize++] = c; - state = STATE_STRING; - break; - } - break; + CFStringEncoding fastestCFEncoding = CFStringGetFastestEncoding(static_cast(self)); + NSStringEncoding encoding = (NSStringEncoding)CFStringConvertEncodingToNSStringEncoding(fastestCFEncoding); - case STATE_STRING_SLASH_X00: - if (c < '0' || c > '7') { - state = STATE_STRING; - index--; - } else { - state = STATE_STRING_SLASH_XX0; - strBuf[strSize - 1] *= 8; - strBuf[strSize - 1] += c - '0'; - } - break; - - case STATE_STRING_SLASH_XX0: - state = STATE_STRING; - if (c < '0' || c > '7') { - index--; - } else { - strBuf[strSize - 1] *= 8; - strBuf[strSize - 1] += c - '0'; - } - break; - } - } - - if (state != STATE_WHITESPACE) { - return error(ret, nullptr, "unexpected EOF\n"); - } - - switch (expect) { - case EXPECT_EQUAL_SEMI: - return error(ret, nullptr, "unexpected EOF, expecting = or ;"); - - case EXPECT_VAL: - return error(ret, nullptr, "unexpected EOF, expecting value"); + NSData* data = [self dataUsingEncoding:encoding]; - case EXPECT_SEMI: - return error(ret, nullptr, "unexpected EOF, expecting ;"); - - default: - break; - } - - return [ret autorelease]; + NSDictionary* propertyList = static_cast( + CFPropertyListCreateWithData(nullptr, static_cast(data), kCFPropertyListImmutable, nullptr, nullptr)); + return [propertyList autorelease]; } BOOL _isAParagraphSeparatorTypeCharacter(unichar ch) { diff --git a/Frameworks/include/_NSCFTemporaryRootObject.h b/Frameworks/include/_NSCFTemporaryRootObject.h index 7e6be85b88..02262c2138 100644 --- a/Frameworks/include/_NSCFTemporaryRootObject.h +++ b/Frameworks/include/_NSCFTemporaryRootObject.h @@ -30,4 +30,8 @@ __attribute__((objc_root_class)) @interface _NSCFTemporaryRootObject { @public Class isa; } + +/* From NSObject */ +- (void)doesNotRecognizeSelector:(SEL)selector; + @end \ No newline at end of file diff --git a/tests/unittests/Foundation/NSStringTests.m b/tests/unittests/Foundation/NSStringTests.m index eb29169159..71c020b16b 100644 --- a/tests/unittests/Foundation/NSStringTests.m +++ b/tests/unittests/Foundation/NSStringTests.m @@ -576,3 +576,41 @@ void testUrlCharacterSetEncoding(NSString* decodedString, NSString* encodedStrin string = @"/tmp/random.foo.tiff"; ASSERT_TRUE([string.pathExtension isEqualToString:@"tiff"]); } + +TEST(NSString, Exceptions) { + NSRange range{ 100, 10 }; + + EXPECT_ANY_THROW([@"hello" characterAtIndex:10]); + + unichar buffer[1024]; + EXPECT_ANY_THROW([@"hello" getCharacters:&buffer[0] range:range]); + + EXPECT_ANY_THROW([[[@"hello" mutableCopy] autorelease] replaceCharactersInRange:range withString:@"boom"]); + + EXPECT_ANY_THROW([(NSMutableString*)@"hello" replaceCharactersInRange:range withString:@"boom"]); + + NSString* immutableFormattedString = [NSString stringWithFormat:@"Hello %p", buffer]; + EXPECT_ANY_THROW([(NSMutableString*)immutableFormattedString replaceCharactersInRange:range withString:@"boom"]); +} + +class StringsFormatPropertyList : public ::testing::TestWithParam {}; + +TEST_P(StringsFormatPropertyList, CanDeserialize) { + const wchar_t* data = GetParam(); + NSString* string = [NSString stringWithCharacters:(const unichar*)data length:wcslen(data)]; + + ASSERT_OBJCNE(nil, string); + + NSDictionary* propertyList = [string propertyListFromStringsFileFormat]; + + ASSERT_OBJCNE(nil, propertyList); + + ASSERT_OBJCEQ(@"value1", propertyList[@"key1"]); + ASSERT_OBJCEQ(@"value2", propertyList[@"key2"]); +} + +INSTANTIATE_TEST_CASE_P(NSString, + StringsFormatPropertyList, + ::testing::Values(L"\uFEFFkey1=value1;\n\"key2\"=\"value2\";", // BOM + L"key1=value1;\n\"key2\"=\"value2\";" // No BOM + )); \ No newline at end of file