From 07031e16511c0588a842981881aafcc7ab0dbb93 Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Fri, 19 Nov 2021 08:42:23 -0800 Subject: [PATCH] [string] new functions for case-insensitive comparison (#7189) This commit adds new string helper functions to convert between lowercase and uppercase letters. It also adds new function `StringStartsWith()` which determines whether the string starts with a given prefix string and `StringMatch()` which compares and matches two strings. This commit also adds a new mechanism in all string helper functions allowing the caller to specify the match mode to be used when comparing strings: Either an exact match of characters, or a case-insensitive match where uppercase and lowercase characters are treated as equal. This commit also updates `test_string` unit test to validate the new functions and behaviors. --- src/core/common/string.cpp | 117 ++++++++++++++++++++++++++++--- src/core/common/string.hpp | 101 +++++++++++++++++++++++++-- tests/unit/test_string.cpp | 136 +++++++++++++++++++++++++++++++++---- 3 files changed, 324 insertions(+), 30 deletions(-) diff --git a/src/core/common/string.cpp b/src/core/common/string.cpp index 612054f91..881f2c8a7 100644 --- a/src/core/common/string.cpp +++ b/src/core/common/string.cpp @@ -28,7 +28,7 @@ /** * @file - * This file implements OpenThread String class. + * This file implements OpenThread String class and functions. */ #include "string.hpp" @@ -38,11 +38,60 @@ namespace ot { +namespace { + +// The definitions below are included in an unnamed namespace +// to limit their scope to this translation unit (this file). + +enum MatchType : uint8_t +{ + kNoMatch, + kPrefixMatch, + kFullMatch, +}; + +MatchType Match(const char *aString, const char *aPrefixString, StringMatchMode aMode) +{ + // This is file private function that is used by other functions. + // It matches @p aString with @p aPrefixString using match @ aMode. + // + // If @p aString and @p aPrefixString match and have the + // same length `kFullMatch` is returned. If @p aString starts + // with @p aPrefixString but contains more characters, then + // `kPrefixMatch` is returned. Otherwise `kNoMatch` is returned. + + MatchType match = kNoMatch; + + switch (aMode) + { + case kStringExactMatch: + while (*aPrefixString != kNullChar) + { + VerifyOrExit(*aString++ == *aPrefixString++); + } + break; + + case kStringCaseInsensitiveMatch: + while (*aPrefixString != kNullChar) + { + VerifyOrExit(ToLowercase(*aString++) == ToLowercase(*aPrefixString++)); + } + break; + } + + match = (*aString == kNullChar) ? kFullMatch : kPrefixMatch; + +exit: + return match; +} + +} // namespace + uint16_t StringLength(const char *aString, uint16_t aMaxLength) { uint16_t ret; - for (ret = 0; (ret < aMaxLength) && (aString[ret] != 0); ret++) + for (ret = 0; (ret < aMaxLength) && (aString[ret] != kNullChar); ret++) { // Empty loop. } @@ -54,7 +103,7 @@ const char *StringFind(const char *aString, char aChar) { const char *ret = nullptr; - for (; *aString != '\0'; aString++) + for (; *aString != kNullChar; aString++) { if (*aString == aChar) { @@ -66,7 +115,7 @@ const char *StringFind(const char *aString, char aChar) return ret; } -const char *StringFind(const char *aString, const char *aSubString) +const char *StringFind(const char *aString, const char *aSubString, StringMatchMode aMode) { const char *ret = nullptr; size_t len = strlen(aString); @@ -76,7 +125,7 @@ const char *StringFind(const char *aString, const char *aSubString) for (size_t index = 0; index <= static_cast(len - subLen); index++) { - if (memcmp(&aString[index], aSubString, subLen) == 0) + if (Match(&aString[index], aSubString, aMode) != kNoMatch) { ExitNow(ret = &aString[index]); } @@ -86,19 +135,65 @@ exit: return ret; } +bool StringStartsWith(const char *aString, const char *aPrefixString, StringMatchMode aMode) +{ + return Match(aString, aPrefixString, aMode) != kNoMatch; +} + bool StringEndsWith(const char *aString, char aChar) { size_t len = strlen(aString); - return len > 0 && aString[len - 1] == aChar; + return (len > 0) && (aString[len - 1] == aChar); } -bool StringEndsWith(const char *aString, const char *aSubString) +bool StringEndsWith(const char *aString, const char *aSubString, StringMatchMode aMode) { size_t len = strlen(aString); size_t subLen = strlen(aSubString); - return (subLen > 0) && (len >= subLen) && (memcmp(aSubString, &aString[len - subLen], subLen) == 0); + return (subLen > 0) && (len >= subLen) && (Match(&aString[len - subLen], aSubString, aMode) != kNoMatch); +} + +bool StringMatch(const char *aFirstString, const char *aSecondString, StringMatchMode aMode) +{ + return Match(aFirstString, aSecondString, aMode) == kFullMatch; +} + +void StringConvertToLowercase(char *aString) +{ + for (; *aString != kNullChar; aString++) + { + *aString = ToLowercase(*aString); + } +} + +void StringConvertToUppercase(char *aString) +{ + for (; *aString != kNullChar; aString++) + { + *aString = ToUppercase(*aString); + } +} + +char ToLowercase(char aChar) +{ + if ((aChar >= 'A') && (aChar <= 'Z')) + { + aChar += 'a' - 'A'; + } + + return aChar; +} + +char ToUppercase(char aChar) +{ + if ((aChar >= 'a') && (aChar <= 'z')) + { + aChar -= 'a' - 'A'; + } + + return aChar; } StringWriter::StringWriter(char *aBuffer, uint16_t aSize) @@ -106,12 +201,12 @@ StringWriter::StringWriter(char *aBuffer, uint16_t aSize) , mLength(0) , mSize(aSize) { - mBuffer[0] = '\0'; + mBuffer[0] = kNullChar; } StringWriter &StringWriter::Clear(void) { - mBuffer[0] = '\0'; + mBuffer[0] = kNullChar; mLength = 0; return *this; } @@ -137,7 +232,7 @@ StringWriter &StringWriter::AppendVarArgs(const char *aFormat, va_list aArgs) if (IsTruncated()) { - mBuffer[mSize - 1] = '\0'; + mBuffer[mSize - 1] = kNullChar; } return *this; diff --git a/src/core/common/string.hpp b/src/core/common/string.hpp index 307079396..982feea86 100644 --- a/src/core/common/string.hpp +++ b/src/core/common/string.hpp @@ -55,6 +55,18 @@ namespace ot { * */ +/** + * This enumeration represents comparison mode when matching strings. + * + */ +enum StringMatchMode : uint8_t +{ + kStringExactMatch, ///< Exact match of characters. + kStringCaseInsensitiveMatch, ///< Case insensitive match (uppercase and lowercase characters are treated as equal). +}; + +static constexpr char kNullChar = '\0'; ///< null character. + /** * This function returns the number of characters that precede the terminating nullptr character. * @@ -83,11 +95,26 @@ const char *StringFind(const char *aString, char aChar); * * @param[in] aString A pointer to the string. * @param[in] aSubString A sub-string to search for. + * @param[in] aMode The string comparison mode, exact match or case insensitive match. * - * @returns The pointer to first occurrence of the @p aSubString in @p aString, or nullptr if cannot be found. + * @returns The pointer to first match of the @p aSubString in @p aString (using comparison @p aMode), or nullptr if + * cannot be found. * */ -const char *StringFind(const char *aString, const char *aSubString); +const char *StringFind(const char *aString, const char *aSubString, StringMatchMode aMode = kStringExactMatch); + +/** + * This function checks whether a null-terminated string starts with a given prefix string. + * + * @param[in] aString A pointer to the string. + * @param[in] aPrefixString A prefix string. + * @param[in] aMode The string comparison mode, exact match or case insensitive match. + * + * @retval TRUE If @p aString starts with @p aPrefixString. + * @retval FALSE If @p aString does not start with @p aPrefixString. + * + */ +bool StringStartsWith(const char *aString, const char *aPrefixString, StringMatchMode aMode = kStringExactMatch); /** * This function checks whether a null-terminated string ends with a given character. @@ -105,13 +132,67 @@ bool StringEndsWith(const char *aString, char aChar); * This function checks whether a null-terminated string ends with a given sub-string. * * @param[in] aString A pointer to the string. - * @param[in] aSubString A sun-string to check against. + * @param[in] aSubString A sub-string to check against. + * @param[in] aMode The string comparison mode, exact match or case insensitive match. * * @retval TRUE If @p aString ends with sub-string @p aSubString. * @retval FALSE If @p aString does not end with sub-string @p aSubString. * */ -bool StringEndsWith(const char *aString, const char *aSubString); +bool StringEndsWith(const char *aString, const char *aSubString, StringMatchMode aMode = kStringExactMatch); + +/** + * This method checks whether or not two null-terminated strings match. + * + * @param[in] aFirstString A pointer to the first string. + * @param[in] aSecondString A pointer to the second string. + * @param[in] aMode The string comparison mode, exact match or case insensitive match. + * + * @retval TRUE If @p aFirstString matches @p aSecondString using match mode @p aMode. + * @retval FALSE If @p aFirstString does not match @p aSecondString using match mode @p aMode. + * + */ +bool StringMatch(const char *aFirstString, const char *aSecondString, StringMatchMode aMode = kStringExactMatch); + +/** + * This function converts all uppercase letter characters in a given string to lowercase. + * + * @param[inout] aString A pointer to the string to convert. + * + */ +void StringConvertToLowercase(char *aString); + +/** + * This function converts all lowercase letter characters in a given string to uppercase. + * + * @param[inout] aString A pointer to the string to convert. + * + */ +void StringConvertToUppercase(char *aString); + +/** + * This function converts an uppercase letter character to lowercase. + * + * If @p aChar is uppercase letter it is converted lowercase. Otherwise, it remains unchanged. + * + * @param[in] aChar The character to convert + * + * @returns The character converted to lowercase. + * + */ +char ToLowercase(char aChar); + +/** + * This function converts a lowercase letter character to uppercase. + * + * If @p aChar is lowercase letter it is converted uppercase. Otherwise, it remains unchanged. + * + * @param[in] aChar The character to convert + * + * @returns The character converted to uppercase. + * + */ +char ToUppercase(char aChar); /** * This class implements writing to a string buffer. @@ -199,6 +280,18 @@ public: */ StringWriter &AppendHexBytes(const uint8_t *aBytes, uint16_t aLength); + /** + * This method converts all uppercase letter characters in the string to lowercase. + * + */ + void ConvertToLowercase(void) { StringConvertToLowercase(mBuffer); } + + /** + * This method converts all lowercase letter characters in the string to uppercase. + * + */ + void ConvertToUppercase(void) { StringConvertToUppercase(mBuffer); } + private: char * mBuffer; uint16_t mLength; diff --git a/tests/unit/test_string.cpp b/tests/unit/test_string.cpp index c03846090..8af8188aa 100644 --- a/tests/unit/test_string.cpp +++ b/tests/unit/test_string.cpp @@ -49,7 +49,7 @@ template void PrintString(const char *aName, const String str; - constexpr char kLongString[] = "abcdefghijklmnopqratuvwxyzabcdefghijklmnopqratuvwxyz"; + const char kLongString[] = "abcdefghijklmnopqratuvwxyzabcdefghijklmnopqratuvwxyz"; printf("\nTest 1: StringWriter constructor\n"); @@ -147,12 +147,12 @@ void TestUtf8(void) void TestStringFind(void) { char emptyString[1] = {'\0'}; - char testString[] = "foo.bar.bar\\."; + char testString[] = "Foo.bar.bar\\."; char testString2[] = "abcabcabcdabc"; printf("\nTest 6: StringFind() function\n"); - VerifyOrQuit(StringFind(testString, 'f') == testString); + VerifyOrQuit(StringFind(testString, 'F') == testString); VerifyOrQuit(StringFind(testString, 'o') == &testString[1]); VerifyOrQuit(StringFind(testString, '.') == &testString[3]); VerifyOrQuit(StringFind(testString, 'r') == &testString[6]); @@ -160,17 +160,19 @@ void TestStringFind(void) VerifyOrQuit(StringFind(testString, 'x') == nullptr); VerifyOrQuit(StringFind(testString, ',') == nullptr); - VerifyOrQuit(StringFind(emptyString, 'f') == nullptr); + VerifyOrQuit(StringFind(emptyString, 'F') == nullptr); VerifyOrQuit(StringFind(emptyString, '.') == nullptr); - VerifyOrQuit(StringFind(testString, "foo") == &testString[0]); + VerifyOrQuit(StringFind(testString, "Foo") == &testString[0]); VerifyOrQuit(StringFind(testString, "oo") == &testString[1]); VerifyOrQuit(StringFind(testString, "bar") == &testString[4]); VerifyOrQuit(StringFind(testString, "bar\\") == &testString[8]); VerifyOrQuit(StringFind(testString, "\\.") == &testString[11]); VerifyOrQuit(StringFind(testString, testString) == testString); - VerifyOrQuit(StringFind(testString, "fooo") == nullptr); - VerifyOrQuit(StringFind(testString, "far") == nullptr); + VerifyOrQuit(StringFind(testString, "Fooo") == nullptr); + VerifyOrQuit(StringFind(testString, "Far") == nullptr); + VerifyOrQuit(StringFind(testString, "FOO") == nullptr); + VerifyOrQuit(StringFind(testString, "BAR") == nullptr); VerifyOrQuit(StringFind(testString, "bar\\..") == nullptr); VerifyOrQuit(StringFind(testString, "") == &testString[0]); @@ -182,27 +184,128 @@ void TestStringFind(void) VerifyOrQuit(StringFind(testString2, "abcabc") == &testString2[0]); VerifyOrQuit(StringFind(testString2, "abcabcd") == &testString2[3]); + VerifyOrQuit(StringFind(testString, "FOO", kStringCaseInsensitiveMatch) == &testString[0]); + VerifyOrQuit(StringFind(testString, "OO", kStringCaseInsensitiveMatch) == &testString[1]); + VerifyOrQuit(StringFind(testString, "BAR", kStringCaseInsensitiveMatch) == &testString[4]); + VerifyOrQuit(StringFind(testString, "BAR\\", kStringCaseInsensitiveMatch) == &testString[8]); + VerifyOrQuit(StringFind(testString, "\\.", kStringCaseInsensitiveMatch) == &testString[11]); + VerifyOrQuit(StringFind(testString, testString) == testString); + VerifyOrQuit(StringFind(testString, "FOOO", kStringCaseInsensitiveMatch) == nullptr); + VerifyOrQuit(StringFind(testString, "FAR", kStringCaseInsensitiveMatch) == nullptr); + VerifyOrQuit(StringFind(testString, "BAR\\..", kStringCaseInsensitiveMatch) == nullptr); + VerifyOrQuit(StringFind(testString, "", kStringCaseInsensitiveMatch) == &testString[0]); + + VerifyOrQuit(StringFind(emptyString, "FOO", kStringCaseInsensitiveMatch) == nullptr); + VerifyOrQuit(StringFind(emptyString, "BAR", kStringCaseInsensitiveMatch) == nullptr); + VerifyOrQuit(StringFind(emptyString, "", kStringCaseInsensitiveMatch) == &emptyString[0]); + + // Verify when sub-string has repeated patterns + VerifyOrQuit(StringFind(testString2, "ABCABC", kStringCaseInsensitiveMatch) == &testString2[0]); + VerifyOrQuit(StringFind(testString2, "ABCABCD", kStringCaseInsensitiveMatch) == &testString2[3]); + + printf(" -- PASS\n"); +} + +void TestStringStartsWith(void) +{ + printf("\nTest 7: StringStartsWith() function\n"); + + VerifyOrQuit(StringStartsWith("FooBar", "Foo")); + VerifyOrQuit(!StringStartsWith("FooBar", "Ba")); + VerifyOrQuit(StringStartsWith("FooBar", "FooBar")); + VerifyOrQuit(!StringStartsWith("FooBar", "FooBarr")); + VerifyOrQuit(!StringStartsWith("FooBar", "foo")); + VerifyOrQuit(!StringStartsWith("FooBar", "FoO")); + + VerifyOrQuit(!StringStartsWith("", "foo")); + + VerifyOrQuit(StringStartsWith("FooBar", "FOO", kStringCaseInsensitiveMatch)); + VerifyOrQuit(!StringStartsWith("FooBar", "BA", kStringCaseInsensitiveMatch)); + VerifyOrQuit(StringStartsWith("FooBar", "FOOBAR", kStringCaseInsensitiveMatch)); + VerifyOrQuit(!StringStartsWith("FooBar", "FooBarr", kStringCaseInsensitiveMatch)); + VerifyOrQuit(StringStartsWith("FooBar", "foO", kStringCaseInsensitiveMatch)); + + VerifyOrQuit(!StringStartsWith("", "foo", kStringCaseInsensitiveMatch)); + printf(" -- PASS\n"); } void TestStringEndsWith(void) { - printf("\nTest 7: StringEndsWith() function\n"); + printf("\nTest 8: StringEndsWith() function\n"); - VerifyOrQuit(StringEndsWith("foobar", 'r')); - VerifyOrQuit(!StringEndsWith("foobar", 'a')); - VerifyOrQuit(!StringEndsWith("foobar", '\0')); + VerifyOrQuit(StringEndsWith("FooBar", 'r')); + VerifyOrQuit(!StringEndsWith("FooBar", 'a')); + VerifyOrQuit(!StringEndsWith("FooBar", '\0')); VerifyOrQuit(StringEndsWith("a", 'a')); VerifyOrQuit(!StringEndsWith("a", 'b')); - VerifyOrQuit(StringEndsWith("foobar", "bar")); - VerifyOrQuit(!StringEndsWith("foobar", "ba")); - VerifyOrQuit(StringEndsWith("foobar", "foobar")); - VerifyOrQuit(!StringEndsWith("foobar", "foobarr")); + VerifyOrQuit(StringEndsWith("FooBar", "Bar")); + VerifyOrQuit(!StringEndsWith("FooBar", "Ba")); + VerifyOrQuit(StringEndsWith("FooBar", "FooBar")); + VerifyOrQuit(!StringEndsWith("FooBar", "FooBarr")); VerifyOrQuit(!StringEndsWith("", 'a')); VerifyOrQuit(!StringEndsWith("", "foo")); + VerifyOrQuit(StringEndsWith("FooBar", "baR", kStringCaseInsensitiveMatch)); + VerifyOrQuit(!StringEndsWith("FooBar", "bA", kStringCaseInsensitiveMatch)); + VerifyOrQuit(StringEndsWith("FooBar", "fOOBar", kStringCaseInsensitiveMatch)); + VerifyOrQuit(!StringEndsWith("FooBar", "Foobarr", kStringCaseInsensitiveMatch)); + VerifyOrQuit(!StringEndsWith("", "Foo", kStringCaseInsensitiveMatch)); + + printf(" -- PASS\n"); +} + +void TestStringMatch(void) +{ + printf("\nTest 9: StringMatch() function\n"); + + VerifyOrQuit(StringMatch("", "")); + VerifyOrQuit(StringMatch("FooBar", "FooBar")); + VerifyOrQuit(!StringMatch("FooBar", "FooBa")); + VerifyOrQuit(!StringMatch("FooBa", "FooBar")); + VerifyOrQuit(!StringMatch("FooBa", "FooBar")); + VerifyOrQuit(!StringMatch("FooBar", "fooBar")); + VerifyOrQuit(!StringMatch("FooBaR", "FooBar")); + + VerifyOrQuit(StringMatch("", "", kStringCaseInsensitiveMatch)); + VerifyOrQuit(StringMatch("FooBar", "fOObAR", kStringCaseInsensitiveMatch)); + VerifyOrQuit(!StringMatch("FooBar", "fOObA", kStringCaseInsensitiveMatch)); + VerifyOrQuit(!StringMatch("FooBa", "FooBar", kStringCaseInsensitiveMatch)); + VerifyOrQuit(!StringMatch("FooBa", "FooBar", kStringCaseInsensitiveMatch)); + VerifyOrQuit(!StringMatch("Fooba", "fooBar", kStringCaseInsensitiveMatch)); + VerifyOrQuit(StringMatch("FooBar", "FOOBAR", kStringCaseInsensitiveMatch)); + VerifyOrQuit(StringMatch("FoobaR", "FooBar", kStringCaseInsensitiveMatch)); + VerifyOrQuit(StringMatch("FOOBAR", "foobar", kStringCaseInsensitiveMatch)); + + printf(" -- PASS\n"); +} + +void TestStringToLowercase(void) +{ + const uint16_t kMaxSize = 100; + + const char kTestString[] = "!@#$%^&*()_+=[].,<>//;:\"'`~ \t\r\n"; + const char kUppercaseString[] = "ABCDEFGHIJKLMNOPQRATUVWXYZABCDEFGHIJKLMNOPQRATUVWXYZ"; + const char kLowercaseString[] = "abcdefghijklmnopqratuvwxyzabcdefghijklmnopqratuvwxyz"; + + char string[kMaxSize]; + + printf("\nTest 10: StringConvertToLowercase() function\n"); + + memcpy(string, kTestString, sizeof(kTestString)); + StringConvertToLowercase(string); + VerifyOrQuit(memcmp(string, kTestString, sizeof(kTestString)) == 0); + StringConvertToUppercase(string); + VerifyOrQuit(memcmp(string, kTestString, sizeof(kTestString)) == 0); + + memcpy(string, kUppercaseString, sizeof(kUppercaseString)); + StringConvertToLowercase(string); + VerifyOrQuit(memcmp(string, kLowercaseString, sizeof(kLowercaseString)) == 0); + StringConvertToUppercase(string); + VerifyOrQuit(memcmp(string, kUppercaseString, sizeof(kUppercaseString)) == 0); + printf(" -- PASS\n"); } @@ -214,7 +317,10 @@ int main(void) ot::TestStringLength(); ot::TestUtf8(); ot::TestStringFind(); + ot::TestStringStartsWith(); ot::TestStringEndsWith(); + ot::TestStringMatch(); + ot::TestStringToLowercase(); printf("\nAll tests passed.\n"); return 0; }