From fd8f3dce3bd9382e7c3b208c18d98f078134b67d Mon Sep 17 00:00:00 2001 From: Carlos Granados Date: Fri, 16 Jan 2026 17:28:49 +0100 Subject: [PATCH] Add functions that work with suffixes and prefixes --- Zend/Optimizer/zend_func_infos.h | 6 + ext/standard/basic_functions.stub.php | 36 ++++ ext/standard/basic_functions_arginfo.h | 40 ++++- ext/standard/string.c | 160 ++++++++++++++++++ .../tests/strings/str_prefix_ensure.phpt | 64 +++++++ .../tests/strings/str_prefix_remove.phpt | 64 +++++++ .../tests/strings/str_prefix_replace.phpt | 66 ++++++++ .../tests/strings/str_suffix_ensure.phpt | 64 +++++++ .../tests/strings/str_suffix_remove.phpt | 64 +++++++ .../tests/strings/str_suffix_replace.phpt | 66 ++++++++ 10 files changed, 629 insertions(+), 1 deletion(-) create mode 100644 ext/standard/tests/strings/str_prefix_ensure.phpt create mode 100644 ext/standard/tests/strings/str_prefix_remove.phpt create mode 100644 ext/standard/tests/strings/str_prefix_replace.phpt create mode 100644 ext/standard/tests/strings/str_suffix_ensure.phpt create mode 100644 ext/standard/tests/strings/str_suffix_remove.phpt create mode 100644 ext/standard/tests/strings/str_suffix_replace.phpt diff --git a/Zend/Optimizer/zend_func_infos.h b/Zend/Optimizer/zend_func_infos.h index b7b118c710c53..89d435a1a6328 100644 --- a/Zend/Optimizer/zend_func_infos.h +++ b/Zend/Optimizer/zend_func_infos.h @@ -481,6 +481,12 @@ static const func_info_t func_infos[] = { F1("stristr", MAY_BE_STRING|MAY_BE_FALSE), F1("strstr", MAY_BE_STRING|MAY_BE_FALSE), F1("strrchr", MAY_BE_STRING|MAY_BE_FALSE), + F1("str_prefix_ensure", MAY_BE_STRING), + F1("str_prefix_remove", MAY_BE_STRING), + F1("str_prefix_replace", MAY_BE_STRING), + F1("str_suffix_ensure", MAY_BE_STRING), + F1("str_suffix_remove", MAY_BE_STRING), + F1("str_suffix_replace", MAY_BE_STRING), F1("chunk_split", MAY_BE_STRING), FN("substr_replace", MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_STRING), F1("quotemeta", MAY_BE_STRING), diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index e27dca069c55b..dbe4f78ad6798 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -2429,6 +2429,42 @@ function str_starts_with(string $haystack, string $needle): bool {} /** @compile-time-eval */ function str_ends_with(string $haystack, string $needle): bool {} +/** + * @compile-time-eval + * @refcount 1 + */ +function str_prefix_ensure(string $prefix, string $subject): string {} + +/** + * @compile-time-eval + * @refcount 1 + */ +function str_prefix_remove(string $prefix, string $subject): string {} + +/** + * @compile-time-eval + * @refcount 1 + */ +function str_prefix_replace(string $prefix, string $replace, string $subject): string {} + +/** + * @compile-time-eval + * @refcount 1 + */ +function str_suffix_ensure(string $suffix, string $subject): string {} + +/** + * @compile-time-eval + * @refcount 1 + */ +function str_suffix_remove(string $suffix, string $subject): string {} + +/** + * @compile-time-eval + * @refcount 1 + */ +function str_suffix_replace(string $suffix, string $replace, string $subject): string {} + /** * @compile-time-eval * @refcount 1 diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index 6f202c01463fd..969f8ec2312e7 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 1a1667a5c59111f096a758d5bb4aa7cf3ec09cfe */ + * Stub hash: ed8b640a6ea3d5fcb1a3c571bfe98c794e6448b4 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0) @@ -928,6 +928,32 @@ ZEND_END_ARG_INFO() #define arginfo_str_ends_with arginfo_str_contains +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_str_prefix_ensure, 0, 2, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, prefix, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, subject, IS_STRING, 0) +ZEND_END_ARG_INFO() + +#define arginfo_str_prefix_remove arginfo_str_prefix_ensure + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_str_prefix_replace, 0, 3, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, prefix, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, replace, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, subject, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_str_suffix_ensure, 0, 2, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, suffix, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, subject, IS_STRING, 0) +ZEND_END_ARG_INFO() + +#define arginfo_str_suffix_remove arginfo_str_suffix_ensure + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_str_suffix_replace, 0, 3, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, suffix, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, replace, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, subject, IS_STRING, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_chunk_split, 0, 1, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, length, IS_LONG, 0, "76") @@ -2560,6 +2586,12 @@ ZEND_FUNCTION(strrchr); ZEND_FUNCTION(str_contains); ZEND_FUNCTION(str_starts_with); ZEND_FUNCTION(str_ends_with); +ZEND_FUNCTION(str_prefix_ensure); +ZEND_FUNCTION(str_prefix_remove); +ZEND_FUNCTION(str_prefix_replace); +ZEND_FUNCTION(str_suffix_ensure); +ZEND_FUNCTION(str_suffix_remove); +ZEND_FUNCTION(str_suffix_replace); ZEND_FUNCTION(chunk_split); ZEND_FUNCTION(substr); ZEND_FUNCTION(substr_replace); @@ -3162,6 +3194,12 @@ static const zend_function_entry ext_functions[] = { ZEND_RAW_FENTRY("str_contains", zif_str_contains, arginfo_str_contains, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_str_contains, NULL) ZEND_RAW_FENTRY("str_starts_with", zif_str_starts_with, arginfo_str_starts_with, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_str_starts_with, NULL) ZEND_RAW_FENTRY("str_ends_with", zif_str_ends_with, arginfo_str_ends_with, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) + ZEND_RAW_FENTRY("str_prefix_ensure", zif_str_prefix_ensure, arginfo_str_prefix_ensure, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) + ZEND_RAW_FENTRY("str_prefix_remove", zif_str_prefix_remove, arginfo_str_prefix_remove, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) + ZEND_RAW_FENTRY("str_prefix_replace", zif_str_prefix_replace, arginfo_str_prefix_replace, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) + ZEND_RAW_FENTRY("str_suffix_ensure", zif_str_suffix_ensure, arginfo_str_suffix_ensure, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) + ZEND_RAW_FENTRY("str_suffix_remove", zif_str_suffix_remove, arginfo_str_suffix_remove, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) + ZEND_RAW_FENTRY("str_suffix_replace", zif_str_suffix_replace, arginfo_str_suffix_replace, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) ZEND_RAW_FENTRY("chunk_split", zif_chunk_split, arginfo_chunk_split, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) ZEND_RAW_FENTRY("substr", zif_substr, arginfo_substr, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_substr, NULL) ZEND_RAW_FENTRY("substr_replace", zif_substr_replace, arginfo_substr_replace, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) diff --git a/ext/standard/string.c b/ext/standard/string.c index 0b7d5be1a2576..d9c79bdfe22b5 100644 --- a/ext/standard/string.c +++ b/ext/standard/string.c @@ -1889,6 +1889,166 @@ PHP_FUNCTION(str_ends_with) } /* }}} */ +/* {{{ Adds prefix to subject string if subject does not already start with prefix */ +PHP_FUNCTION(str_prefix_ensure) +{ + zend_string *subject, *prefix; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STR(prefix) + Z_PARAM_STR(subject) + ZEND_PARSE_PARAMETERS_END(); + + if (ZSTR_LEN(prefix) == 0) { + RETURN_STR_COPY(subject); + } + + if (ZSTR_LEN(prefix) <= ZSTR_LEN(subject) && + memcmp(ZSTR_VAL(subject), ZSTR_VAL(prefix), ZSTR_LEN(prefix)) == 0) { + RETURN_STR_COPY(subject); + } + + zend_string *result = zend_string_alloc(ZSTR_LEN(prefix) + ZSTR_LEN(subject), 0); + memcpy(ZSTR_VAL(result), ZSTR_VAL(prefix), ZSTR_LEN(prefix)); + memcpy(ZSTR_VAL(result) + ZSTR_LEN(prefix), ZSTR_VAL(subject), ZSTR_LEN(subject)); + ZSTR_VAL(result)[ZSTR_LEN(prefix) + ZSTR_LEN(subject)] = '\0'; + RETURN_NEW_STR(result); +} +/* }}} */ + +/* {{{ Removes prefix from subject string if subject starts with prefix */ +PHP_FUNCTION(str_prefix_remove) +{ + zend_string *subject, *prefix; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STR(prefix) + Z_PARAM_STR(subject) + ZEND_PARSE_PARAMETERS_END(); + + if (ZSTR_LEN(prefix) > ZSTR_LEN(subject)) { + RETURN_STR_COPY(subject); + } + + if (memcmp(ZSTR_VAL(subject), ZSTR_VAL(prefix), ZSTR_LEN(prefix)) == 0) { + RETURN_STRINGL(ZSTR_VAL(subject) + ZSTR_LEN(prefix), ZSTR_LEN(subject) - ZSTR_LEN(prefix)); + } + + RETURN_STR_COPY(subject); +} +/* }}} */ + +/* {{{ Replaces prefix in subject string if subject starts with prefix */ +PHP_FUNCTION(str_prefix_replace) +{ + zend_string *subject, *prefix, *replace; + + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_STR(prefix) + Z_PARAM_STR(replace) + Z_PARAM_STR(subject) + ZEND_PARSE_PARAMETERS_END(); + + if (ZSTR_LEN(prefix) > ZSTR_LEN(subject)) { + RETURN_STR_COPY(subject); + } + + if (memcmp(ZSTR_VAL(subject), ZSTR_VAL(prefix), ZSTR_LEN(prefix)) == 0) { + size_t remaining_len = ZSTR_LEN(subject) - ZSTR_LEN(prefix); + zend_string *result = zend_string_alloc(ZSTR_LEN(replace) + remaining_len, 0); + memcpy(ZSTR_VAL(result), ZSTR_VAL(replace), ZSTR_LEN(replace)); + memcpy(ZSTR_VAL(result) + ZSTR_LEN(replace), ZSTR_VAL(subject) + ZSTR_LEN(prefix), remaining_len); + ZSTR_VAL(result)[ZSTR_LEN(replace) + remaining_len] = '\0'; + RETURN_NEW_STR(result); + } + + RETURN_STR_COPY(subject); +} +/* }}} */ + +/* {{{ Adds suffix to subject string if subject does not already end with suffix */ +PHP_FUNCTION(str_suffix_ensure) +{ + zend_string *subject, *suffix; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STR(suffix) + Z_PARAM_STR(subject) + ZEND_PARSE_PARAMETERS_END(); + + if (ZSTR_LEN(suffix) == 0) { + RETURN_STR_COPY(subject); + } + + if (ZSTR_LEN(suffix) <= ZSTR_LEN(subject) && + memcmp( + ZSTR_VAL(subject) + ZSTR_LEN(subject) - ZSTR_LEN(suffix), + ZSTR_VAL(suffix), ZSTR_LEN(suffix)) == 0) { + RETURN_STR_COPY(subject); + } + + zend_string *result = zend_string_alloc(ZSTR_LEN(subject) + ZSTR_LEN(suffix), 0); + memcpy(ZSTR_VAL(result), ZSTR_VAL(subject), ZSTR_LEN(subject)); + memcpy(ZSTR_VAL(result) + ZSTR_LEN(subject), ZSTR_VAL(suffix), ZSTR_LEN(suffix)); + ZSTR_VAL(result)[ZSTR_LEN(subject) + ZSTR_LEN(suffix)] = '\0'; + RETURN_NEW_STR(result); +} +/* }}} */ + +/* {{{ Removes suffix from subject string if subject ends with suffix */ +PHP_FUNCTION(str_suffix_remove) +{ + zend_string *subject, *suffix; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STR(suffix) + Z_PARAM_STR(subject) + ZEND_PARSE_PARAMETERS_END(); + + if (ZSTR_LEN(suffix) > ZSTR_LEN(subject)) { + RETURN_STR_COPY(subject); + } + + if (memcmp( + ZSTR_VAL(subject) + ZSTR_LEN(subject) - ZSTR_LEN(suffix), + ZSTR_VAL(suffix), ZSTR_LEN(suffix)) == 0) { + RETURN_STRINGL(ZSTR_VAL(subject), ZSTR_LEN(subject) - ZSTR_LEN(suffix)); + } + + RETURN_STR_COPY(subject); +} +/* }}} */ + +/* {{{ Replaces suffix in subject string if subject ends with suffix */ +PHP_FUNCTION(str_suffix_replace) +{ + zend_string *subject, *suffix, *replace; + + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_STR(suffix) + Z_PARAM_STR(replace) + Z_PARAM_STR(subject) + ZEND_PARSE_PARAMETERS_END(); + + if (ZSTR_LEN(suffix) > ZSTR_LEN(subject)) { + RETURN_STR_COPY(subject); + } + + if (memcmp( + ZSTR_VAL(subject) + ZSTR_LEN(subject) - ZSTR_LEN(suffix), + ZSTR_VAL(suffix), ZSTR_LEN(suffix)) == 0) { + size_t base_len = ZSTR_LEN(subject) - ZSTR_LEN(suffix); + zend_string *result = zend_string_alloc(base_len + ZSTR_LEN(replace), 0); + memcpy(ZSTR_VAL(result), ZSTR_VAL(subject), base_len); + memcpy(ZSTR_VAL(result) + base_len, ZSTR_VAL(replace), ZSTR_LEN(replace)); + ZSTR_VAL(result)[base_len + ZSTR_LEN(replace)] = '\0'; + RETURN_NEW_STR(result); + } + + RETURN_STR_COPY(subject); +} +/* }}} */ + static inline void _zend_strpos(zval *return_value, zend_string *haystack, zend_string *needle, zend_long offset) { const char *found = NULL; diff --git a/ext/standard/tests/strings/str_prefix_ensure.phpt b/ext/standard/tests/strings/str_prefix_ensure.phpt new file mode 100644 index 0000000000000..5492ff7176dcc --- /dev/null +++ b/ext/standard/tests/strings/str_prefix_ensure.phpt @@ -0,0 +1,64 @@ +--TEST-- +Test str_prefix_ensure() function +--FILE-- + +--EXPECTF-- +*** Testing str_prefix_ensure() : various strings *** +string(21) "TheBeginningMiddleEnd" +string(18) "BeginningMiddleEnd" +string(18) "BeginningMiddleEnd" +string(6) "prefix" +string(0) "" +string(1) " " +string(4) "abca" +string(4) "test" +string(19) "%0BeginningMiddleEnd" +string(1) "%0" +string(1) "%0" +string(2) "%0a" +string(2) "%0a" +string(7) "%0abc%0ab" +string(5) "c%0%0ab" +string(6) "a%0da%0b" +string(6) "a%0za%0b" diff --git a/ext/standard/tests/strings/str_prefix_remove.phpt b/ext/standard/tests/strings/str_prefix_remove.phpt new file mode 100644 index 0000000000000..3a8c3f5fd6f17 --- /dev/null +++ b/ext/standard/tests/strings/str_prefix_remove.phpt @@ -0,0 +1,64 @@ +--TEST-- +Test str_prefix_remove() function +--FILE-- + +--EXPECTF-- +*** Testing str_prefix_remove() : various strings *** +string(9) "MiddleEnd" +string(18) "BeginningMiddleEnd" +string(18) "BeginningMiddleEnd" +string(0) "" +string(18) "BeginningMiddleEnd" +string(18) "BeginningMiddleEnd" +string(0) "" +string(0) "" +string(18) "BeginningMiddleEnd" +string(1) "%0" +string(0) "" +string(1) "a" +string(1) "a" +string(3) "b%0a" +string(3) "b%0a" +string(1) "a" +string(1) "a" diff --git a/ext/standard/tests/strings/str_prefix_replace.phpt b/ext/standard/tests/strings/str_prefix_replace.phpt new file mode 100644 index 0000000000000..69030df3839ed --- /dev/null +++ b/ext/standard/tests/strings/str_prefix_replace.phpt @@ -0,0 +1,66 @@ +--TEST-- +Test str_prefix_replace() function +--FILE-- + +--EXPECTF-- +*** Testing str_prefix_replace() : various strings *** +string(14) "StartMiddleEnd" +string(18) "BeginningMiddleEnd" +string(9) "MiddleEnd" +string(21) "NewBeginningMiddleEnd" +string(19) "https://example.com" +string(8) "/opt/bin" +string(2) "ab" +string(8) "replaced" +string(0) "" +string(18) "BeginningMiddleEnd" +string(2) "ba" +string(2) "%0a" +string(3) "dab" +string(4) "%0dab" +string(3) "b%0a" +string(5) "newst" diff --git a/ext/standard/tests/strings/str_suffix_ensure.phpt b/ext/standard/tests/strings/str_suffix_ensure.phpt new file mode 100644 index 0000000000000..c1376e5cc7b68 --- /dev/null +++ b/ext/standard/tests/strings/str_suffix_ensure.phpt @@ -0,0 +1,64 @@ +--TEST-- +Test str_suffix_ensure() function +--FILE-- + +--EXPECTF-- +*** Testing str_suffix_ensure() : various strings *** +string(21) "BeginningMiddleEnding" +string(18) "BeginningMiddleEnd" +string(18) "BeginningMiddleEnd" +string(6) "suffix" +string(0) "" +string(1) " " +string(4) "aabc" +string(4) "test" +string(19) "BeginningMiddleEnd%0" +string(1) "%0" +string(1) "%0" +string(2) "a%0" +string(2) "a%0" +string(4) "ab%0c" +string(5) "ab%0%0c" +string(6) "b%0ad%0a" +string(6) "b%0az%0a" diff --git a/ext/standard/tests/strings/str_suffix_remove.phpt b/ext/standard/tests/strings/str_suffix_remove.phpt new file mode 100644 index 0000000000000..6178e0d3581bf --- /dev/null +++ b/ext/standard/tests/strings/str_suffix_remove.phpt @@ -0,0 +1,64 @@ +--TEST-- +Test str_suffix_remove() function +--FILE-- + +--EXPECTF-- +*** Testing str_suffix_remove() : various strings *** +string(15) "beginningMiddle" +string(18) "beginningMiddleEnd" +string(18) "beginningMiddleEnd" +string(0) "" +string(18) "beginningMiddleEnd" +string(18) "beginningMiddleEnd" +string(0) "" +string(0) "" +string(18) "beginningMiddleEnd" +string(1) "%0" +string(0) "" +string(1) "a" +string(1) "a" +string(3) "a%0b" +string(3) "a%0b" +string(1) "a" +string(1) "a" diff --git a/ext/standard/tests/strings/str_suffix_replace.phpt b/ext/standard/tests/strings/str_suffix_replace.phpt new file mode 100644 index 0000000000000..6ec6e93b0ee45 --- /dev/null +++ b/ext/standard/tests/strings/str_suffix_replace.phpt @@ -0,0 +1,66 @@ +--TEST-- +Test str_suffix_replace() function +--FILE-- + +--EXPECTF-- +*** Testing str_suffix_replace() : various strings *** +string(21) "BeginningMiddleFinish" +string(18) "BeginningMiddleEnd" +string(15) "BeginningMiddle" +string(21) "BeginningMiddleEndNew" +string(15) "file.backup.txt" +string(11) "document.md" +string(2) "ab" +string(8) "replaced" +string(0) "" +string(18) "BeginningMiddleEnd" +string(2) "ab" +string(2) "a%0" +string(3) "abd" +string(4) "ab%0d" +string(3) "a%0b" +string(5) "tenew"