diff --git a/core/sm_stringutil.cpp b/core/sm_stringutil.cpp index 8b6b8363..d68ec552 100644 --- a/core/sm_stringutil.cpp +++ b/core/sm_stringutil.cpp @@ -910,3 +910,165 @@ char *sm_strdup(const char *str) strcpy(ptr, str); return ptr; } + +unsigned int UTIL_ReplaceAll(char *subject, size_t maxlength, const char *search, const char *replace) +{ + size_t searchLen = strlen(search); + size_t replaceLen = strlen(replace); + + char *ptr = subject; + unsigned int total = 0; + while ((ptr = UTIL_ReplaceEx(ptr, maxlength, search, searchLen, replace, replaceLen)) != NULL) + { + total++; + if (*ptr == '\0') + { + break; + } + } + + return total; +} + +/** + * NOTE: Do not edit this for the love of god unless you have + * read the test cases and understand the code behind each one. + * While I don't guarantee there aren't mistakes, I do guarantee + * that plugins will end up relying on tiny idiosyncracies of this + * function, just like they did with AMX Mod X. + * + * There are explicitly more cases than the AMX Mod X version because + * we're not doing a blind copy. Each case is specifically optimized + * for what needs to be done. Even better, we don't have to error on + * bad buffer sizes. Instead, this function will smartly cut off the + * string in a way that pushes old data out. + */ +char *UTIL_ReplaceEx(char *subject, size_t maxLen, const char *search, size_t searchLen, const char *replace, size_t replaceLen) +{ + char *ptr = subject; + size_t browsed = 0; + size_t textLen = strlen(subject); + + /* It's not possible to search or replace */ + if (searchLen > textLen || maxLen <= 1) + { + return NULL; + } + + /* Subtract one off the maxlength so we can include the null terminator */ + maxLen--; + + while (*ptr != '\0' && (browsed <= textLen - searchLen)) + { + /* See if we get a comparison */ + if (strncmp(ptr, search, searchLen) == 0) + { + char *retPtr = NULL; + if (replaceLen > searchLen) + { + /* First, see if we have enough space to do this operation */ + if (maxLen - textLen < replaceLen - searchLen) + { + /* First, see if the replacement length goes out of bounds. */ + if (browsed + replaceLen >= maxLen) + { + /* EXAMPLE CASE: + * Subject: AABBBCCC + * Buffer : 12 bytes + * Search : BBB + * Replace: DDDDDDDDDD + * OUTPUT : AADDDDDDDDD + * POSITION: ^ + */ + /* If it does, we'll just bound the length and do a strcpy. */ + replaceLen = maxLen - browsed; + /* Note, we add one to the final result for the null terminator */ + strncopy(ptr, replace, replaceLen+1); + } else { + /* EXAMPLE CASE: + * Subject: AABBBCCC + * Buffer : 12 bytes + * Search : BBB + * Replace: DDDDDDD + * OUTPUT : AADDDDDDDCC + * POSITION: ^ + */ + /* We're going to have some bytes left over... */ + size_t origBytesToCopy = (textLen - (browsed + searchLen)) + 1; + size_t realBytesToCopy = (maxLen - (browsed + replaceLen)) + 1; + char *moveFrom = ptr + searchLen + (origBytesToCopy - realBytesToCopy); + char *moveTo = ptr + replaceLen; + + /* First, move our old data out of the way. */ + memmove(moveTo, moveFrom, realBytesToCopy); + + /* Now, do our replacement. */ + memcpy(ptr, replace, replaceLen); + } + } else { + /* EXAMPLE CASE: + * Subject: AABBBCCC + * Buffer : 12 bytes + * Search : BBB + * Replace: DDDD + * OUTPUT : AADDDDCCC + * POSITION: ^ + */ + /* Yes, we have enough space. Do a normal move operation. */ + char *moveFrom = ptr + searchLen; + char *moveTo = ptr + replaceLen; + + /* First move our old data out of the way. */ + size_t bytesToCopy = (textLen - (browsed + searchLen)) + 1; + memmove(moveTo, moveFrom, bytesToCopy); + + /* Now do our replacement. */ + memcpy(ptr, replace, replaceLen); + } + } else if (replaceLen < searchLen) { + /* EXAMPLE CASE: + * Subject: AABBBCCC + * Buffer : 12 bytes + * Search : BBB + * Replace: D + * OUTPUT : AADCCC + * POSOTION: ^ + */ + /* If the replacement does not grow the string length, we do not + * need to do any fancy checking at all. Yay! + */ + char *moveFrom = ptr + searchLen; /* Start after the search pointer */ + char *moveTo = ptr + replaceLen; /* Copy to where the replacement ends */ + + /* Copy our replacement in, if any */ + if (replaceLen) + { + memcpy(ptr, replace, replaceLen); + } + + /* Figure out how many bytes to move down, including null terminator */ + size_t bytesToCopy = (textLen - (browsed + searchLen)) + 1; + + /* Move the rest of the string down */ + memmove(moveTo, moveFrom, bytesToCopy); + } else { + /* EXAMPLE CASE: + * Subject: AABBBCCC + * Buffer : 12 bytes + * Search : BBB + * Replace: DDD + * OUTPUT : AADDDCCC + * POSITION: ^ + */ + /* We don't have to move anything around, just do a straight copy */ + memcpy(ptr, replace, replaceLen); + } + + return ptr + replaceLen; + } + ptr++; + browsed++; + } + + return NULL; +} diff --git a/core/sm_stringutil.h b/core/sm_stringutil.h index 77cfbac9..b6498fc0 100644 --- a/core/sm_stringutil.h +++ b/core/sm_stringutil.h @@ -33,5 +33,7 @@ size_t UTIL_Format(char *buffer, size_t maxlength, const char *fmt, ...); size_t UTIL_FormatArgs(char *buffer, size_t maxlength, const char *fmt, va_list ap); char *sm_strdup(const char *str); size_t CorePlayerTranslate(int client, char *buffer, size_t maxlength, const char *phrase, void **params); +unsigned int UTIL_ReplaceAll(char *subject, size_t maxlength, const char *search, const char *replace); +char *UTIL_ReplaceEx(char *subject, size_t maxLen, const char *search, size_t searchLen, const char *replace, size_t replaceLen); #endif // _INCLUDE_SOURCEMOD_STRINGUTIL_H_ diff --git a/core/smn_string.cpp b/core/smn_string.cpp index c85e9666..6b03c308 100644 --- a/core/smn_string.cpp +++ b/core/smn_string.cpp @@ -407,6 +407,43 @@ static cell_t IsCharLower(IPluginContext *pContext, const cell_t *params) return islower(chr); } +static cell_t ReplaceString(IPluginContext *pContext, const cell_t *params) +{ + char *text, *search, *replace; + size_t maxlength; + + pContext->LocalToString(params[1], &text); + pContext->LocalToString(params[3], &search); + pContext->LocalToString(params[4], &replace); + maxlength = (size_t)params[2]; + + + return UTIL_ReplaceAll(text, maxlength, search, replace); +} + +static cell_t ReplaceStringEx(IPluginContext *pContext, const cell_t *params) +{ + char *text, *search, *replace; + size_t maxlength; + + pContext->LocalToString(params[1], &text); + pContext->LocalToString(params[3], &search); + pContext->LocalToString(params[4], &replace); + maxlength = (size_t)params[2]; + + size_t searchLen = (params[5] == -1) ? strlen(search) : (size_t)params[5]; + size_t replaceLen = (params[6] == -1) ? strlen(replace) : (size_t)params[6]; + + char *ptr = UTIL_ReplaceEx(text, maxlength, search, searchLen, replace, replaceLen); + + if (ptr == NULL) + { + return -1; + } + + return ptr - text; +} + static cell_t TrimString(IPluginContext *pContext, const cell_t *params) { char *str; @@ -444,6 +481,10 @@ static cell_t TrimString(IPluginContext *pContext, const cell_t *params) REGISTER_NATIVES(basicStrings) { + {"BreakString", BreakString}, + {"FloatToString", sm_floattostr}, + {"Format", sm_format}, + {"FormatEx", sm_formatex}, {"GetCharBytes", GetCharBytes}, {"IntToString", sm_numtostr}, {"IsCharAlpha", IsCharAlpha}, @@ -452,9 +493,10 @@ REGISTER_NATIVES(basicStrings) {"IsCharNumeric", IsCharNumeric}, {"IsCharSpace", IsCharSpace}, {"IsCharUpper", IsCharUpper}, + {"ReplaceString", ReplaceString}, + {"ReplaceStringEx", ReplaceStringEx}, {"strlen", sm_strlen}, {"StrBreak", BreakString}, /* Backwards compat shim */ - {"BreakString", BreakString}, {"StrContains", sm_contain}, {"strcmp", sm_strcmp}, {"StrCompare", sm_strcmp}, /* Backwards compat shim */ @@ -463,10 +505,7 @@ REGISTER_NATIVES(basicStrings) {"StrCopy", sm_strcopy}, /* Backwards compat shim */ {"StringToInt", sm_strconvint}, {"StringToFloat", sm_strtofloat}, - {"FloatToString", sm_floattostr}, - {"Format", sm_format}, - {"FormatEx", sm_formatex}, - {"VFormat", sm_vformat}, {"TrimString", TrimString}, + {"VFormat", sm_vformat}, {NULL, NULL}, }; diff --git a/plugins/include/string.inc b/plugins/include/string.inc index c405e8ce..dc62e309 100644 --- a/plugins/include/string.inc +++ b/plugins/include/string.inc @@ -216,6 +216,36 @@ stock StrBreak(const String:source[], String:arg[], argLen) return BreakString(source, arg, argLen); } +/** + * Given a string, replaces all occurrences of a search string with a + * replacement string. + * + * @param text String to perform search and replacements on. + * @param maxlength Maximum length of the string buffer. + * @param search String to search for. + * @param replace String to replace the search string with. + * @return Number of replacements that were performed. + */ +native ReplaceString(String:text[], maxlength, const String:search[], const String:replace[]); + +/** + * Given a string, replaces the first occurrence of a search string with a + * replacement string. + * + * @param text String to perform search and replacements on. + * @param maxlength Maximum length of the string buffer. + * @param search String to search for. + * @param replace String to replace the search string with. + * @param searchLen If higher than -1, its value will be used instead of + * a strlen() call on the search parameter. + * @param replaceLen If higher than -1, its value will be used instead of + * a strlen() call on the replace parameter. + * @return Index into the buffer (relative to the start) from where + * the last replacement ended, or -1 if no replacements were + * made. + */ +native ReplaceStringEx(String:text[], maxlength, const String:search[], const String:replace[], searchLen=-1, replaceLen=-1); + /** * Returns the number of bytes a character is using. This is * for multi-byte characters (UTF-8). For normal ASCII characters,