Skip to content
Merged
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_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/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)
fi
94 changes: 2 additions & 92 deletions src/helpers/av_error_messages.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "av_error_messages.h"
#include "av_replace_placeholders.h"
#include "../av_base_model.h"
#include "Zend/zend_API.h"
#include "Zend/zend_exceptions.h"
Expand All @@ -17,97 +18,6 @@ static const char *av_error_type_messages[] = {
[AV_ERROR_TYPE] = "The {field} must be {expected}.",
};

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[] = {{"{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 = php_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 zend_string_init(template, length, 0);
}

// Allocate and process in a single pass
zend_string *result = zend_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 = zend_string_truncate(result, output_pos, 0);

for (size_t i = 1; i < table_size; i++) {
if (table[i].replace != NULL) {
zend_string_release(table[i].replace);
}
}

return result;
}

static zend_always_inline void add_field_error_to_array(zval *errors_array, const char *error_message, size_t length)
{
zval error_msg;
Expand Down Expand Up @@ -263,7 +173,7 @@ static zend_always_inline zend_string *build_union_only_basic_types(uint32_t pur
return zend_string_truncate(result, output_pos, 0);
}

static zend_always_inline zend_string *build_union_type_string(zend_type property_type)
zend_string *build_union_type_string(zend_type property_type)
{
uint32_t pure_mask = ZEND_TYPE_PURE_MASK(property_type);
bool is_simple_union = !ZEND_TYPE_HAS_LIST(property_type) && ZEND_TYPE_IS_SET(property_type) && (pure_mask & (pure_mask - 1)) != 0;
Expand Down
5 changes: 4 additions & 1 deletion src/helpers/av_error_messages.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ typedef enum {
AV_ERROR_TYPE
} av_error_type;

// Placeholder substitution
zend_string *av_replace_placeholders(const char *template, size_t length, av_field *field, av_property_info *prop_info);

// Error message generation
static zend_string *generate_type_name(const zend_type *type);
static bool is_type_enum(const zend_type *type);
static zend_string *build_single_type_with_article(const zend_type *type);
static zend_string *build_union_type_string(zend_type property_type);
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);

Expand Down
110 changes: 110 additions & 0 deletions src/helpers/av_replace_placeholders.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#include "av_replace_placeholders.h"
#include "av_error_messages.h"
#include "av_value_to_string.h"
#include "av_wrappers.h"
#include <string.h>

/*
* 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;
}
15 changes: 15 additions & 0 deletions src/helpers/av_replace_placeholders.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#ifndef AV_HELPERS_REPLACE_PLACEHOLDERS_H
#define AV_HELPERS_REPLACE_PLACEHOLDERS_H

#include "av_structs.h"
#include <Zend/zend_types.h>

/*
* Substitutes the {field}, {value} and {expected} placeholders of an error
* message template with the corresponding field/value/type strings.
*
* See av_replace_placeholders.c for the substitution rules.
*/
zend_string *av_replace_placeholders(const char *template, size_t length, av_field *field, av_property_info *prop_info);

#endif /* AV_HELPERS_REPLACE_PLACEHOLDERS_H */
16 changes: 16 additions & 0 deletions src/helpers/av_wrappers.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "av_wrappers.h"
#include "php.h"
#include "Zend/zend_API.h"
#include "Zend/zend_interfaces.h"
#include "Zend/zend_list.h"
Expand Down Expand Up @@ -31,6 +32,16 @@ void av_efree(void *ptr)
efree(ptr);
}

zend_string *av_string_alloc(size_t length, bool persistent)
{
return zend_string_alloc(length, persistent);
}

zend_string *av_string_truncate(zend_string *s, size_t length, bool persistent)
{
return zend_string_truncate(s, length, persistent);
}

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);
Expand Down Expand Up @@ -84,3 +95,8 @@ void av_zval_ptr_dtor(zval *zval_ptr)
{
zval_ptr_dtor(zval_ptr);
}

const char *av_memnstr(const char *haystack, const char *needle, size_t needle_len, const char *end)
{
return php_memnstr(haystack, needle, needle_len, end);
}
5 changes: 5 additions & 0 deletions src/helpers/av_wrappers.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ 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);
zend_string *av_string_alloc(size_t length, bool persistent);
zend_string *av_string_truncate(zend_string *s, size_t length, bool persistent);

/* 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);
Expand All @@ -23,4 +25,7 @@ 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);

/* 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);

#endif /* AV_HELPERS_AV_WRAPPERS_H */
39 changes: 39 additions & 0 deletions tests/c/support/test_helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,42 @@ void efree_stub(void *ptr, int num_calls)
{
free(ptr);
}

zend_string *string_alloc_stub(size_t length, bool persistent, int num_calls)
{
zend_string *s = malloc(sizeof(zend_string) + length);
if (s) {
s->gc.refcount = 1;
s->gc.u.type_info = 0;
s->h = 0;
s->len = length;
s->val[length] = '\0';
}
return s;
}

zend_string *string_truncate_stub(zend_string *s, size_t length, bool persistent, int num_calls)
{
if (s) {
s->len = length;
s->val[length] = '\0';
}
return s;
}

const char *memnstr_stub(const char *haystack, const char *needle, size_t needle_len, const char *end, int num_calls)
{
(void)num_calls;
if (needle_len == 0)
return haystack;

size_t haystack_len = (size_t)(end - haystack);
if (haystack_len < needle_len)
return NULL;

for (const char *p = haystack; p <= end - needle_len; p++) {
if (memcmp(p, needle, needle_len) == 0)
return p;
}
return NULL;
}
3 changes: 3 additions & 0 deletions tests/c/support/test_helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@ extern zend_string *string_init_stub(const char *str, size_t len, bool persisten
extern void string_release_stub(zend_string *s, int num_calls);
extern void *emalloc_stub(size_t size, int num_calls);
extern void efree_stub(void *ptr, int num_calls);
extern zend_string *string_alloc_stub(size_t length, bool persistent, int num_calls);
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);

#endif /* TEST_HELPERS_H */
Loading
Loading