From 4dd5f994be237794d4782fc790648768de256346 Mon Sep 17 00:00:00 2001 From: Rubidium Date: Thu, 27 Apr 2023 13:21:08 +0200 Subject: [PATCH] Codechange: replace strncasecmp with case ignoring variant of StrStarts/EndsWith --- src/console_cmds.cpp | 16 +- src/fileio.cpp | 2 +- src/fios.cpp | 2 +- src/hotkeys.cpp | 3 +- src/network/network_chat_gui.cpp | 2 +- src/os/windows/font_win32.cpp | 4 +- src/stdafx.h | 17 -- src/string.cpp | 39 +++- src/string_func.h | 8 +- src/tests/string_func.cpp | 358 +++++++++++++++++++++++++++++++ 10 files changed, 410 insertions(+), 41 deletions(-) diff --git a/src/console_cmds.cpp b/src/console_cmds.cpp index 6d0ca5cdf2..4827d30cf0 100644 --- a/src/console_cmds.cpp +++ b/src/console_cmds.cpp @@ -2022,7 +2022,7 @@ DEF_CONSOLE_CMD(ConFont) byte arg_index = 2; /* We may encounter "aa" or "noaa" but it must be the last argument. */ if (StrEqualsIgnoreCase(argv[arg_index], "aa") || StrEqualsIgnoreCase(argv[arg_index], "noaa")) { - aa = strncasecmp(argv[arg_index++], "no", 2) != 0; + aa = !StrStartsWithIgnoreCase(argv[arg_index++], "no"); if (argc > arg_index) return false; } else { /* For we want a string. */ @@ -2044,7 +2044,7 @@ DEF_CONSOLE_CMD(ConFont) if (argc > arg_index) { /* Last argument must be "aa" or "noaa". */ if (!StrEqualsIgnoreCase(argv[arg_index], "aa") && !StrEqualsIgnoreCase(argv[arg_index], "noaa")) return false; - aa = strncasecmp(argv[arg_index++], "no", 2) != 0; + aa = !StrStartsWithIgnoreCase(argv[arg_index++], "no"); if (argc > arg_index) return false; } @@ -2220,7 +2220,7 @@ DEF_CONSOLE_CMD(ConNewGRFProfile) const std::vector &files = GetAllGRFFiles(); /* "list" sub-command */ - if (argc == 1 || strncasecmp(argv[1], "lis", 3) == 0) { + if (argc == 1 || StrStartsWithIgnoreCase(argv[1], "lis")) { IConsolePrint(CC_INFO, "Loaded GRF files:"); int i = 1; for (GRFFile *grf : files) { @@ -2236,7 +2236,7 @@ DEF_CONSOLE_CMD(ConNewGRFProfile) } /* "select" sub-command */ - if (strncasecmp(argv[1], "sel", 3) == 0 && argc >= 3) { + if (StrStartsWithIgnoreCase(argv[1], "sel") && argc >= 3) { for (size_t argnum = 2; argnum < argc; ++argnum) { int grfnum = atoi(argv[argnum]); if (grfnum < 1 || grfnum > (int)files.size()) { // safe cast, files.size() should not be larger than a few hundred in the most extreme cases @@ -2254,7 +2254,7 @@ DEF_CONSOLE_CMD(ConNewGRFProfile) } /* "unselect" sub-command */ - if (strncasecmp(argv[1], "uns", 3) == 0 && argc >= 3) { + if (StrStartsWithIgnoreCase(argv[1], "uns") && argc >= 3) { for (size_t argnum = 2; argnum < argc; ++argnum) { if (StrEqualsIgnoreCase(argv[argnum], "all")) { _newgrf_profilers.clear(); @@ -2273,7 +2273,7 @@ DEF_CONSOLE_CMD(ConNewGRFProfile) } /* "start" sub-command */ - if (strncasecmp(argv[1], "sta", 3) == 0) { + if (StrStartsWithIgnoreCase(argv[1], "sta")) { std::string grfids; size_t started = 0; for (NewGRFProfiler &pr : _newgrf_profilers) { @@ -2309,13 +2309,13 @@ DEF_CONSOLE_CMD(ConNewGRFProfile) } /* "stop" sub-command */ - if (strncasecmp(argv[1], "sto", 3) == 0) { + if (StrStartsWithIgnoreCase(argv[1], "sto")) { NewGRFProfiler::FinishAll(); return true; } /* "abort" sub-command */ - if (strncasecmp(argv[1], "abo", 3) == 0) { + if (StrStartsWithIgnoreCase(argv[1], "abo")) { for (NewGRFProfiler &pr : _newgrf_profilers) { pr.Abort(); } diff --git a/src/fileio.cpp b/src/fileio.cpp index 73b13e3117..758b2cf2a5 100644 --- a/src/fileio.cpp +++ b/src/fileio.cpp @@ -769,7 +769,7 @@ static bool ChangeWorkingDirectoryToExecutable(const char *exe) bool success = false; #ifdef WITH_COCOA char *app_bundle = strchr(tmp, '.'); - while (app_bundle != nullptr && strncasecmp(app_bundle, ".app", 4) != 0) app_bundle = strchr(&app_bundle[1], '.'); + while (app_bundle != nullptr && !StrStartsWithIgnoreCase(app_bundle, ".app")) app_bundle = strchr(&app_bundle[1], '.'); if (app_bundle != nullptr) *app_bundle = '\0'; #endif /* WITH_COCOA */ diff --git a/src/fios.cpp b/src/fios.cpp index 23fa6af2f7..672b9fb5df 100644 --- a/src/fios.cpp +++ b/src/fios.cpp @@ -386,7 +386,7 @@ static void FiosGetFileList(SaveLoadOperation fop, fios_getlist_callback_proc *c /* found file must be directory, but not '.' or '..' */ if (FiosIsValidFile(_fios_path->c_str(), dirent, &sb) && S_ISDIR(sb.st_mode) && - (!FiosIsHiddenFile(dirent) || strncasecmp(d_name, PERSONAL_DIR, strlen(d_name)) == 0) && + (!FiosIsHiddenFile(dirent) || StrStartsWithIgnoreCase(PERSONAL_DIR, d_name)) && strcmp(d_name, ".") != 0 && strcmp(d_name, "..") != 0) { fios = &file_list.emplace_back(); fios->type = FIOS_TYPE_DIR; diff --git a/src/hotkeys.cpp b/src/hotkeys.cpp index 0a10290c18..73c7f52bd5 100644 --- a/src/hotkeys.cpp +++ b/src/hotkeys.cpp @@ -99,8 +99,9 @@ static uint16 ParseCode(const char *start, const char *end) assert(start <= end); while (start < end && *start == ' ') start++; while (end > start && *end == ' ') end--; + std::string_view str{start, (size_t)(start - end)}; for (uint i = 0; i < lengthof(_keycode_to_name); i++) { - if (strlen(_keycode_to_name[i].name) == (size_t)(end - start) && strncasecmp(start, _keycode_to_name[i].name, end - start) == 0) { + if (StrEqualsIgnoreCase(str, _keycode_to_name[i].name)) { return _keycode_to_name[i].keycode; } } diff --git a/src/network/network_chat_gui.cpp b/src/network/network_chat_gui.cpp index 6a0d2cb916..20fd7cedf9 100644 --- a/src/network/network_chat_gui.cpp +++ b/src/network/network_chat_gui.cpp @@ -421,7 +421,7 @@ struct NetworkChatWindow : public Window { } len = strlen(cur_name); - if (tb_len < len && strncasecmp(cur_name, tb_buf, tb_len) == 0) { + if (tb_len < len && StrStartsWith(cur_name, tb_buf)) { /* Save the data it was before completion */ if (!second_scan) seprintf(_chat_tab_completion_buf, lastof(_chat_tab_completion_buf), "%s", tb->buf); _chat_tab_completion_active = true; diff --git a/src/os/windows/font_win32.cpp b/src/os/windows/font_win32.cpp index 8a916db67a..7de608c97c 100644 --- a/src/os/windows/font_win32.cpp +++ b/src/os/windows/font_win32.cpp @@ -134,9 +134,9 @@ FT_Error GetFontByFaceName(const char *font_name, FT_Face *face) err = FT_New_Face(_library, font_path, index, face); if (err != FT_Err_Ok) break; - if (strncasecmp(font_name, (*face)->family_name, strlen((*face)->family_name)) == 0) break; + if (StrStartsWithIgnoreCase(font_name, (*face)->family_name)) break; /* Try english name if font name failed */ - if (strncasecmp(font_name + strlen(font_name) + 1, (*face)->family_name, strlen((*face)->family_name)) == 0) break; + if (StrStartsWithIgnoreCase(font_name + strlen(font_name) + 1, (*face)->family_name)) break; err = FT_Err_Cannot_Open_Resource; } while ((FT_Long)++index != (*face)->num_faces); diff --git a/src/stdafx.h b/src/stdafx.h index 57e8a1ae8b..cfbf724bf6 100644 --- a/src/stdafx.h +++ b/src/stdafx.h @@ -37,10 +37,6 @@ # define _GNU_SOURCE #endif -#if defined(__HAIKU__) || defined(__CYGWIN__) -# include /* strncasecmp */ -#endif - /* It seems that we need to include stdint.h before anything else * We need INT64_MAX, which for most systems comes from stdint.h. However, MSVC * does not have stdint.h. @@ -71,11 +67,6 @@ # include #endif -#if defined(__OS2__) -# include -# define strcasecmp stricmp -#endif - /* Stuff for GCC */ #if defined(__GNUC__) || (defined(__clang__) && !defined(_MSC_VER)) # define NORETURN __attribute__ ((noreturn)) @@ -207,9 +198,6 @@ # endif # endif -# define strcasecmp stricmp -# define strncasecmp strnicmp - /* MSVC doesn't have these :( */ # define S_ISDIR(mode) (mode & S_IFDIR) # define S_ISREG(mode) (mode & S_IFREG) @@ -411,11 +399,6 @@ void NORETURN AssertFailedError(int line, const char *file, const char *expressi # define assert(expression) if (unlikely(!(expression))) AssertFailedError(__LINE__, __FILE__, #expression); #endif -#if defined(OPENBSD) - /* OpenBSD uses strcasecmp(3) */ -# define _stricmp strcasecmp -#endif - #if defined(MAX_PATH) /* It's already defined, no need to override */ #elif defined(PATH_MAX) && PATH_MAX > 0 diff --git a/src/string.cpp b/src/string.cpp index 0f853f9112..0ff554f229 100644 --- a/src/string.cpp +++ b/src/string.cpp @@ -23,26 +23,27 @@ #include #ifdef _MSC_VER -#include // required by vsnprintf implementation for MSVC +# include // required by vsnprintf implementation for MSVC +# define strncasecmp strnicmp #endif #ifdef _WIN32 -#include "os/windows/win32.h" +# include "os/windows/win32.h" #endif #ifdef WITH_UNISCRIBE -#include "os/windows/string_uniscribe.h" +# include "os/windows/string_uniscribe.h" #endif #ifdef WITH_ICU_I18N /* Required by strnatcmp. */ -#include -#include "language.h" -#include "gfx_func.h" +# include +# include "language.h" +# include "gfx_func.h" #endif /* WITH_ICU_I18N */ #if defined(WITH_COCOA) -#include "os/macosx/string_osx.h" +# include "os/macosx/string_osx.h" #endif /* The function vsnprintf is used internally to perform the required formatting @@ -355,6 +356,18 @@ bool StrStartsWith(const std::string_view str, const std::string_view prefix) return str.compare(0, prefix_len, prefix, 0, prefix_len) == 0; } +/** + * Check whether the given string starts with the given prefix, ignoring case. + * @param str The string to look at. + * @param prefix The prefix to look for. + * @return True iff the begin of the string is the same as the prefix, ignoring case. + */ +bool StrStartsWithIgnoreCase(std::string_view str, const std::string_view prefix) +{ + if (str.size() < prefix.size()) return false; + return StrEqualsIgnoreCase(str.substr(0, prefix.size()), prefix); +} + /** * Check whether the given string ends with the given suffix. * @param str The string to look at. @@ -396,6 +409,18 @@ struct CaseInsensitiveCharTraits : public std::char_traits { /** Case insensitive string view. */ typedef std::basic_string_view CaseInsensitiveStringView; +/** + * Check whether the given string ends with the given suffix, ignoring case. + * @param str The string to look at. + * @param suffix The suffix to look for. + * @return True iff the end of the string is the same as the suffix, ignoring case. + */ +bool StrEndsWithIgnoreCase(std::string_view str, const std::string_view suffix) +{ + if (str.size() < suffix.size()) return false; + return StrEqualsIgnoreCase(str.substr(str.size() - suffix.size()), suffix); +} + /** * Compares two string( view)s, while ignoring the case of the characters. * @param str1 The first string. diff --git a/src/string_func.h b/src/string_func.h index 5293691807..9ec5507faa 100644 --- a/src/string_func.h +++ b/src/string_func.h @@ -48,11 +48,13 @@ void str_strip_colours(char *str); bool strtolower(char *str); bool strtolower(std::string &str, std::string::size_type offs = 0); -bool StrValid(const char *str, const char *last) NOACCESS(2); +[[nodiscard]] bool StrValid(const char *str, const char *last) NOACCESS(2); void StrTrimInPlace(std::string &str); -bool StrStartsWith(const std::string_view str, const std::string_view prefix); -bool StrEndsWith(const std::string_view str, const std::string_view suffix); +[[nodiscard]] bool StrStartsWith(const std::string_view str, const std::string_view prefix); +[[nodiscard]] bool StrStartsWithIgnoreCase(std::string_view str, const std::string_view prefix); +[[nodiscard]] bool StrEndsWith(const std::string_view str, const std::string_view suffix); +[[nodiscard]] bool StrEndsWithIgnoreCase(std::string_view str, const std::string_view suffix); [[nodiscard]] int StrCompareIgnoreCase(const std::string_view str1, const std::string_view str2); [[nodiscard]] bool StrEqualsIgnoreCase(const std::string_view str1, const std::string_view str2); diff --git a/src/tests/string_func.cpp b/src/tests/string_func.cpp index 3797f894e7..d56d829fff 100644 --- a/src/tests/string_func.cpp +++ b/src/tests/string_func.cpp @@ -13,6 +13,8 @@ #include "../string_func.h" +/**** String compare/equals *****/ + TEST_CASE("StrCompareIgnoreCase - std::string") { /* Same string, with different cases. */ @@ -158,3 +160,359 @@ TEST_CASE("StrEqualsIgnoreCase - std::string_view") CHECK(!StrEqualsIgnoreCase(base.substr(0, 1), base.substr(0, 2))); // Same position, different lengths CHECK(!StrEqualsIgnoreCase(base.substr(0, 2), base.substr(0, 1))); // Same position, different lengths } + +/**** String starts with *****/ + +TEST_CASE("StrStartsWith - std::string") +{ + /* Everything starts with an empty prefix. */ + CHECK(StrStartsWith(std::string{""}, std::string{""})); + CHECK(StrStartsWith(std::string{"a"}, std::string{""})); + + /* Equal strings. */ + CHECK(StrStartsWith(std::string{"a"}, std::string{"a"})); + CHECK(StrStartsWith(std::string{"A"}, std::string{"A"})); + + /* Starts with same. */ + CHECK(StrStartsWith(std::string{"ab"}, std::string{"a"})); + CHECK(StrStartsWith(std::string{"Ab"}, std::string{"A"})); + + /* Different cases. */ + CHECK(!StrStartsWith(std::string{"a"}, std::string{"A"})); + CHECK(!StrStartsWith(std::string{"A"}, std::string{"a"})); + CHECK(!StrStartsWith(std::string{"ab"}, std::string{"A"})); + CHECK(!StrStartsWith(std::string{"Ab"}, std::string{"a"})); + + /* Does not start the same. */ + CHECK(!StrStartsWith(std::string{""}, std::string{"b"})); + CHECK(!StrStartsWith(std::string{"a"}, std::string{"b"})); + CHECK(!StrStartsWith(std::string{"b"}, std::string{"a"})); + CHECK(!StrStartsWith(std::string{"a"}, std::string{"aa"})); +} + +TEST_CASE("StrStartsWith - char pointer") +{ + CHECK(StrStartsWith("", "")); + CHECK(StrStartsWith("a", "")); + + /* Equal strings. */ + CHECK(StrStartsWith("a", "a")); + CHECK(StrStartsWith("A", "A")); + + /* Starts with same. */ + CHECK(StrStartsWith("ab", "a")); + CHECK(StrStartsWith("Ab", "A")); + + /* Different cases. */ + CHECK(!StrStartsWith("a", "A")); + CHECK(!StrStartsWith("A", "a")); + CHECK(!StrStartsWith("ab", "A")); + CHECK(!StrStartsWith("Ab", "a")); + + /* Does not start the same. */ + CHECK(!StrStartsWith("", "b")); + CHECK(!StrStartsWith("a", "b")); + CHECK(!StrStartsWith("b", "a")); + CHECK(!StrStartsWith("a", "aa")); +} + +TEST_CASE("StrStartsWith - std::string_view") +{ + /* + * With std::string_view the only way to access the data is via .data(), + * which does not guarantee the termination that would be required by + * things such as stricmp/strcasecmp. So, just passing .data() into stricmp + * or strcasecmp would fail if it does not account for the length of the + * view. Thus, contrary to the string/char* tests, this uses the same base + * string but gets different sections to trigger these corner cases. + */ + std::string_view base{"aabAb"}; + + /* Everything starts with an empty prefix. */ + CHECK(StrStartsWith(base.substr(0, 0), base.substr(1, 0))); // Different positions + CHECK(StrStartsWith(base.substr(0, 1), base.substr(0, 0))); + + /* Equals string. */ + CHECK(StrStartsWith(base.substr(0, 1), base.substr(1, 1))); // Different positions + CHECK(StrStartsWith(base.substr(3, 1), base.substr(3, 1))); + + /* Starts with same. */ + CHECK(StrStartsWith(base.substr(1, 2), base.substr(0, 1))); + CHECK(StrStartsWith(base.substr(3, 2), base.substr(3, 1))); + + /* Different cases. */ + CHECK(!StrStartsWith(base.substr(0, 1), base.substr(3, 1))); + CHECK(!StrStartsWith(base.substr(3, 1), base.substr(0, 1))); + CHECK(!StrStartsWith(base.substr(1, 2), base.substr(3, 1))); + CHECK(!StrStartsWith(base.substr(3, 2), base.substr(0, 1))); + + /* Does not start the same. */ + CHECK(!StrStartsWith(base.substr(2, 0), base.substr(2, 1))); + CHECK(!StrStartsWith(base.substr(0, 1), base.substr(2, 1))); + CHECK(!StrStartsWith(base.substr(2, 1), base.substr(0, 1))); + CHECK(!StrStartsWith(base.substr(0, 1), base.substr(0, 2))); +} + + +TEST_CASE("StrStartsWithIgnoreCase - std::string") +{ + /* Everything starts with an empty prefix. */ + CHECK(StrStartsWithIgnoreCase(std::string{""}, std::string{""})); + CHECK(StrStartsWithIgnoreCase(std::string{"a"}, std::string{""})); + + /* Equals string, ignoring case. */ + CHECK(StrStartsWithIgnoreCase(std::string{"a"}, std::string{"a"})); + CHECK(StrStartsWithIgnoreCase(std::string{"a"}, std::string{"A"})); + CHECK(StrStartsWithIgnoreCase(std::string{"A"}, std::string{"a"})); + CHECK(StrStartsWithIgnoreCase(std::string{"A"}, std::string{"A"})); + + /* Starts with same, ignoring case. */ + CHECK(StrStartsWithIgnoreCase(std::string{"ab"}, std::string{"a"})); + CHECK(StrStartsWithIgnoreCase(std::string{"ab"}, std::string{"A"})); + CHECK(StrStartsWithIgnoreCase(std::string{"Ab"}, std::string{"a"})); + CHECK(StrStartsWithIgnoreCase(std::string{"Ab"}, std::string{"A"})); + + /* Does not start the same. */ + CHECK(!StrStartsWithIgnoreCase(std::string{""}, std::string{"b"})); + CHECK(!StrStartsWithIgnoreCase(std::string{"a"}, std::string{"b"})); + CHECK(!StrStartsWithIgnoreCase(std::string{"b"}, std::string{"a"})); + CHECK(!StrStartsWithIgnoreCase(std::string{"a"}, std::string{"aa"})); +} + +TEST_CASE("StrStartsWithIgnoreCase - char pointer") +{ + /* Everything starts with an empty prefix. */ + CHECK(StrStartsWithIgnoreCase("", "")); + CHECK(StrStartsWithIgnoreCase("a", "")); + + /* Equals string, ignoring case. */ + CHECK(StrStartsWithIgnoreCase("a", "a")); + CHECK(StrStartsWithIgnoreCase("a", "A")); + CHECK(StrStartsWithIgnoreCase("A", "a")); + CHECK(StrStartsWithIgnoreCase("A", "A")); + + /* Starts with same, ignoring case. */ + CHECK(StrStartsWithIgnoreCase("ab", "a")); + CHECK(StrStartsWithIgnoreCase("ab", "A")); + CHECK(StrStartsWithIgnoreCase("Ab", "a")); + CHECK(StrStartsWithIgnoreCase("Ab", "A")); + + /* Does not start the same. */ + CHECK(!StrStartsWithIgnoreCase("", "b")); + CHECK(!StrStartsWithIgnoreCase("a", "b")); + CHECK(!StrStartsWithIgnoreCase("b", "a")); + CHECK(!StrStartsWithIgnoreCase("a", "aa")); +} + +TEST_CASE("StrStartsWithIgnoreCase - std::string_view") +{ + /* + * With std::string_view the only way to access the data is via .data(), + * which does not guarantee the termination that would be required by + * things such as stricmp/strcasecmp. So, just passing .data() into stricmp + * or strcasecmp would fail if it does not account for the length of the + * view. Thus, contrary to the string/char* tests, this uses the same base + * string but gets different sections to trigger these corner cases. + */ + std::string_view base{"aabAb"}; + + /* Everything starts with an empty prefix. */ + CHECK(StrStartsWithIgnoreCase(base.substr(0, 0), base.substr(1, 0))); // Different positions + CHECK(StrStartsWithIgnoreCase(base.substr(0, 1), base.substr(0, 0))); + + /* Equals string, ignoring case. */ + CHECK(StrStartsWithIgnoreCase(base.substr(0, 1), base.substr(1, 1))); // Different positions + CHECK(StrStartsWithIgnoreCase(base.substr(0, 1), base.substr(3, 1))); + CHECK(StrStartsWithIgnoreCase(base.substr(3, 1), base.substr(0, 1))); + CHECK(StrStartsWithIgnoreCase(base.substr(3, 1), base.substr(3, 1))); + + /* Starts with same, ignoring case. */ + CHECK(StrStartsWithIgnoreCase(base.substr(1, 2), base.substr(0, 1))); + CHECK(StrStartsWithIgnoreCase(base.substr(1, 2), base.substr(3, 1))); + CHECK(StrStartsWithIgnoreCase(base.substr(3, 2), base.substr(0, 1))); + CHECK(StrStartsWithIgnoreCase(base.substr(3, 2), base.substr(3, 1))); + + /* Does not start the same. */ + CHECK(!StrStartsWithIgnoreCase(base.substr(2, 0), base.substr(2, 1))); + CHECK(!StrStartsWithIgnoreCase(base.substr(0, 1), base.substr(2, 1))); + CHECK(!StrStartsWithIgnoreCase(base.substr(2, 1), base.substr(0, 1))); + CHECK(!StrStartsWithIgnoreCase(base.substr(0, 1), base.substr(0, 2))); +} + +/**** String ends with *****/ + +TEST_CASE("StrEndsWith - std::string") +{ + /* Everything ends with an empty prefix. */ + CHECK(StrEndsWith(std::string{""}, std::string{""})); + CHECK(StrEndsWith(std::string{"a"}, std::string{""})); + + /* Equal strings. */ + CHECK(StrEndsWith(std::string{"a"}, std::string{"a"})); + CHECK(StrEndsWith(std::string{"A"}, std::string{"A"})); + + /* Ends with same. */ + CHECK(StrEndsWith(std::string{"ba"}, std::string{"a"})); + CHECK(StrEndsWith(std::string{"bA"}, std::string{"A"})); + + /* Different cases. */ + CHECK(!StrEndsWith(std::string{"a"}, std::string{"A"})); + CHECK(!StrEndsWith(std::string{"A"}, std::string{"a"})); + CHECK(!StrEndsWith(std::string{"ba"}, std::string{"A"})); + CHECK(!StrEndsWith(std::string{"bA"}, std::string{"a"})); + + /* Does not end the same. */ + CHECK(!StrEndsWith(std::string{""}, std::string{"b"})); + CHECK(!StrEndsWith(std::string{"a"}, std::string{"b"})); + CHECK(!StrEndsWith(std::string{"b"}, std::string{"a"})); + CHECK(!StrEndsWith(std::string{"a"}, std::string{"aa"})); +} + +TEST_CASE("StrEndsWith - char pointer") +{ + CHECK(StrEndsWith("", "")); + CHECK(StrEndsWith("a", "")); + + /* Equal strings. */ + CHECK(StrEndsWith("a", "a")); + CHECK(StrEndsWith("A", "A")); + + /* Ends with same. */ + CHECK(StrEndsWith("ba", "a")); + CHECK(StrEndsWith("bA", "A")); + + /* Different cases. */ + CHECK(!StrEndsWith("a", "A")); + CHECK(!StrEndsWith("A", "a")); + CHECK(!StrEndsWith("ba", "A")); + CHECK(!StrEndsWith("bA", "a")); + + /* Does not end the same. */ + CHECK(!StrEndsWith("", "b")); + CHECK(!StrEndsWith("a", "b")); + CHECK(!StrEndsWith("b", "a")); + CHECK(!StrEndsWith("a", "aa")); +} + +TEST_CASE("StrEndsWith - std::string_view") +{ + /* + * With std::string_view the only way to access the data is via .data(), + * which does not guarantee the termination that would be required by + * things such as stricmp/strcasecmp. So, just passing .data() into stricmp + * or strcasecmp would fail if it does not account for the length of the + * view. Thus, contrary to the string/char* tests, this uses the same base + * string but gets different sections to trigger these corner cases. + */ + std::string_view base{"aabAba"}; + + /* Everything ends with an empty prefix. */ + CHECK(StrEndsWith(base.substr(0, 0), base.substr(1, 0))); // Different positions + CHECK(StrEndsWith(base.substr(0, 1), base.substr(0, 0))); + + /* Equals string. */ + CHECK(StrEndsWith(base.substr(0, 1), base.substr(1, 1))); // Different positions + CHECK(StrEndsWith(base.substr(3, 1), base.substr(3, 1))); + + /* Ends with same. */ + CHECK(StrEndsWith(base.substr(4, 2), base.substr(0, 1))); + CHECK(StrEndsWith(base.substr(2, 2), base.substr(3, 1))); + + /* Different cases. */ + CHECK(!StrEndsWith(base.substr(0, 1), base.substr(3, 1))); + CHECK(!StrEndsWith(base.substr(3, 1), base.substr(0, 1))); + CHECK(!StrEndsWith(base.substr(4, 2), base.substr(3, 1))); + CHECK(!StrEndsWith(base.substr(2, 2), base.substr(0, 1))); + + /* Does not end the same. */ + CHECK(!StrEndsWith(base.substr(2, 0), base.substr(2, 1))); + CHECK(!StrEndsWith(base.substr(0, 1), base.substr(2, 1))); + CHECK(!StrEndsWith(base.substr(2, 1), base.substr(0, 1))); + CHECK(!StrEndsWith(base.substr(0, 1), base.substr(0, 2))); +} + + +TEST_CASE("StrEndsWithIgnoreCase - std::string") +{ + /* Everything ends with an empty prefix. */ + CHECK(StrEndsWithIgnoreCase(std::string{""}, std::string{""})); + CHECK(StrEndsWithIgnoreCase(std::string{"a"}, std::string{""})); + + /* Equals string, ignoring case. */ + CHECK(StrEndsWithIgnoreCase(std::string{"a"}, std::string{"a"})); + CHECK(StrEndsWithIgnoreCase(std::string{"a"}, std::string{"A"})); + CHECK(StrEndsWithIgnoreCase(std::string{"A"}, std::string{"a"})); + CHECK(StrEndsWithIgnoreCase(std::string{"A"}, std::string{"A"})); + + /* Ends with same, ignoring case. */ + CHECK(StrEndsWithIgnoreCase(std::string{"ba"}, std::string{"a"})); + CHECK(StrEndsWithIgnoreCase(std::string{"ba"}, std::string{"A"})); + CHECK(StrEndsWithIgnoreCase(std::string{"bA"}, std::string{"a"})); + CHECK(StrEndsWithIgnoreCase(std::string{"bA"}, std::string{"A"})); + + /* Does not end the same. */ + CHECK(!StrEndsWithIgnoreCase(std::string{""}, std::string{"b"})); + CHECK(!StrEndsWithIgnoreCase(std::string{"a"}, std::string{"b"})); + CHECK(!StrEndsWithIgnoreCase(std::string{"b"}, std::string{"a"})); + CHECK(!StrEndsWithIgnoreCase(std::string{"a"}, std::string{"aa"})); +} + +TEST_CASE("StrEndsWithIgnoreCase - char pointer") +{ + /* Everything ends with an empty prefix. */ + CHECK(StrEndsWithIgnoreCase("", "")); + CHECK(StrEndsWithIgnoreCase("a", "")); + + /* Equals string, ignoring case. */ + CHECK(StrEndsWithIgnoreCase("a", "a")); + CHECK(StrEndsWithIgnoreCase("a", "A")); + CHECK(StrEndsWithIgnoreCase("A", "a")); + CHECK(StrEndsWithIgnoreCase("A", "A")); + + /* Ends with same, ignoring case. */ + CHECK(StrEndsWithIgnoreCase("ba", "a")); + CHECK(StrEndsWithIgnoreCase("ba", "A")); + CHECK(StrEndsWithIgnoreCase("bA", "a")); + CHECK(StrEndsWithIgnoreCase("bA", "A")); + + /* Does not end the same. */ + CHECK(!StrEndsWithIgnoreCase("", "b")); + CHECK(!StrEndsWithIgnoreCase("a", "b")); + CHECK(!StrEndsWithIgnoreCase("b", "a")); + CHECK(!StrEndsWithIgnoreCase("a", "aa")); +} + +TEST_CASE("StrEndsWithIgnoreCase - std::string_view") +{ + /* + * With std::string_view the only way to access the data is via .data(), + * which does not guarantee the termination that would be required by + * things such as stricmp/strcasecmp. So, just passing .data() into stricmp + * or strcasecmp would fail if it does not account for the length of the + * view. Thus, contrary to the string/char* tests, this uses the same base + * string but gets different sections to trigger these corner cases. + */ + std::string_view base{"aabAba"}; + + /* Everything ends with an empty prefix. */ + CHECK(StrEndsWithIgnoreCase(base.substr(0, 0), base.substr(1, 0))); // Different positions + CHECK(StrEndsWithIgnoreCase(base.substr(0, 1), base.substr(0, 0))); + + /* Equals string, ignoring case. */ + CHECK(StrEndsWithIgnoreCase(base.substr(0, 1), base.substr(1, 1))); // Different positions + CHECK(StrEndsWithIgnoreCase(base.substr(0, 1), base.substr(3, 1))); + CHECK(StrEndsWithIgnoreCase(base.substr(3, 1), base.substr(0, 1))); + CHECK(StrEndsWithIgnoreCase(base.substr(3, 1), base.substr(3, 1))); + + /* Ends with same, ignoring case. */ + CHECK(StrEndsWithIgnoreCase(base.substr(2, 2), base.substr(0, 1))); + CHECK(StrEndsWithIgnoreCase(base.substr(2, 2), base.substr(3, 1))); + CHECK(StrEndsWithIgnoreCase(base.substr(4, 2), base.substr(0, 1))); + CHECK(StrEndsWithIgnoreCase(base.substr(4, 2), base.substr(3, 1))); + + /* Does not end the same. */ + CHECK(!StrEndsWithIgnoreCase(base.substr(2, 0), base.substr(2, 1))); + CHECK(!StrEndsWithIgnoreCase(base.substr(0, 1), base.substr(2, 1))); + CHECK(!StrEndsWithIgnoreCase(base.substr(2, 1), base.substr(0, 1))); + CHECK(!StrEndsWithIgnoreCase(base.substr(0, 1), base.substr(0, 2))); +}