From 3cc3763027ad8d967cdbe3e614447bba3e55eaeb Mon Sep 17 00:00:00 2001 From: Vibe Nuage Agent Date: Thu, 10 Sep 2026 14:38:17 +0000 Subject: [PATCH 1/4] feat: Implement proper zval to string conversion in av_value_to_string Replace the placeholder implementation of av_value_to_string with full type-aware conversion of any PHP zval into a zend_string for use in error message templates: - null -> "null" - bool -> "true"/"false" (instead of "1"/"") - int/double -> numeric string via zend_long_to_str/zend_double_to_str - string -> quoted (single quotes wrap the value) - array -> "array" - object -> __toString() result if Stringable, else the class name - resource -> the resource type name, or "resource" - references are dereferenced before inspection Add zend_list.h and math.h includes for the resource type lookup. Co-authored-by: matapatos --- src/helpers/av_error_messages.c | 44 ++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/src/helpers/av_error_messages.c b/src/helpers/av_error_messages.c index dbb0d94..c902faa 100644 --- a/src/helpers/av_error_messages.c +++ b/src/helpers/av_error_messages.c @@ -9,6 +9,8 @@ #include "zend_portability.h" #include "zend_string.h" #include "zend_types.h" +#include "zend_list.h" +#include #include #include @@ -19,11 +21,47 @@ static const char* av_error_type_messages[] = { static zend_string* av_value_to_string(zval *value) { - // TODO: Implement zval to zend_string implementation if (value == NULL) { - return zend_string_init("null", 4, 0); + return zend_string_init("null", sizeof("null") - 1, 0); + } + + ZVAL_DEREF(value); + + switch (Z_TYPE_P(value)) { + case IS_NULL: + return zend_string_init("null", sizeof("null") - 1, 0); + case IS_TRUE: + return zend_string_init("true", sizeof("true") - 1, 0); + case IS_FALSE: + return zend_string_init("false", sizeof("false") - 1, 0); + case IS_LONG: + return zend_long_to_str(Z_LVAL_P(value)); + case IS_DOUBLE: + return zend_double_to_str(Z_DVAL_P(value)); + case IS_STRING: + return zend_string_concat3("'", 1, Z_STRVAL_P(value), Z_STRLEN_P(value), "'", 1); + case IS_ARRAY: + return zend_string_init("array", sizeof("array") - 1, 0); + case IS_OBJECT: + if (instanceof_function(Z_OBJCE_P(value), zend_ce_stringable)) { + zval result; + if (zend_call_method_with_0_params(Z_OBJ_P(value), NULL, NULL, "__tostring", &result) == SUCCESS && Z_TYPE(result) == IS_STRING) { + zend_string *str = zend_string_concat3("'", 1, Z_STRVAL(result), Z_STRLEN(result), "'", 1); + zval_ptr_dtor(&result); + return str; + } + if (Z_TYPE(result) != IS_UNDEF) { + zval_ptr_dtor(&result); + } + } + return zend_string_copy(Z_OBJCE_P(value)->name); + case IS_RESOURCE: { + const char *type_name = zend_rsrc_list_get_rsrc_type(Z_RES_P(value)); + return zend_string_init(type_name ? type_name : "resource", strlen(type_name ? type_name : "resource"), 0); + } + default: + return zval_get_string(value); } - return zval_get_string(value); } static zend_string* av_replace_placeholders(const char *template, size_t length, av_field *field, av_property_info *prop_info) From 80cae469eacfea2518aa9cf14ba195a93db3c9f7 Mon Sep 17 00:00:00 2001 From: Vibe Nuage Agent Date: Thu, 10 Sep 2026 14:53:54 +0000 Subject: [PATCH 2/4] test: Add unit tests for av_value_to_string Add Ceedling C unit tests covering av_value_to_string for every zval type (null, bool, long, double, string, array, object, resource) plus the Stringable/__toString and resource-unknown-type fallback paths. To make the function unit-testable in isolation: - Move av_value_to_string into its own translation unit (src/helpers/av_value_to_string.{c,h}) so tests link only that minimal file instead of the full av_error_messages.c dependency graph. - Route all Zend internals it touches through mockable wrappers in av_wrappers.{c,h}: av_string_concat3, av_string_copy, av_long_to_str, av_double_to_str, av_is_stringable, av_instanceof_function, av_rsrc_list_get_rsrc_type, av_call_tostring, av_zval_ptr_dtor. - Fix a latent bug exposed by the tests: the Stringable branch compared the return of zend_call_method_with_0_params (a zval*) to SUCCESS, which is always false, making __toString a dead path. av_call_tostring now reports success only when __toString returns a string. - Register the new source file in config.m4. - Teach CMock (project.yml) to treat zend_class_entry/zend_object/ zend_resource as void so pointer args compare by value instead of trying to sizeof an incomplete struct. Verification: ceedling test:all -> 114/114 pass (89 existing + 25 new); extension builds cleanly with the new translation unit. Co-authored-by: matapatos --- config.m4 | 2 +- project.yml | 7 +- src/helpers/av_error_messages.c | 47 ----- src/helpers/av_error_messages.h | 3 + src/helpers/av_value_to_string.c | 65 +++++++ src/helpers/av_value_to_string.h | 14 ++ src/helpers/av_wrappers.c | 51 +++++ src/helpers/av_wrappers.h | 16 +- tests/c/test_value_to_string.c | 307 +++++++++++++++++++++++++++++++ 9 files changed, 462 insertions(+), 50 deletions(-) create mode 100644 src/helpers/av_value_to_string.c create mode 100644 src/helpers/av_value_to_string.h create mode 100644 tests/c/test_value_to_string.c diff --git a/config.m4 b/config.m4 index 0f7003e..b8ca6f2 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/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/helpers/av_value_to_string.c src/fields/av_field.c src/fields/av_alias.c src/validators/av_typehint_validator.c, $ext_shared) fi diff --git a/project.yml b/project.yml index 017c9c7..58fa318 100644 --- a/project.yml +++ b/project.yml @@ -186,7 +186,12 @@ int8: INT8 bool: UINT8 #:treat_as_array: {} # hint to cmock that these types are pointers to something - #:treat_as_void: [] # hint to cmock that these types are actually aliases of void + # Treat opaque Zend types as void so CMock compares pointers by value instead + # of trying to sizeof an incomplete struct definition. + :treat_as_void: + - zend_class_entry + - zend_object + - zend_resource :memcmp_if_unknown: true # allow cmock to use the memory comparison assertions for unknown types :when_ptr: :compare_data # hint to cmock how to handle pointers in general, the options being :compare_ptr, :compare_data, or :smart diff --git a/src/helpers/av_error_messages.c b/src/helpers/av_error_messages.c index c902faa..7da47c2 100644 --- a/src/helpers/av_error_messages.c +++ b/src/helpers/av_error_messages.c @@ -9,8 +9,6 @@ #include "zend_portability.h" #include "zend_string.h" #include "zend_types.h" -#include "zend_list.h" -#include #include #include @@ -19,51 +17,6 @@ static const char* av_error_type_messages[] = { [AV_ERROR_TYPE] = "The {field} must be {expected}.", }; -static zend_string* av_value_to_string(zval *value) -{ - if (value == NULL) { - return zend_string_init("null", sizeof("null") - 1, 0); - } - - ZVAL_DEREF(value); - - switch (Z_TYPE_P(value)) { - case IS_NULL: - return zend_string_init("null", sizeof("null") - 1, 0); - case IS_TRUE: - return zend_string_init("true", sizeof("true") - 1, 0); - case IS_FALSE: - return zend_string_init("false", sizeof("false") - 1, 0); - case IS_LONG: - return zend_long_to_str(Z_LVAL_P(value)); - case IS_DOUBLE: - return zend_double_to_str(Z_DVAL_P(value)); - case IS_STRING: - return zend_string_concat3("'", 1, Z_STRVAL_P(value), Z_STRLEN_P(value), "'", 1); - case IS_ARRAY: - return zend_string_init("array", sizeof("array") - 1, 0); - case IS_OBJECT: - if (instanceof_function(Z_OBJCE_P(value), zend_ce_stringable)) { - zval result; - if (zend_call_method_with_0_params(Z_OBJ_P(value), NULL, NULL, "__tostring", &result) == SUCCESS && Z_TYPE(result) == IS_STRING) { - zend_string *str = zend_string_concat3("'", 1, Z_STRVAL(result), Z_STRLEN(result), "'", 1); - zval_ptr_dtor(&result); - return str; - } - if (Z_TYPE(result) != IS_UNDEF) { - zval_ptr_dtor(&result); - } - } - return zend_string_copy(Z_OBJCE_P(value)->name); - case IS_RESOURCE: { - const char *type_name = zend_rsrc_list_get_rsrc_type(Z_RES_P(value)); - return zend_string_init(type_name ? type_name : "resource", strlen(type_name ? type_name : "resource"), 0); - } - default: - return zval_get_string(value); - } -} - static 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[] = { diff --git a/src/helpers/av_error_messages.h b/src/helpers/av_error_messages.h index a43d1e0..599a957 100644 --- a/src/helpers/av_error_messages.h +++ b/src/helpers/av_error_messages.h @@ -19,6 +19,9 @@ static zend_string* build_union_type_string(zend_type property_type); static zend_string* generate_error_message(av_field *field, zend_type property_type); static bool av_vowel_sound(char c); +// Value conversion +zend_string* av_value_to_string(zval *value); + // Updates errors static zend_always_inline void add_field_error_to_array(zval *errors_array, const char *error_message, size_t length); static zend_always_inline void add_field_error(zval *errors, zend_string *field_name, const char *error_message, size_t length); diff --git a/src/helpers/av_value_to_string.c b/src/helpers/av_value_to_string.c new file mode 100644 index 0000000..d087d0a --- /dev/null +++ b/src/helpers/av_value_to_string.c @@ -0,0 +1,65 @@ +#include "av_value_to_string.h" +#include "av_wrappers.h" +#include "Zend/zend_operators.h" +#include "Zend/zend_types.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_value_to_string.h b/src/helpers/av_value_to_string.h new file mode 100644 index 0000000..71ea1c0 --- /dev/null +++ b/src/helpers/av_value_to_string.h @@ -0,0 +1,14 @@ +#ifndef AV_HELPERS_VALUE_TO_STRING_H +#define AV_HELPERS_VALUE_TO_STRING_H + +#include +#include + +/* + * Converts any PHP zval into a zend_string for use in error message + * templates (the {value} placeholder). See av_value_to_string.c for the + * per-type conversion rules. + */ +zend_string* av_value_to_string(zval *value); + +#endif /* AV_HELPERS_VALUE_TO_STRING_H */ diff --git a/src/helpers/av_wrappers.c b/src/helpers/av_wrappers.c index f29811d..a3efe67 100644 --- a/src/helpers/av_wrappers.c +++ b/src/helpers/av_wrappers.c @@ -1,5 +1,9 @@ #include "av_wrappers.h" #include "Zend/zend_API.h" +#include "Zend/zend_interfaces.h" +#include "Zend/zend_list.h" +#include "Zend/zend_operators.h" +#include "Zend/zend_variables.h" /* * Wrapper implementations for Zend internals. @@ -22,3 +26,50 @@ void* av_emalloc(size_t size) { void av_efree(void *ptr) { efree(ptr); } + +zend_string* av_string_concat3(const char *str1, size_t str1_len, + const char *str2, size_t str2_len, + const char *str3, size_t str3_len) { + return zend_string_concat3(str1, str1_len, str2, str2_len, str3, str3_len); +} + +zend_string* av_string_copy(zend_string *s) { + return zend_string_copy(s); +} + +zend_string* av_long_to_str(zend_long num) { + return zend_long_to_str(num); +} + +zend_string* av_double_to_str(double num) { + return zend_double_to_str(num); +} + +bool av_instanceof_function(const zend_class_entry *instance_ce, const zend_class_entry *ce) { + return instanceof_function(instance_ce, ce); +} + +bool av_is_stringable(const zend_class_entry *instance_ce) { + return instanceof_function(instance_ce, zend_ce_stringable); +} + +const char* av_rsrc_list_get_rsrc_type(zend_resource *res) { + return zend_rsrc_list_get_rsrc_type(res); +} + +zend_result av_call_tostring(zend_object *object, zval *retval) { + ZVAL_UNDEF(retval); + zend_call_method_with_0_params(object, NULL, NULL, "__tostring", retval); + if (Z_TYPE_P(retval) != IS_STRING) { + if (Z_TYPE_P(retval) != IS_UNDEF) { + zval_ptr_dtor(retval); + ZVAL_UNDEF(retval); + } + return FAILURE; + } + return SUCCESS; +} + +void av_zval_ptr_dtor(zval *zval_ptr) { + zval_ptr_dtor(zval_ptr); +} diff --git a/src/helpers/av_wrappers.h b/src/helpers/av_wrappers.h index c34a077..c27f85e 100644 --- a/src/helpers/av_wrappers.h +++ b/src/helpers/av_wrappers.h @@ -6,10 +6,24 @@ #define AV_HELPERS_AV_WRAPPERS_H #include +#include zend_string* av_string_init(const char *str, size_t len, bool persistent); void av_string_release(zend_string *s); void* av_emalloc(size_t size); void av_efree(void *ptr); -#endif /* AV_HELPERS_AV_WRAPPERS_H */ \ No newline at end of file +/* Additional wrappers used by av_value_to_string to allow mocking in unit tests. */ +zend_string* av_string_concat3(const char *str1, size_t str1_len, + const char *str2, size_t str2_len, + const char *str3, size_t str3_len); +zend_string* av_string_copy(zend_string *s); +zend_string* av_long_to_str(zend_long num); +zend_string* av_double_to_str(double num); +bool av_instanceof_function(const zend_class_entry *instance_ce, const zend_class_entry *ce); +bool av_is_stringable(const zend_class_entry *instance_ce); +const char* av_rsrc_list_get_rsrc_type(zend_resource *res); +zend_result av_call_tostring(zend_object *object, zval *retval); +void av_zval_ptr_dtor(zval *zval_ptr); + +#endif /* AV_HELPERS_AV_WRAPPERS_H */ diff --git a/tests/c/test_value_to_string.c b/tests/c/test_value_to_string.c new file mode 100644 index 0000000..d4388ba --- /dev/null +++ b/tests/c/test_value_to_string.c @@ -0,0 +1,307 @@ +#include "unity.h" +#include "test_helpers.h" +#include "helpers/mock_av_wrappers.h" +#include "helpers/av_value_to_string.h" +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Mock callbacks for the wrappers used by av_value_to_string. +// They build real zend_string allocations through the malloc-based stub so the +// produced strings can be inspected and freed with av_string_release. +// --------------------------------------------------------------------------- + +static zend_string* concat3_stub(const char *str1, size_t str1_len, + const char *str2, size_t str2_len, + const char *str3, size_t str3_len, + int num_calls) { + size_t total = str1_len + str2_len + str3_len; + zend_string *s = string_init_stub("", total, 0, num_calls); + char *p = s->val; + memcpy(p, str1, str1_len); p += str1_len; + memcpy(p, str2, str2_len); p += str2_len; + memcpy(p, str3, str3_len); p += str3_len; + s->val[total] = '\0'; + s->len = total; + return s; +} + +static zend_string* string_copy_stub(zend_string *s, int num_calls) { + return string_init_stub(s->val, s->len, 0, num_calls); +} + +static zend_string* long_to_str_stub(zend_long num, int num_calls) { + char buf[32]; + int len = snprintf(buf, sizeof(buf), ZEND_LONG_FMT, num); + return string_init_stub(buf, (size_t)len, 0, num_calls); +} + +static zend_string* double_to_str_stub(double num, int num_calls) { + char buf[64]; + int len = snprintf(buf, sizeof(buf), "%.14g", num); + return string_init_stub(buf, (size_t)len, 0, num_calls); +} + +// Test-controlled state for the instanceof / resource / __toString paths. +static bool g_instanceof_result; +static const char *g_resource_type_name; +static zend_result g_tostring_result; +static const char *g_tostring_value; + +static bool instanceof_stub(const zend_class_entry *instance_ce, + const zend_class_entry *ce, int num_calls) { + (void)instance_ce; (void)ce; (void)num_calls; + return g_instanceof_result; +} + +static bool is_stringable_stub(const zend_class_entry *instance_ce, int num_calls) { + (void)instance_ce; (void)num_calls; + return g_instanceof_result; +} + +static const char* rsrc_type_stub(zend_resource *res, int num_calls) { + (void)res; (void)num_calls; + return g_resource_type_name; +} + +static zend_result tostring_stub(zend_object *object, zval *retval, int num_calls) { + (void)object; (void)num_calls; + ZVAL_UNDEF(retval); + if (g_tostring_result == SUCCESS && g_tostring_value != NULL) { + ZVAL_STR(retval, string_init_stub(g_tostring_value, strlen(g_tostring_value), 0, num_calls)); + } + return g_tostring_result; +} + +static void zval_ptr_dtor_stub(zval *zval_ptr, int num_calls) { + (void)num_calls; + if (zval_ptr != NULL && Z_TYPE_P(zval_ptr) == IS_STRING && Z_STR_P(zval_ptr) != NULL) { + string_release_stub(Z_STR_P(zval_ptr), num_calls); + ZVAL_UNDEF(zval_ptr); + } +} + +// --------------------------------------------------------------------------- +// Helpers to build zvals of each type without a running Zend engine. +// --------------------------------------------------------------------------- + +static zend_class_entry make_ce(const char *name) { + zend_class_entry ce; + memset(&ce, 0, sizeof(ce)); + ce.name = string_init_stub(name, strlen(name), 0, 0); + return ce; +} + +static zend_object make_object(zend_class_entry *ce) { + zend_object obj; + memset(&obj, 0, sizeof(obj)); + obj.ce = ce; + return obj; +} + +static zend_resource make_resource(int type) { + zend_resource res; + memset(&res, 0, sizeof(res)); + res.type = type; + return res; +} + +// --------------------------------------------------------------------------- +// setUp / tearDown +// --------------------------------------------------------------------------- + +void setUp(void) { + av_string_init_Stub(string_init_stub); + av_string_release_Stub(string_release_stub); + av_emalloc_Stub(emalloc_stub); + av_efree_Stub(efree_stub); + av_string_concat3_Stub(concat3_stub); + av_string_copy_Stub(string_copy_stub); + av_long_to_str_Stub(long_to_str_stub); + av_double_to_str_Stub(double_to_str_stub); + av_instanceof_function_Stub(instanceof_stub); + av_is_stringable_Stub(is_stringable_stub); + av_rsrc_list_get_rsrc_type_Stub(rsrc_type_stub); + av_call_tostring_Stub(tostring_stub); + av_zval_ptr_dtor_Stub(zval_ptr_dtor_stub); + + g_instanceof_result = false; + g_resource_type_name = "stream"; + g_tostring_result = FAILURE; + g_tostring_value = NULL; +} + +void tearDown(void) { +} + +// --------------------------------------------------------------------------- +// null +// --------------------------------------------------------------------------- + +void test_null_value_to_string(void) { + TEST_ASSERT_EQUAL_STRING("null", av_value_to_string(NULL)->val); +} + +void test_explicit_null_zval(void) { + zval zv; ZVAL_NULL(&zv); + zend_string *result = av_value_to_string(&zv); + TEST_ASSERT_EQUAL_STRING("null", result->val); + av_string_release(result); +} + +// --------------------------------------------------------------------------- +// booleans +// --------------------------------------------------------------------------- + +void test_true_to_string(void) { + zval zv; ZVAL_TRUE(&zv); + zend_string *result = av_value_to_string(&zv); + TEST_ASSERT_EQUAL_STRING("true", result->val); + av_string_release(result); +} + +void test_false_to_string(void) { + zval zv; ZVAL_FALSE(&zv); + zend_string *result = av_value_to_string(&zv); + TEST_ASSERT_EQUAL_STRING("false", result->val); + av_string_release(result); +} + +// --------------------------------------------------------------------------- +// long +// --------------------------------------------------------------------------- + +TEST_CASE(0) +TEST_CASE(1) +TEST_CASE(-1) +TEST_CASE(42) +TEST_CASE(-42) +TEST_CASE(2147483647) +TEST_CASE(-2147483648) +void test_long_to_string(zend_long value) { + zval zv; ZVAL_LONG(&zv, value); + zend_string *result = av_value_to_string(&zv); + char expected[32]; + snprintf(expected, sizeof(expected), ZEND_LONG_FMT, value); + TEST_ASSERT_EQUAL_STRING(expected, result->val); + av_string_release(result); +} + +// --------------------------------------------------------------------------- +// double +// --------------------------------------------------------------------------- + +TEST_CASE(0.0) +TEST_CASE(1.5) +TEST_CASE(-3.14) +TEST_CASE(100.0) +void test_double_to_string(double value) { + zval zv; ZVAL_DOUBLE(&zv, value); + zend_string *result = av_value_to_string(&zv); + char expected[64]; + snprintf(expected, sizeof(expected), "%.14g", value); + TEST_ASSERT_EQUAL_STRING(expected, result->val); + av_string_release(result); +} + +// --------------------------------------------------------------------------- +// string +// --------------------------------------------------------------------------- + +TEST_CASE("") +TEST_CASE("hello") +TEST_CASE("The quick brown fox") +TEST_CASE("with'quotes") +void test_string_to_string(const char *value) { + zval zv; ZVAL_STR(&zv, string_init_stub(value, strlen(value), 0, 0)); + zend_string *result = av_value_to_string(&zv); + char expected[256]; + snprintf(expected, sizeof(expected), "'%s'", value); + TEST_ASSERT_EQUAL_STRING(expected, result->val); + av_string_release(zv.value.str); + av_string_release(result); +} + +// --------------------------------------------------------------------------- +// array +// --------------------------------------------------------------------------- + +void test_array_to_string(void) { + zval zv; ZVAL_ARR(&zv, NULL); + zend_string *result = av_value_to_string(&zv); + TEST_ASSERT_EQUAL_STRING("array", result->val); + av_string_release(result); +} + +// --------------------------------------------------------------------------- +// object +// --------------------------------------------------------------------------- + +void test_object_with_tostring_uses_result(void) { + zend_class_entry ce = make_ce("MyStringable"); + zend_object obj = make_object(&ce); + zval zv; ZVAL_OBJ(&zv, &obj); + + g_instanceof_result = true; + g_tostring_result = SUCCESS; + g_tostring_value = "stringified"; + + zend_string *result = av_value_to_string(&zv); + TEST_ASSERT_EQUAL_STRING("'stringified'", result->val); + av_string_release(result); + av_string_release(ce.name); +} + +void test_object_stringable_but_tostring_fails_falls_back_to_class_name(void) { + zend_class_entry ce = make_ce("FailingStringable"); + zend_object obj = make_object(&ce); + zval zv; ZVAL_OBJ(&zv, &obj); + + g_instanceof_result = true; + g_tostring_result = FAILURE; + + zend_string *result = av_value_to_string(&zv); + TEST_ASSERT_EQUAL_STRING("FailingStringable", result->val); + av_string_release(result); + av_string_release(ce.name); +} + +void test_object_not_stringable_uses_class_name(void) { + zend_class_entry ce = make_ce("PlainObject"); + zend_object obj = make_object(&ce); + zval zv; ZVAL_OBJ(&zv, &obj); + + g_instanceof_result = false; + + zend_string *result = av_value_to_string(&zv); + TEST_ASSERT_EQUAL_STRING("PlainObject", result->val); + av_string_release(result); + av_string_release(ce.name); +} + +// --------------------------------------------------------------------------- +// resource +// --------------------------------------------------------------------------- + +void test_resource_to_string(void) { + zend_resource res = make_resource(1); + zval zv; ZVAL_RES(&zv, &res); + g_resource_type_name = "stream"; + + zend_string *result = av_value_to_string(&zv); + TEST_ASSERT_EQUAL_STRING("stream", result->val); + av_string_release(result); +} + +void test_resource_with_unknown_type_falls_back(void) { + zend_resource res = make_resource(999); + zval zv; ZVAL_RES(&zv, &res); + g_resource_type_name = NULL; + + zend_string *result = av_value_to_string(&zv); + TEST_ASSERT_EQUAL_STRING("resource", result->val); + av_string_release(result); +} From fa0ac8f5f47411f98e5a3c61aa2494fce7a93008 Mon Sep 17 00:00:00 2001 From: Vibe Nuage Agent Date: Thu, 10 Sep 2026 15:16:23 +0000 Subject: [PATCH 3/4] fix: Remove premature zend_string.h include from av_wrappers.h Including in av_wrappers.h before the core Zend headers caused PHP 8.2 build failures: ZSTR_VAL/ZSTR_LEN macros were undefined, cascading into errors in zend_hash.h and zend_operators.h. The forward declaration in zend_types.h suffices for the header's prototypes; the .c file includes zend_API.h which pulls zend_string.h in the correct transitive order. Co-authored-by: matapatos --- src/helpers/av_wrappers.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/helpers/av_wrappers.h b/src/helpers/av_wrappers.h index c27f85e..604a817 100644 --- a/src/helpers/av_wrappers.h +++ b/src/helpers/av_wrappers.h @@ -6,7 +6,6 @@ #define AV_HELPERS_AV_WRAPPERS_H #include -#include zend_string* av_string_init(const char *str, size_t len, bool persistent); void av_string_release(zend_string *s); From 2173ad2d5425c09f1d4278dda7bf66c5de8f1cfb Mon Sep 17 00:00:00 2001 From: Vibe Nuage Agent Date: Thu, 10 Sep 2026 15:19:20 +0000 Subject: [PATCH 4/4] fix: Use zend_API.h as Zend entry point in av_value_to_string The previous piecemeal includes of zend_string.h (in the header) and zend_operators.h/zend_types.h (in the .c) work on PHP 8.4 but break on PHP 8.2: including zend_string.h before the full zend.h chain leaves ZSTR_VAL/ZSTR_LEN and the zend_string_* inline declarations undefined when zend_hash.h and zend_operators.h expand, causing "conflicting types" and "subscripted value is neither array nor pointer" errors. Fix by following the repo convention: include Zend/zend_API.h, which pulls in zend.h and the rest of the Zend headers in the correct order. Remove the standalone zend_string.h include from the header since zend_types.h already forward-declares zend_string for the prototype. Co-authored-by: matapatos --- src/helpers/av_value_to_string.c | 2 +- src/helpers/av_value_to_string.h | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/helpers/av_value_to_string.c b/src/helpers/av_value_to_string.c index d087d0a..c26c555 100644 --- a/src/helpers/av_value_to_string.c +++ b/src/helpers/av_value_to_string.c @@ -1,7 +1,7 @@ #include "av_value_to_string.h" #include "av_wrappers.h" +#include "Zend/zend_API.h" #include "Zend/zend_operators.h" -#include "Zend/zend_types.h" #include /* diff --git a/src/helpers/av_value_to_string.h b/src/helpers/av_value_to_string.h index 71ea1c0..deb4851 100644 --- a/src/helpers/av_value_to_string.h +++ b/src/helpers/av_value_to_string.h @@ -2,7 +2,6 @@ #define AV_HELPERS_VALUE_TO_STRING_H #include -#include /* * Converts any PHP zval into a zend_string for use in error message