Index: src/OFConstantString.m ================================================================== --- src/OFConstantString.m +++ src/OFConstantString.m @@ -367,10 +367,37 @@ return [self rangeOfString: string options: options range: range]; } + +- (size_t)indexOfCharacterFromSet: (OFCharacterSet *)characterSet +{ + [self finishInitialization]; + + return [self indexOfCharacterFromSet: characterSet]; +} + +- (size_t)indexOfCharacterFromSet: (OFCharacterSet *)characterSet + options: (int)options +{ + [self finishInitialization]; + + return [self indexOfCharacterFromSet: characterSet + options: options]; +} + +- (size_t)indexOfCharacterFromSet: (OFCharacterSet *)characterSet + options: (int)options + range: (of_range_t)range +{ + [self finishInitialization]; + + return [self indexOfCharacterFromSet: characterSet + options: options + range: range]; +} - (bool)containsString: (OFString *)string { [self finishInitialization]; Index: src/OFString.h ================================================================== --- src/OFString.h +++ src/OFString.h @@ -102,10 +102,11 @@ */ typedef void (^of_string_line_enumeration_block_t)(OFString *line, bool *stop); #endif @class OFArray OF_GENERIC(ObjectType); +@class OFCharacterSet; @class OFURL; /*! * @class OFString OFString.h ObjFW/OFString.h * @@ -988,10 +989,51 @@ */ - (of_range_t)rangeOfString: (OFString *)string options: (int)options range: (of_range_t)range; +/*! + * @brief Returns the index of the first character from the set. + * + * @param characterSet The set of characters to search for + * @return The index of the first occurrence of a character from the set or + * `OF_NOT_FOUND` if it was not found + */ +- (size_t)indexOfCharacterFromSet: (OFCharacterSet *)characterSet; + +/*! + * @brief Returns the index of the first character from the set. + * + * @param characterSet The set of characters to search for + * @param options Options modifying search behaviour.@n + * Possible values are: + * Value | Description + * -----------------------------|------------------------------- + * `OF_STRING_SEARCH_BACKWARDS` | Search backwards in the string + * @return The index of the first occurrence of a character from the set or + * `OF_NOT_FOUND` if it was not found + */ +- (size_t)indexOfCharacterFromSet: (OFCharacterSet *)characterSet + options: (int)options; + +/*! + * @brief Returns the index of the first character from the set. + * + * @param characterSet The set of characters to search for + * @param options Options modifying search behaviour.@n + * Possible values are: + * Value | Description + * -----------------------------|------------------------------- + * `OF_STRING_SEARCH_BACKWARDS` | Search backwards in the string + * @param range The range in which to search + * @return The index of the first occurrence of a character from the set or + * `OF_NOT_FOUND` if it was not found + */ +- (size_t)indexOfCharacterFromSet: (OFCharacterSet *)characterSet + options: (int)options + range: (of_range_t)range; + /*! * @brief Returns whether the string contains the specified string. * * @param string The string to search * @return Whether the string contains the specified string Index: src/OFString.m ================================================================== --- src/OFString.m +++ src/OFString.m @@ -31,12 +31,13 @@ #import "OFString.h" #import "OFString_UTF8.h" #import "OFString_UTF8+Private.h" #import "OFArray.h" -#import "OFDictionary.h" +#import "OFCharacterSet.h" #import "OFData.h" +#import "OFDictionary.h" #import "OFLocalization.h" #ifdef OF_HAVE_FILES # import "OFFile.h" # import "OFFileManager.h" #endif @@ -1850,13 +1851,12 @@ @throw [OFOutOfRangeException exception]; pool = objc_autoreleasePoolPush(); searchCharacters = [string characters]; - characters = malloc(range.length * sizeof(of_unichar_t)); - if (characters == NULL) + if ((characters = malloc(range.length * sizeof(of_unichar_t))) == NULL) @throw [OFOutOfMemoryException exceptionWithRequestedSize: range.length * sizeof(of_unichar_t)]; @try { [self getCharacters: characters @@ -1892,10 +1892,73 @@ objc_autoreleasePoolPop(pool); return of_range(OF_NOT_FOUND, 0); } + +- (size_t)indexOfCharacterFromSet: (OFCharacterSet *)characterSet +{ + return [self indexOfCharacterFromSet: characterSet + options: 0 + range: of_range(0, [self length])]; +} + +- (size_t)indexOfCharacterFromSet: (OFCharacterSet *)characterSet + options: (int)options +{ + return [self indexOfCharacterFromSet: characterSet + options: options + range: of_range(0, [self length])]; +} + +- (size_t)indexOfCharacterFromSet: (OFCharacterSet *)characterSet + options: (int)options + range: (of_range_t)range +{ + bool (*characterIsMember)(id, SEL, of_unichar_t) = + (bool (*)(id, SEL, of_unichar_t))[characterSet + methodForSelector: @selector(characterIsMember:)]; + of_unichar_t *characters; + + if (range.length == 0) + return OF_NOT_FOUND; + + if (range.length > SIZE_MAX / sizeof(of_unichar_t)) + @throw [OFOutOfRangeException exception]; + + if ((characters = malloc(range.length * sizeof(of_unichar_t))) == NULL) + @throw [OFOutOfMemoryException exceptionWithRequestedSize: + range.length * sizeof(of_unichar_t)]; + + @try { + [self getCharacters: characters + inRange: range]; + + if (options & OF_STRING_SEARCH_BACKWARDS) { + for (size_t i = range.length - 1;; i--) { + if (characterIsMember(characterSet, + @selector(characterIsMember:), + characters[i])) + return range.location + i; + + /* No match and we're at the last character */ + if (i == 0) + break; + } + } else { + for (size_t i = 0; i < range.length; i++) + if (characterIsMember(characterSet, + @selector(characterIsMember:), + characters[i])) + return range.location + i; + } + } @finally { + free(characters); + } + + return OF_NOT_FOUND; +} - (bool)containsString: (OFString *)string { void *pool; const of_unichar_t *characters, *searchCharacters; Index: tests/OFStringTests.m ================================================================== --- tests/OFStringTests.m +++ tests/OFStringTests.m @@ -224,10 +224,11 @@ OFString *is; OFArray *a; int i; const of_unichar_t *ua; const uint16_t *u16a; + OFCharacterSet *cs; EntityHandler *h; #ifdef OF_HAVE_BLOCKS __block int j; __block bool ok; #endif @@ -501,10 +502,39 @@ [C(@"𝄞öö") rangeOfString: @"𝄞" options: OF_STRING_SEARCH_BACKWARDS].location == 0 && [C(@"𝄞öö") rangeOfString: @"x" options: OF_STRING_SEARCH_BACKWARDS].location == OF_NOT_FOUND) + EXPECT_EXCEPTION( + @"Detect out of range in -[rangeOfString:options:range:]", + OFOutOfRangeException, + [C(@"𝄞öö") rangeOfString: @"ö" + options: 0 + range: of_range(3, 1)]) + + cs = [OFCharacterSet characterSetWithCharactersInString: @"cđ"]; + TEST(@"-[indexOfCharacterFromSet:]", + [C(@"abcđabcđe") indexOfCharacterFromSet: cs] == 2 && + [C(@"abcđabcđë") + indexOfCharacterFromSet: cs + options: OF_STRING_SEARCH_BACKWARDS] == 7 && + [C(@"abcđabcđë") + indexOfCharacterFromSet: cs + options: 0 + range: of_range(4, 4)] == 6 && + [C(@"abcđabcđëf") + indexOfCharacterFromSet: cs + options: 0 + range: of_range(8, 2)] == OF_NOT_FOUND) + + EXPECT_EXCEPTION( + @"Detect out of range in -[indexOfCharacterFromSet:options:range:]", + OFOutOfRangeException, + [C(@"𝄞öö") indexOfCharacterFromSet: cs + options: 0 + range: of_range(3, 1)]) + TEST(@"-[substringWithRange:]", [[C(@"𝄞öö") substringWithRange: of_range(1, 1)] isEqual: @"ö"] && [[C(@"𝄞öö") substringWithRange: of_range(3, 0)] isEqual: @""]) EXPECT_EXCEPTION(@"Detect out of range in -[substringWithRange:] #1", @@ -781,12 +811,11 @@ TEST(@"-[SHA512Hash]", [[C(@"asdfoobar") SHA512Hash] isEqual: @"0464c427da158b02161bb44a3090bbfc594611ef6a53603640454b56412a9247c" @"3579a329e53a5dc74676b106755e3394f9454a2d42273242615d32f80437d61"]) - OFCharacterSet *cs = [OFCharacterSet - characterSetWithCharactersInString: @"abfo'_~$🍏"]; + cs = [OFCharacterSet characterSetWithCharactersInString: @"abfo'_~$🍏"]; TEST(@"-[stringByURLEncodingWithAllowedCharacters:]", [[C(@"foo\"ba'_~$]🍏🍌") stringByURLEncodingWithAllowedCharacters: cs] isEqual: @"foo%22ba'_~$%5D🍏%F0%9F%8D%8C"]) TEST(@"-[stringByURLDecoding]",