diff --git a/NEWS b/NEWS index ecb4d105e917..ec6e0f599681 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,10 @@ PHP NEWS . Fixed bug GH-15375 (Nested "yield from" skips items after a valid() or next() call on the inner generator). (iliaal) +- Intl: + . Fixed bug GH-23094 (NumberFormatter parsing offsets use UTF-16 positions + for UTF-8 strings). (ColumbusLabs) + - Opcache: . Fixed opcache.protect_memory race under ZTS. (realFlowControl) diff --git a/ext/intl/formatter/formatter_parse.c b/ext/intl/formatter/formatter_parse.c index 993990065040..2c5ac3222f3d 100644 --- a/ext/intl/formatter/formatter_parse.c +++ b/ext/intl/formatter/formatter_parse.c @@ -27,6 +27,42 @@ #define ICU_LOCALE_BUG 1 +static bool numfmt_utf8_offset_to_utf16(const char *str, size_t str_len, int32_t *position, UErrorCode *status) +{ + int32_t utf16_position; + + if (*position < 0 || (size_t) *position > str_len) { + return true; + } + + *status = U_ZERO_ERROR; + u_strFromUTF8(NULL, 0, &utf16_position, str, *position, status); + if (*status != U_BUFFER_OVERFLOW_ERROR && U_FAILURE(*status)) { + return false; + } + *status = U_ZERO_ERROR; + + *position = utf16_position; + return true; +} + +static int32_t numfmt_utf16_offset_to_utf8(const UChar *str, int32_t str_len, int32_t position) +{ + int32_t utf8_position; + UErrorCode status = U_ZERO_ERROR; + + if (position < 0 || position > str_len) { + return position; + } + + u_strToUTF8(NULL, 0, &utf8_position, str, position, &status); + if (status != U_BUFFER_OVERFLOW_ERROR && U_FAILURE(status)) { + return position; + } + + return utf8_position; +} + /* {{{ Parse a number. */ PHP_FUNCTION( numfmt_parse ) { @@ -61,6 +97,10 @@ PHP_FUNCTION( numfmt_parse ) /* Convert given string to UTF-16. */ intl_convert_utf8_to_utf16(&sstr, &sstr_len, str, str_len, &INTL_DATA_ERROR_CODE(nfo)); INTL_METHOD_CHECK_STATUS( nfo, "String conversion to UTF-16 failed" ); + if (zposition && !numfmt_utf8_offset_to_utf16(str, str_len, &position, &INTL_DATA_ERROR_CODE(nfo))) { + efree(sstr); + INTL_METHOD_CHECK_STATUS(nfo, "Invalid UTF-8 offset"); + } #if ICU_LOCALE_BUG && defined(LC_NUMERIC) /* need to copy here since setlocale may change it later */ @@ -101,6 +141,7 @@ PHP_FUNCTION( numfmt_parse ) } if (zposition) { + position = numfmt_utf16_offset_to_utf8(sstr, sstr_len, position); ZEND_TRY_ASSIGN_REF_LONG(zposition, position); } @@ -150,11 +191,16 @@ PHP_FUNCTION( numfmt_parse_currency ) if(zposition) { position = (int32_t) zval_get_long(zposition); + if (!numfmt_utf8_offset_to_utf16(str, str_len, &position, &INTL_DATA_ERROR_CODE(nfo))) { + efree(sstr); + INTL_METHOD_CHECK_STATUS(nfo, "Invalid UTF-8 offset"); + } position_p = &position; } number = unum_parseDoubleCurrency(FORMATTER_OBJECT(nfo), sstr, sstr_len, position_p, currency, &INTL_DATA_ERROR_CODE(nfo)); if(zposition) { + position = numfmt_utf16_offset_to_utf8(sstr, sstr_len, position); ZEND_TRY_ASSIGN_REF_LONG(zposition, position); } if (sstr) { diff --git a/ext/intl/tests/gh23094.phpt b/ext/intl/tests/gh23094.phpt new file mode 100644 index 000000000000..9ace16fa481f --- /dev/null +++ b/ext/intl/tests/gh23094.phpt @@ -0,0 +1,47 @@ +--TEST-- +GH-23094 NumberFormatter parse offsets use UTF-8 byte positions +--EXTENSIONS-- +intl +--FILE-- +parse($prefix . '123', NumberFormatter::TYPE_INT32, $offset)); +var_dump($offset); + +$offset = 1; +var_dump($formatter->parse("\u{00E9}123", NumberFormatter::TYPE_INT32, $offset)); +var_dump($offset); +var_dump(intl_is_failure($formatter->getErrorCode())); + +$formatter = new NumberFormatter('en_US', NumberFormatter::CURRENCY); +$offset = strlen($prefix); +$currency = null; +var_dump($formatter->parseCurrency($prefix . '$123.45', $currency, $offset)); +var_dump($currency); +var_dump($offset); + +$offset = 1; +$currency = null; +var_dump($formatter->parseCurrency("\u{00E9}$123.45", $currency, $offset)); +var_dump($currency); +var_dump($offset); +var_dump(intl_is_failure($formatter->getErrorCode())); + +?> +--EXPECT-- +int(123) +int(7) +bool(false) +int(1) +bool(true) +float(123.45) +string(3) "USD" +int(11) +bool(false) +NULL +int(1) +bool(true)