php-src/ext/intl/listformatter/listformatter_class.c
Gina Peter Banyard 6600d0e00f
ext/intl: Refactor error handling (#19196)
This is a comprehensive refactoring of the error mechanism of the Intl extension.

By moving the prefixing of the current method/function being executed to actual error message creation by accessing the execution context, we get the following benefits:
- Accurate error messages indicating *what* call caused the error
  - As we *always* "copy" the message, the `copyMsg` arg becomes unused, meaning we can reduce the size of the `intl_error` struct by 4 bytes.
  - Saving it as a zend_string means we know the length of the message
- Remove the need to pass around a "function name" `char*` across multiple calls
- Use Intl's exception mechanism to generate exceptions for constructor call
  - This removes the need for replacing the error handler
  - Which didn't do anything anyway in silent mode, which required throwing non-descriptive exceptions
2025-07-30 16:00:37 +01:00

241 lines
8 KiB
C

/*
+----------------------------------------------------------------------+
| This source file is subject to version 3.01 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available through the world-wide-web at the following url: |
| https://www.php.net/license/3_01.txt |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
| Authors: Bogdan Ungureanu <bogdanungureanu21@gmail.com> |
+----------------------------------------------------------------------+
*/
#include "php.h"
#include "php_intl.h"
#include <unicode/ulistformatter.h>
#include "listformatter_class.h"
#include "listformatter_arginfo.h"
#include "intl_convert.h"
static zend_object_handlers listformatter_handlers;
static void listformatter_free_obj(zend_object *object)
{
ListFormatter_object *obj = php_intl_listformatter_fetch_object(object);
if( obj->lf_data.ulistfmt )
ulistfmt_close( obj->lf_data.ulistfmt );
obj->lf_data.ulistfmt = NULL;
intl_error_reset( &obj->lf_data.error );
zend_object_std_dtor(&obj->zo);
}
static zend_object *listformatter_create_object(zend_class_entry *class_type)
{
ListFormatter_object *obj;
obj = zend_object_alloc(sizeof(ListFormatter_object), class_type);
obj->lf_data.ulistfmt = NULL;
intl_error_reset( &obj->lf_data.error );
zend_object_std_init(&obj->zo, class_type);
object_properties_init(&obj->zo, class_type);
obj->zo.handlers = &listformatter_handlers;
return &obj->zo;
}
PHP_METHOD(IntlListFormatter, __construct)
{
ListFormatter_object *obj = Z_INTL_LISTFORMATTER_P(ZEND_THIS);
char* locale;
size_t locale_len = 0;
#if U_ICU_VERSION_MAJOR_NUM >= 67
zend_long type = ULISTFMT_TYPE_AND;
zend_long width = ULISTFMT_WIDTH_WIDE;
#else
zend_long type = INTL_LISTFORMATTER_FALLBACK_TYPE_AND;
zend_long width = INTL_LISTFORMATTER_FALLBACK_WIDTH_WIDE;
#endif
ZEND_PARSE_PARAMETERS_START(1, 3)
Z_PARAM_STRING(locale, locale_len)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(type)
Z_PARAM_LONG(width)
ZEND_PARSE_PARAMETERS_END();
if(locale_len == 0) {
locale = (char *)intl_locale_get_default();
}
if (locale_len > INTL_MAX_LOCALE_LEN) {
zend_argument_value_error(1, "must be less than or equal to %d characters", INTL_MAX_LOCALE_LEN);
RETURN_THROWS();
}
if (strlen(uloc_getISO3Language(locale)) == 0) {
zend_argument_value_error(1, "\"%s\" is invalid", locale);
RETURN_THROWS();
}
UErrorCode status = U_ZERO_ERROR;
#if U_ICU_VERSION_MAJOR_NUM >= 67
if (type != ULISTFMT_TYPE_AND && type != ULISTFMT_TYPE_OR && type != ULISTFMT_TYPE_UNITS) {
zend_argument_value_error(2, "must be one of IntlListFormatter::TYPE_AND, IntlListFormatter::TYPE_OR, or IntlListFormatter::TYPE_UNITS");
RETURN_THROWS();
}
if (width != ULISTFMT_WIDTH_WIDE && width != ULISTFMT_WIDTH_SHORT && width != ULISTFMT_WIDTH_NARROW) {
zend_argument_value_error(3, "must be one of IntlListFormatter::WIDTH_WIDE, IntlListFormatter::WIDTH_SHORT, or IntlListFormatter::WIDTH_NARROW");
RETURN_THROWS();
}
LISTFORMATTER_OBJECT(obj) = ulistfmt_openForType(locale, type, width, &status);
#else
if (type != INTL_LISTFORMATTER_FALLBACK_TYPE_AND) {
zend_argument_value_error(2, "contains an unsupported type. ICU 66 and below only support IntlListFormatter::TYPE_AND");
RETURN_THROWS();
}
if (width != INTL_LISTFORMATTER_FALLBACK_WIDTH_WIDE) {
zend_argument_value_error(3, "contains an unsupported width. ICU 66 and below only support IntlListFormatter::WIDTH_WIDE");
RETURN_THROWS();
}
LISTFORMATTER_OBJECT(obj) = ulistfmt_open(locale, &status);
#endif
if (U_FAILURE(status)) {
intl_error_set(NULL, status, "Constructor failed");
zend_throw_exception(IntlException_ce_ptr, "Constructor failed", 0);
RETURN_THROWS();
}
}
PHP_METHOD(IntlListFormatter, format)
{
ListFormatter_object *obj = Z_INTL_LISTFORMATTER_P(ZEND_THIS);
HashTable *ht;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ARRAY_HT(ht)
ZEND_PARSE_PARAMETERS_END();
uint32_t count = zend_hash_num_elements(ht);
if (count == 0) {
RETURN_EMPTY_STRING();
}
const UChar **items = (const UChar **)safe_emalloc(count, sizeof(const UChar *), 0);
int32_t *itemLengths = (int32_t *)safe_emalloc(count, sizeof(int32_t), 0);
uint32_t i = 0;
zval *val;
ZEND_HASH_FOREACH_VAL(ht, val) {
zend_string *str_val, *tmp_str;
str_val = zval_get_tmp_string(val, &tmp_str);
// Convert PHP string to UTF-16
UChar *ustr = NULL;
int32_t ustr_len = 0;
UErrorCode status = U_ZERO_ERROR;
intl_convert_utf8_to_utf16(&ustr, &ustr_len, ZSTR_VAL(str_val), ZSTR_LEN(str_val), &status);
zend_tmp_string_release(tmp_str);
if (U_FAILURE(status)) {
// We can't use goto cleanup because items and itemLengths are incompletely allocated
for (uint32_t j = 0; j < i; j++) {
efree((void *)items[j]);
}
efree(items);
efree(itemLengths);
intl_error_set(NULL, status, "Failed to convert string to UTF-16");
RETURN_FALSE;
}
items[i] = ustr;
itemLengths[i] = ustr_len;
i++;
} ZEND_HASH_FOREACH_END();
UErrorCode status = U_ZERO_ERROR;
int32_t resultLength;
UChar *result = NULL;
resultLength = ulistfmt_format(LISTFORMATTER_OBJECT(obj), items, itemLengths, count, NULL, 0, &status);
if (U_FAILURE(status) && status != U_BUFFER_OVERFLOW_ERROR) {
intl_error_set(NULL, status, "Failed to format list");
RETVAL_FALSE;
goto cleanup;
}
// Allocate buffer and try again
status = U_ZERO_ERROR;
result = (UChar *)safe_emalloc(resultLength + 1, sizeof(UChar), 0);
ulistfmt_format(LISTFORMATTER_OBJECT(obj), items, itemLengths, count, result, resultLength, &status);
if (U_FAILURE(status)) {
if (result) {
efree(result);
}
intl_error_set(NULL, status, "Failed to format list");
RETVAL_FALSE;
goto cleanup;
}
// Convert result back to UTF-8
zend_string *ret = intl_convert_utf16_to_utf8(result, resultLength, &status);
efree(result);
if (!ret) {
intl_error_set(NULL, status, "Failed to convert result to UTF-8");
RETVAL_FALSE;
} else {
RETVAL_NEW_STR(ret);
}
cleanup:
for (i = 0; i < count; i++) {
efree((void *)items[i]);
}
efree(items);
efree(itemLengths);
}
PHP_METHOD(IntlListFormatter, getErrorCode)
{
ZEND_PARSE_PARAMETERS_NONE();
ListFormatter_object *obj = Z_INTL_LISTFORMATTER_P(ZEND_THIS);
UErrorCode status = intl_error_get_code(LISTFORMATTER_ERROR_P(obj));
RETURN_LONG(status);
}
PHP_METHOD(IntlListFormatter, getErrorMessage)
{
ZEND_PARSE_PARAMETERS_NONE();
ListFormatter_object *obj = Z_INTL_LISTFORMATTER_P(ZEND_THIS);
zend_string *message = intl_error_get_message(LISTFORMATTER_ERROR_P(obj));
RETURN_STR(message);
}
void listformatter_register_class(void)
{
zend_class_entry *class_entry = register_class_IntlListFormatter();
class_entry->create_object = listformatter_create_object;
memcpy(&listformatter_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
listformatter_handlers.offset = XtOffsetOf(ListFormatter_object, zo);
listformatter_handlers.free_obj = listformatter_free_obj;
listformatter_handlers.clone_obj = NULL;
}