Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -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
257 changes: 209 additions & 48 deletions src/helpers/av_error_messages.c
Original file line number Diff line number Diff line change
@@ -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 <stddef.h>
#include <string.h>

Expand All @@ -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);
}
}

Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -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;
}
}
Expand All @@ -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;
}
Expand All @@ -225,34 +221,34 @@ 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;
}
}
ZEND_TYPE_FOREACH_END();

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;
}
Expand All @@ -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;
}
Loading
Loading