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 dbb0d94..7da47c2 100644 --- a/src/helpers/av_error_messages.c +++ b/src/helpers/av_error_messages.c @@ -17,15 +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) -{ - // TODO: Implement zval to zend_string implementation - if (value == NULL) { - return zend_string_init("null", 4, 0); - } - 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..c26c555 --- /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_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_value_to_string.h b/src/helpers/av_value_to_string.h new file mode 100644 index 0000000..deb4851 --- /dev/null +++ b/src/helpers/av_value_to_string.h @@ -0,0 +1,13 @@ +#ifndef AV_HELPERS_VALUE_TO_STRING_H +#define AV_HELPERS_VALUE_TO_STRING_H + +#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..604a817 100644 --- a/src/helpers/av_wrappers.h +++ b/src/helpers/av_wrappers.h @@ -12,4 +12,17 @@ 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); +}