From 7631cc3481d66ad5def2b4314b67aff62ffabc32 Mon Sep 17 00:00:00 2001 From: Vibe Nuage Agent Date: Fri, 11 Sep 2026 21:40:25 +0000 Subject: [PATCH] refactor: merge av_value_to_string and av_replace_placeholders into av_error_messages Consolidate the three error-message translation units into a single av_error_messages.c. To keep the merged TU linkable in the Ceedling unit-test build (which has no Zend engine), route every direct Zend call through av_wrappers: zend_hash_find/next_index_insert/add, zend_new_array, zend_lookup_class_ex, zend_string_init (via a new av_zval_stringl), snprintf (av_snprintf) and fmax (av_fmax). Drop the hand-written build_union_type_string stub in test_error_messages.c; the tests now link the real implementation and build zend_type values by mask, exercising the genuine type-to-article logic. Add ap_php_snprintf/ap_php_vsnprintf stubs to the test helpers so test files can keep using snprintf() after php.h redefines it. Co-authored-by: matapatos --- config.m4 | 2 +- src/helpers/av_error_messages.c | 257 +++++++++++++++++++++----- src/helpers/av_replace_placeholders.c | 110 ----------- src/helpers/av_value_to_string.c | 65 ------- src/helpers/av_wrappers.c | 48 +++++ src/helpers/av_wrappers.h | 13 ++ tests/c/support/test_helpers.c | 18 ++ tests/c/support/test_helpers.h | 7 + tests/c/test_error_messages.c | 48 ++--- tests/c/test_value_to_string.c | 2 +- 10 files changed, 311 insertions(+), 259 deletions(-) delete mode 100644 src/helpers/av_replace_placeholders.c delete mode 100644 src/helpers/av_value_to_string.c diff --git a/config.m4 b/config.m4 index 9495a98..0f7003e 100644 --- a/config.m4 +++ b/config.m4 @@ -10,5 +10,5 @@ if test "$PHP_ATTRIBUTES_VALIDATION" != "no"; then fi PHP_ADD_LIBRARY(stdc++, 1, ATTRIBUTES_VALIDATION_SHARED_LIBADD) - PHP_NEW_EXTENSION(attributes_validation, attributes_validation.c src/av_validate_function.c src/av_call_function.c src/av_base_model.c src/av_exception.c src/av_model_configs.c src/helpers/av_wrappers.c src/helpers/av_options.c src/helpers/av_string.c src/helpers/av_error_messages.c src/helpers/av_replace_placeholders.c src/helpers/av_value_to_string.c src/fields/av_field.c src/fields/av_alias.c src/validators/av_typehint_validator.c, $ext_shared) + PHP_NEW_EXTENSION(attributes_validation, attributes_validation.c src/av_validate_function.c src/av_call_function.c src/av_base_model.c src/av_exception.c src/av_model_configs.c src/helpers/av_wrappers.c src/helpers/av_options.c src/helpers/av_string.c src/helpers/av_error_messages.c src/fields/av_field.c src/fields/av_alias.c src/validators/av_typehint_validator.c, $ext_shared) fi diff --git a/src/helpers/av_error_messages.c b/src/helpers/av_error_messages.c index fcb5893..2f78af9 100644 --- a/src/helpers/av_error_messages.c +++ b/src/helpers/av_error_messages.c @@ -1,15 +1,11 @@ #include "av_error_messages.h" -#include "av_replace_placeholders.h" -#include "../av_base_model.h" +#include "av_wrappers.h" #include "Zend/zend_API.h" #include "Zend/zend_exceptions.h" #include "Zend/zend_interfaces.h" #include "Zend/zend_operators.h" -#include "php.h" -#include "zend.h" -#include "zend_portability.h" -#include "zend_string.h" -#include "zend_types.h" +#include "Zend/zend_string.h" +#include "Zend/zend_types.h" #include #include @@ -21,21 +17,21 @@ static const char *av_error_type_messages[] = { static zend_always_inline void add_field_error_to_array(zval *errors_array, const char *error_message, size_t length) { zval error_msg; - ZVAL_STRINGL(&error_msg, error_message, length); - zend_hash_next_index_insert(Z_ARRVAL_P(errors_array), &error_msg); + av_zval_stringl(&error_msg, error_message, length); + av_hash_next_index_insert(Z_ARRVAL_P(errors_array), &error_msg); } static zend_always_inline void add_field_error(zval *errors, zend_string *field_name, const char *error_message, size_t length) { - zval *existing = zend_hash_find(Z_ARRVAL_P(errors), field_name); + zval *existing = av_hash_find(Z_ARRVAL_P(errors), field_name); if (existing && Z_TYPE_P(existing) == IS_ARRAY) { add_field_error_to_array(existing, error_message, length); } else { zval error_array; - array_init(&error_array); + ZVAL_ARR(&error_array, av_new_array(0)); add_field_error_to_array(&error_array, error_message, length); - zend_hash_add(Z_ARRVAL_P(errors), field_name, &error_array); + av_hash_add(Z_ARRVAL_P(errors), field_name, &error_array); } } @@ -54,45 +50,45 @@ static bool av_vowel_sound(char c) static zend_string *generate_type_name(const zend_type *type) { if (ZEND_TYPE_IS_INTERSECTION(*type)) { - return zend_string_init("mixed", 5, 0); + return av_string_init("mixed", 5, 0); } if (ZEND_TYPE_HAS_NAME(*type)) { - return zend_string_copy(ZEND_TYPE_NAME(*type)); + return av_string_copy(ZEND_TYPE_NAME(*type)); } uint32_t type_mask = ZEND_TYPE_PURE_MASK(*type); if (type_mask == MAY_BE_BOOL) - return zend_string_init("boolean", 7, 0); + return av_string_init("boolean", 7, 0); if (type_mask == MAY_BE_LONG) - return zend_string_init("integer", 7, 0); + return av_string_init("integer", 7, 0); if (type_mask == MAY_BE_DOUBLE) - return zend_string_init("float", 5, 0); + return av_string_init("float", 5, 0); if (type_mask == MAY_BE_STRING) - return zend_string_init("string", 6, 0); + return av_string_init("string", 6, 0); if (type_mask == MAY_BE_ARRAY) - return zend_string_init("array", 5, 0); + return av_string_init("array", 5, 0); if (type_mask == MAY_BE_OBJECT) - return zend_string_init("object", 6, 0); + return av_string_init("object", 6, 0); if (type_mask == MAY_BE_RESOURCE) - return zend_string_init("resource", 8, 0); + return av_string_init("resource", 8, 0); if (type_mask == MAY_BE_NULL) - return zend_string_init("null", 4, 0); + return av_string_init("null", 4, 0); if (type_mask == MAY_BE_CALLABLE) - return zend_string_init("callable", 8, 0); + return av_string_init("callable", 8, 0); if (type_mask == MAY_BE_VOID) - return zend_string_init("void", 4, 0); + return av_string_init("void", 4, 0); - return zend_string_init("mixed", 5, 0); + return av_string_init("mixed", 5, 0); } static zend_always_inline zend_string *build_single_type_with_article(const zend_type *type) { zend_string *type_name = generate_type_name(type); const char *article = av_vowel_sound(ZSTR_VAL(type_name)[0]) ? "an" : "a"; - zend_string *result = zend_string_concat3(article, strlen(article), " ", 1, ZSTR_VAL(type_name), ZSTR_LEN(type_name)); - zend_string_release(type_name); + zend_string *result = av_string_concat3(article, strlen(article), " ", 1, ZSTR_VAL(type_name), ZSTR_LEN(type_name)); + av_string_release(type_name); return result; } @@ -125,11 +121,11 @@ static zend_always_inline zend_string *build_union_only_basic_types(uint32_t pur ZEND_ASSERT(count.total >= 2); ZEND_ASSERT(count.total <= type_mappings_size); - size_t num_commas = fmax(count.total - 2, 0) * (sizeof(", ") - 1); // Comma + space after comma + size_t num_commas = av_fmax(count.total - 2, 0) * (sizeof(", ") - 1); // Comma + space after comma size_t num_ors = sizeof(" or ") - 1; count.max_string_size += num_commas + num_ors; - zend_string *result = zend_string_alloc(count.max_string_size, 0); + zend_string *result = av_string_alloc(count.max_string_size, 0); char *output = ZSTR_VAL(result); size_t output_pos = 0; int i = 0; @@ -170,7 +166,7 @@ static zend_always_inline zend_string *build_union_only_basic_types(uint32_t pur output[output_pos] = '\0'; ZEND_ASSERT(output_pos == count.max_string_size); - return zend_string_truncate(result, output_pos, 0); + return av_string_truncate(result, output_pos, 0); } zend_string *build_union_type_string(zend_type property_type) @@ -194,11 +190,11 @@ zend_string *build_union_type_string(zend_type property_type) if (!result) { result = type_part; } else { - zend_string *prefix = zend_string_init(" or ", 4, 0); - zend_string *temp = zend_string_concat3(ZSTR_VAL(result), ZSTR_LEN(result), ZSTR_VAL(prefix), ZSTR_LEN(prefix), ZSTR_VAL(type_part), ZSTR_LEN(type_part)); - zend_string_release(result); - zend_string_release(prefix); - zend_string_release(type_part); + zend_string *prefix = av_string_init(" or ", 4, 0); + zend_string *temp = av_string_concat3(ZSTR_VAL(result), ZSTR_LEN(result), ZSTR_VAL(prefix), ZSTR_LEN(prefix), ZSTR_VAL(type_part), ZSTR_LEN(type_part)); + av_string_release(result); + av_string_release(prefix); + av_string_release(type_part); result = temp; } } @@ -213,7 +209,7 @@ static bool is_type_enum(const zend_type *type) return false; } - zend_class_entry *ce = zend_lookup_class_ex(ZEND_TYPE_NAME(*type), NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD); + zend_class_entry *ce = av_lookup_class_ex(ZEND_TYPE_NAME(*type), NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD); if (!ce) { return false; } @@ -225,16 +221,16 @@ static zend_string *generate_error_message(av_field *field, zend_type property_t { zend_string *full_path = field->name; if (field->parent && ZSTR_LEN(field->parent) > 0) { - full_path = zend_string_concat3(ZSTR_VAL(field->parent), ZSTR_LEN(field->parent), ".", 1, ZSTR_VAL(field->name), ZSTR_LEN(field->name)); + full_path = av_string_concat3(ZSTR_VAL(field->parent), ZSTR_LEN(field->parent), ".", 1, ZSTR_VAL(field->name), ZSTR_LEN(field->name)); } const zend_type *type; ZEND_TYPE_FOREACH(property_type, type) { if (ZEND_TYPE_HAS_NAME(*type) && is_type_enum(type)) { - zend_string *msg = zend_string_concat3("The selected ", sizeof("The selected ") - 1, ZSTR_VAL(full_path), ZSTR_LEN(full_path), " is invalid.", sizeof(" is invalid.") - 1); + zend_string *msg = av_string_concat3("The selected ", sizeof("The selected ") - 1, ZSTR_VAL(full_path), ZSTR_LEN(full_path), " is invalid.", sizeof(" is invalid.") - 1); if (full_path != field->name) - zend_string_release(full_path); + av_string_release(full_path); return msg; } } @@ -242,17 +238,17 @@ static zend_string *generate_error_message(av_field *field, zend_type property_t zend_string *type_string = build_union_type_string(property_type); if (!type_string) { - type_string = zend_string_init("mixed", sizeof("mixed") - 1, 0); + type_string = av_string_init("mixed", sizeof("mixed") - 1, 0); } size_t message_len = sizeof("The ") - 1 + ZSTR_LEN(field->name) + sizeof(" must be ") - 1 + ZSTR_LEN(type_string) + sizeof(".") - 1; - zend_string *message = zend_string_alloc(message_len, 0); + zend_string *message = av_string_alloc(message_len, 0); - snprintf(ZSTR_VAL(message), message_len + 1, "The %s must be %s.", ZSTR_VAL(field->name), ZSTR_VAL(type_string)); + av_snprintf(ZSTR_VAL(message), message_len + 1, "The %s must be %s.", ZSTR_VAL(field->name), ZSTR_VAL(type_string)); - zend_string_release(type_string); + av_string_release(type_string); if (full_path != field->name) - zend_string_release(full_path); + av_string_release(full_path); return message; } @@ -264,12 +260,177 @@ void av_add_field_error_with_prefix(av_error_type type, av_field *field, av_prop if (!field->parent || ZSTR_LEN(field->parent) == 0) { add_field_error(errors, field->name, ZSTR_VAL(replaced_message), ZSTR_LEN(replaced_message)); - zend_string_release(replaced_message); + av_string_release(replaced_message); return; } - zend_string *full_path = zend_string_concat3(ZSTR_VAL(field->parent), ZSTR_LEN(field->parent), ".", 1, ZSTR_VAL(field->name), ZSTR_LEN(field->name)); + zend_string *full_path = av_string_concat3(ZSTR_VAL(field->parent), ZSTR_LEN(field->parent), ".", 1, ZSTR_VAL(field->name), ZSTR_LEN(field->name)); add_field_error(errors, full_path, ZSTR_VAL(replaced_message), ZSTR_LEN(replaced_message)); - zend_string_release(full_path); - zend_string_release(replaced_message); + av_string_release(full_path); + av_string_release(replaced_message); +} + +/* + * Converts any PHP zval into a zend_string suitable for inclusion in an + * error message template (the {value} placeholder). + * + * The conversion is fully type-aware so error messages stay readable: + * null -> "null" + * bool -> "true" / "false" + * int -> numeric string + * double -> numeric string + * string -> single-quoted value + * array -> "array" + * object -> __toString() result when Stringable, else the class name + * resource -> resource type name, or "resource" + * + * All Zend internals are reached through the mockable av_wrappers so the + * function can be unit tested in isolation. + */ +zend_string *av_value_to_string(zval *value) +{ + if (value == NULL) { + return av_string_init("null", sizeof("null") - 1, 0); + } + + ZVAL_DEREF(value); + + switch (Z_TYPE_P(value)) { + case IS_NULL: + return av_string_init("null", sizeof("null") - 1, 0); + case IS_TRUE: + return av_string_init("true", sizeof("true") - 1, 0); + case IS_FALSE: + return av_string_init("false", sizeof("false") - 1, 0); + case IS_LONG: + return av_long_to_str(Z_LVAL_P(value)); + case IS_DOUBLE: + return av_double_to_str(Z_DVAL_P(value)); + case IS_STRING: + return av_string_concat3("'", 1, Z_STRVAL_P(value), Z_STRLEN_P(value), "'", 1); + case IS_ARRAY: + return av_string_init("array", sizeof("array") - 1, 0); + case IS_OBJECT: + if (av_is_stringable(Z_OBJCE_P(value))) { + zval result; + if (av_call_tostring(Z_OBJ_P(value), &result) == SUCCESS) { + zend_string *str = av_string_concat3("'", 1, Z_STRVAL(result), Z_STRLEN(result), "'", 1); + av_zval_ptr_dtor(&result); + return str; + } + } + return av_string_copy(Z_OBJCE_P(value)->name); + case IS_RESOURCE: { + const char *type_name = av_rsrc_list_get_rsrc_type(Z_RES_P(value)); + const char *fallback = "resource"; + return av_string_init(type_name ? type_name : fallback, strlen(type_name ? type_name : fallback), 0); + } + default: + return av_string_init("(unknown)", sizeof("(unknown)") - 1, 0); + } +} + +/* + * Substitutes the {field}, {value} and {expected} placeholders of an error + * message template: + * + * {field} -> field->name + * {value} -> av_value_to_string(field->value) + * {expected} -> build_union_type_string(prop_info->property->type) + * + * Each placeholder may occur multiple times. The result is a freshly + * allocated zend_string that the caller must release with av_string_release. + * + * All Zend internals are reached through the mockable av_wrappers so the + * function can be unit tested in isolation. + */ +zend_string *av_replace_placeholders(const char *template, size_t length, av_field *field, av_property_info *prop_info) +{ + struct { + const char *search; + size_t len; + size_t counts; + zend_string *replace; + } table[] = {{"{field}", sizeof("{field}") - 1, 0, field->name}, {"{value}", sizeof("{value}") - 1, 0, NULL}, {"{expected}", sizeof("{expected}") - 1, 0, NULL}}; + size_t table_size = sizeof(table) / sizeof(table[0]); + + size_t max_template_size = length; + size_t total_placeholders = 0; + for (size_t i = 0; i < table_size; i++) { + const char *search_pos = template; + while ((search_pos = av_memnstr(search_pos, table[i].search, table[i].len, template + length))) { + table[i].counts += 1; + search_pos += table[i].len; + } + + if (table[i].counts == 0) + continue; + + total_placeholders += table[i].counts; + + if (table[i].replace == NULL) { + if (i == 1) { // {value} + table[i].replace = av_value_to_string(field->value); + } else if (i == 2) { // {expected} + table[i].replace = build_union_type_string(prop_info->property->type); + } + } + max_template_size += table[i].counts * (ZSTR_LEN(table[i].replace) - table[i].len); + } + + if (total_placeholders == 0) { + return av_string_init(template, length, 0); + } + + // Allocate and process in a single pass + zend_string *result = av_string_alloc(max_template_size, 0); + char *output = ZSTR_VAL(result); + size_t output_pos = 0; + + const char *input = template; + const char *input_end = template + length; + + while (input < input_end) { + if (total_placeholders == 0) { + output[output_pos++] = *input++; + continue; + } + + bool replaced = false; + for (size_t i = 0; i < table_size; i++) { + if (table[i].counts == 0) + continue; + if (input + table[i].len > input_end) + continue; + if (memcmp(input, table[i].search, table[i].len) != 0) + continue; + + table[i].counts -= 1; + total_placeholders -= 1; + + memcpy(output + output_pos, ZSTR_VAL(table[i].replace), ZSTR_LEN(table[i].replace)); + output_pos += ZSTR_LEN(table[i].replace); + input += table[i].len; + replaced = true; + break; + } + + if (!replaced) { + output[output_pos++] = *input++; + } + } + + ZEND_ASSERT(output_pos == max_template_size); + + // Null-terminate and truncate to actual size + output[output_pos] = '\0'; + result = av_string_truncate(result, output_pos, 0); + + for (size_t i = 1; i < table_size; i++) { + if (table[i].replace != NULL) { + av_string_release(table[i].replace); + } + } + + return result; } diff --git a/src/helpers/av_replace_placeholders.c b/src/helpers/av_replace_placeholders.c deleted file mode 100644 index e55a3ce..0000000 --- a/src/helpers/av_replace_placeholders.c +++ /dev/null @@ -1,110 +0,0 @@ -#include "av_replace_placeholders.h" -#include "av_error_messages.h" -#include "av_value_to_string.h" -#include "av_wrappers.h" -#include - -/* - * Substitutes the {field}, {value} and {expected} placeholders of an error - * message template: - * - * {field} -> field->name - * {value} -> av_value_to_string(field->value) - * {expected} -> build_union_type_string(prop_info->property->type) - * - * Each placeholder may occur multiple times. The result is a freshly - * allocated zend_string that the caller must release with av_string_release. - * - * All Zend internals are reached through the mockable av_wrappers so the - * function can be unit tested in isolation. - */ -zend_string *av_replace_placeholders(const char *template, size_t length, av_field *field, av_property_info *prop_info) -{ - struct { - const char *search; - size_t len; - size_t counts; - zend_string *replace; - } table[] = {{"{field}", sizeof("{field}") - 1, 0, field->name}, {"{value}", sizeof("{value}") - 1, 0, NULL}, {"{expected}", sizeof("{expected}") - 1, 0, NULL}}; - size_t table_size = sizeof(table) / sizeof(table[0]); - - size_t max_template_size = length; - size_t total_placeholders = 0; - for (size_t i = 0; i < table_size; i++) { - const char *search_pos = template; - while ((search_pos = av_memnstr(search_pos, table[i].search, table[i].len, template + length))) { - table[i].counts += 1; - search_pos += table[i].len; - } - - if (table[i].counts == 0) - continue; - - total_placeholders += table[i].counts; - - if (table[i].replace == NULL) { - if (i == 1) { // {value} - table[i].replace = av_value_to_string(field->value); - } else if (i == 2) { // {expected} - table[i].replace = build_union_type_string(prop_info->property->type); - } - } - max_template_size += table[i].counts * (ZSTR_LEN(table[i].replace) - table[i].len); - } - - if (total_placeholders == 0) { - return av_string_init(template, length, 0); - } - - // Allocate and process in a single pass - zend_string *result = av_string_alloc(max_template_size, 0); - char *output = ZSTR_VAL(result); - size_t output_pos = 0; - - const char *input = template; - const char *input_end = template + length; - - while (input < input_end) { - if (total_placeholders == 0) { - output[output_pos++] = *input++; - continue; - } - - bool replaced = false; - for (size_t i = 0; i < table_size; i++) { - if (table[i].counts == 0) - continue; - if (input + table[i].len > input_end) - continue; - if (memcmp(input, table[i].search, table[i].len) != 0) - continue; - - table[i].counts -= 1; - total_placeholders -= 1; - - memcpy(output + output_pos, ZSTR_VAL(table[i].replace), ZSTR_LEN(table[i].replace)); - output_pos += ZSTR_LEN(table[i].replace); - input += table[i].len; - replaced = true; - break; - } - - if (!replaced) { - output[output_pos++] = *input++; - } - } - - ZEND_ASSERT(output_pos == max_template_size); - - // Null-terminate and truncate to actual size - output[output_pos] = '\0'; - result = av_string_truncate(result, output_pos, 0); - - for (size_t i = 1; i < table_size; i++) { - if (table[i].replace != NULL) { - av_string_release(table[i].replace); - } - } - - return result; -} diff --git a/src/helpers/av_value_to_string.c b/src/helpers/av_value_to_string.c deleted file mode 100644 index 1191966..0000000 --- a/src/helpers/av_value_to_string.c +++ /dev/null @@ -1,65 +0,0 @@ -#include "av_value_to_string.h" -#include "av_wrappers.h" -#include "Zend/zend_API.h" -#include "Zend/zend_operators.h" -#include - -/* - * Converts any PHP zval into a zend_string suitable for inclusion in an - * error message template (the {value} placeholder). - * - * The conversion is fully type-aware so error messages stay readable: - * null -> "null" - * bool -> "true" / "false" - * int -> numeric string - * double -> numeric string - * string -> single-quoted value - * array -> "array" - * object -> __toString() result when Stringable, else the class name - * resource -> resource type name, or "resource" - * - * All Zend internals are reached through the mockable av_wrappers so the - * function can be unit tested in isolation. - */ -zend_string *av_value_to_string(zval *value) -{ - if (value == NULL) { - return av_string_init("null", sizeof("null") - 1, 0); - } - - ZVAL_DEREF(value); - - switch (Z_TYPE_P(value)) { - case IS_NULL: - return av_string_init("null", sizeof("null") - 1, 0); - case IS_TRUE: - return av_string_init("true", sizeof("true") - 1, 0); - case IS_FALSE: - return av_string_init("false", sizeof("false") - 1, 0); - case IS_LONG: - return av_long_to_str(Z_LVAL_P(value)); - case IS_DOUBLE: - return av_double_to_str(Z_DVAL_P(value)); - case IS_STRING: - return av_string_concat3("'", 1, Z_STRVAL_P(value), Z_STRLEN_P(value), "'", 1); - case IS_ARRAY: - return av_string_init("array", sizeof("array") - 1, 0); - case IS_OBJECT: - if (av_is_stringable(Z_OBJCE_P(value))) { - zval result; - if (av_call_tostring(Z_OBJ_P(value), &result) == SUCCESS) { - zend_string *str = av_string_concat3("'", 1, Z_STRVAL(result), Z_STRLEN(result), "'", 1); - av_zval_ptr_dtor(&result); - return str; - } - } - return av_string_copy(Z_OBJCE_P(value)->name); - case IS_RESOURCE: { - const char *type_name = av_rsrc_list_get_rsrc_type(Z_RES_P(value)); - const char *fallback = "resource"; - return av_string_init(type_name ? type_name : fallback, strlen(type_name ? type_name : fallback), 0); - } - default: - return av_string_init("(unknown)", sizeof("(unknown)") - 1, 0); - } -} diff --git a/src/helpers/av_wrappers.c b/src/helpers/av_wrappers.c index 255f104..f7b9d63 100644 --- a/src/helpers/av_wrappers.c +++ b/src/helpers/av_wrappers.c @@ -1,10 +1,14 @@ #include "av_wrappers.h" #include "php.h" #include "Zend/zend_API.h" +#include "Zend/zend_compile.h" +#include "Zend/zend_hash.h" #include "Zend/zend_interfaces.h" #include "Zend/zend_list.h" #include "Zend/zend_operators.h" #include "Zend/zend_variables.h" +#include +#include /* * Wrapper implementations for Zend internals. @@ -100,3 +104,47 @@ const char *av_memnstr(const char *haystack, const char *needle, size_t needle_l { return php_memnstr(haystack, needle, needle_len, end); } + +zval *av_hash_find(const HashTable *ht, zend_string *key) +{ + return zend_hash_find(ht, key); +} + +zval *av_hash_next_index_insert(HashTable *ht, zval *pData) +{ + return zend_hash_next_index_insert(ht, pData); +} + +zval *av_hash_add(HashTable *ht, zend_string *key, zval *pData) +{ + return zend_hash_add(ht, key, pData); +} + +HashTable *av_new_array(uint32_t size) +{ + return zend_new_array(size); +} + +void av_zval_stringl(zval *z, const char *str, size_t len) +{ + ZVAL_NEW_STR(z, av_string_init(str, len, 0)); +} + +zend_class_entry *av_lookup_class_ex(zend_string *name, zend_string *lcname, uint32_t flags) +{ + return zend_lookup_class_ex(name, lcname, flags); +} + +int av_snprintf(char *buffer, size_t size, const char *format, ...) +{ + va_list args; + va_start(args, format); + int result = vsnprintf(buffer, size, format, args); + va_end(args); + return result; +} + +double av_fmax(double a, double b) +{ + return fmax(a, b); +} diff --git a/src/helpers/av_wrappers.h b/src/helpers/av_wrappers.h index 6140c25..79ff5cb 100644 --- a/src/helpers/av_wrappers.h +++ b/src/helpers/av_wrappers.h @@ -28,4 +28,17 @@ void av_zval_ptr_dtor(zval *zval_ptr); /* Additional wrappers used by av_replace_placeholders to allow mocking in unit tests. */ const char *av_memnstr(const char *haystack, const char *needle, size_t needle_len, const char *end); +/* Additional wrappers used by av_error_messages to keep the unit-test build + * free of unresolved Zend/library symbols. Every Zend Hash Table, class lookup, + * string formatting and math call in av_error_messages.c goes through these so + * the whole translation unit can be linked into Ceedling tests. */ +zval *av_hash_find(const HashTable *ht, zend_string *key); +zval *av_hash_next_index_insert(HashTable *ht, zval *pData); +zval *av_hash_add(HashTable *ht, zend_string *key, zval *pData); +HashTable *av_new_array(uint32_t size); +void av_zval_stringl(zval *z, const char *str, size_t len); +zend_class_entry *av_lookup_class_ex(zend_string *name, zend_string *lcname, uint32_t flags); +int av_snprintf(char *buffer, size_t size, const char *format, ...) ZEND_ATTRIBUTE_FORMAT(printf, 3, 4); +double av_fmax(double a, double b); + #endif /* AV_HELPERS_AV_WRAPPERS_H */ diff --git a/tests/c/support/test_helpers.c b/tests/c/support/test_helpers.c index b769a61..8e879b4 100644 --- a/tests/c/support/test_helpers.c +++ b/tests/c/support/test_helpers.c @@ -1,6 +1,7 @@ #include "unity.h" #include "helpers/mock_av_wrappers.h" #include +#include #include #include @@ -79,3 +80,20 @@ const char *memnstr_stub(const char *haystack, const char *needle, size_t needle } return NULL; } + +// PHP's php.h #defines snprintf -> ap_php_snprintf and vsnprintf -> +// ap_php_vsnprintf. The unit-test build has no Zend library, so provide these +// symbols delegating to the compiler builtins (which ignore the macros). +int ap_php_snprintf(char *buf, size_t len, const char *format, ...) +{ + va_list args; + va_start(args, format); + int result = __builtin_vsnprintf(buf, len, format, args); + va_end(args); + return result; +} + +int ap_php_vsnprintf(char *buf, size_t len, const char *format, va_list ap) +{ + return __builtin_vsnprintf(buf, len, format, ap); +} diff --git a/tests/c/support/test_helpers.h b/tests/c/support/test_helpers.h index 0f0d8bc..315d34e 100644 --- a/tests/c/support/test_helpers.h +++ b/tests/c/support/test_helpers.h @@ -12,4 +12,11 @@ extern zend_string *string_alloc_stub(size_t length, bool persistent, int num_ca extern zend_string *string_truncate_stub(zend_string *s, size_t length, bool persistent, int num_calls); extern const char *memnstr_stub(const char *haystack, const char *needle, size_t needle_len, const char *end, int num_calls); +// Stubs for the PHP snprintf API (php.h #defines snprintf/vsnprintf to +// ap_php_snprintf/ap_php_vsnprintf). The Ceedling unit-test build has no +// Zend library to link against, so these delegate to the compiler builtins so +// the test files can keep using snprintf() naturally after including php.h. +extern int ap_php_snprintf(char *buf, size_t len, const char *format, ...); +extern int ap_php_vsnprintf(char *buf, size_t len, const char *format, va_list ap); + #endif /* TEST_HELPERS_H */ diff --git a/tests/c/test_error_messages.c b/tests/c/test_error_messages.c index 93e30d6..3bc022d 100644 --- a/tests/c/test_error_messages.c +++ b/tests/c/test_error_messages.c @@ -1,9 +1,9 @@ #include "unity.h" #include "test_helpers.h" #include "helpers/mock_av_wrappers.h" -#include "helpers/av_replace_placeholders.h" -#include "helpers/av_value_to_string.h" +#include "helpers/av_error_messages.h" #include +#include #include #include #include @@ -72,22 +72,6 @@ static zend_string *double_to_str_stub(double num, int num_calls) return string_init_stub("0", 1, 0, num_calls); } -// --------------------------------------------------------------------------- -// Hand-written stub for build_union_type_string (the {expected} provider). -// The real implementation lives in av_error_messages.c, which pulls in the full -// Zend engine and is therefore not linked into this unit test. CMock cannot -// mock it in isolation because it shares its header with the function under -// test, so we provide a minimal definition returning a configurable string. -// --------------------------------------------------------------------------- - -static const char *g_expected_type_string; - -zend_string *build_union_type_string(zend_type property_type) -{ - (void)property_type; - return string_init_stub(g_expected_type_string, strlen(g_expected_type_string), 0, 0); -} - // --------------------------------------------------------------------------- // Helpers to build av_field / av_property_info without a running Zend engine. // --------------------------------------------------------------------------- @@ -101,10 +85,16 @@ static av_field make_field(const char *name, zval *value) return field; } -static av_property_info make_prop_info(zend_property_info *prop, zend_type *type) +// Builds an av_property_info carrying a single basic type mask. The real +// build_union_type_string() (now linked from av_error_messages.c) derives the +// {expected} string from this mask, so the placeholder tests exercise the real +// type-to-article logic instead of a stub. +static av_property_info make_prop_info(zend_property_info *prop, zend_type *type, uint32_t type_mask) { memset(prop, 0, sizeof(*prop)); memset(type, 0, sizeof(*type)); + type->type_mask = type_mask; + type->ptr = NULL; prop->type = *type; av_property_info info; info.model = NULL; @@ -130,8 +120,6 @@ void setUp(void) av_long_to_str_Stub(long_to_str_stub); av_double_to_str_Stub(double_to_str_stub); av_memnstr_Stub(memnstr_stub); - - g_expected_type_string = "an integer"; } void tearDown(void) @@ -281,20 +269,18 @@ void test_value_placeholder_replaced_with_boolean_value(void) } // --------------------------------------------------------------------------- -// {expected} placeholder (delegates to build_union_type_string, stubbed here) +// {expected} placeholder (delegates to the real build_union_type_string) // --------------------------------------------------------------------------- void test_expected_placeholder_replaced_with_type_string(void) { - g_expected_type_string = "an integer"; - zval value; ZVAL_NULL(&value); av_field field = make_field("age", &value); zend_property_info prop; zend_type type; - av_property_info prop_info = make_prop_info(&prop, &type); + av_property_info prop_info = make_prop_info(&prop, &type, MAY_BE_LONG); const char *template = "The {field} must be {expected}."; zend_string *result = av_replace_placeholders(template, strlen(template), &field, &prop_info); @@ -305,15 +291,13 @@ void test_expected_placeholder_replaced_with_type_string(void) void test_expected_placeholder_repeated(void) { - g_expected_type_string = "a string"; - zval value; ZVAL_NULL(&value); av_field field = make_field("name", &value); zend_property_info prop; zend_type type; - av_property_info prop_info = make_prop_info(&prop, &type); + av_property_info prop_info = make_prop_info(&prop, &type, MAY_BE_STRING); const char *template = "{field} must be {expected} or {expected}"; zend_string *result = av_replace_placeholders(template, strlen(template), &field, &prop_info); @@ -328,15 +312,13 @@ void test_expected_placeholder_repeated(void) void test_all_three_placeholders_replaced(void) { - g_expected_type_string = "an integer"; - zval value; ZVAL_LONG(&value, 7); av_field field = make_field("count", &value); zend_property_info prop; zend_type type; - av_property_info prop_info = make_prop_info(&prop, &type); + av_property_info prop_info = make_prop_info(&prop, &type, MAY_BE_LONG); const char *template = "The {field} must be {expected}, got {value}."; zend_string *result = av_replace_placeholders(template, strlen(template), &field, &prop_info); @@ -347,15 +329,13 @@ void test_all_three_placeholders_replaced(void) void test_non_placeholder_braced_text_is_preserved(void) { - g_expected_type_string = "a float"; - zval value; ZVAL_LONG(&value, 1); av_field field = make_field("price", &value); zend_property_info prop; zend_type type; - av_property_info prop_info = make_prop_info(&prop, &type); + av_property_info prop_info = make_prop_info(&prop, &type, MAY_BE_DOUBLE); // "{fieldx}" is not a placeholder and must be left untouched. const char *template = "{field} {fieldx} must be {expected}; got {value}"; diff --git a/tests/c/test_value_to_string.c b/tests/c/test_value_to_string.c index b72c271..2fefb07 100644 --- a/tests/c/test_value_to_string.c +++ b/tests/c/test_value_to_string.c @@ -1,7 +1,7 @@ #include "unity.h" #include "test_helpers.h" #include "helpers/mock_av_wrappers.h" -#include "helpers/av_value_to_string.h" +#include "helpers/av_error_messages.h" #include #include #include