[php-src] PHP-8.5: Merge branch 'PHP-8.4' into PHP-8.5

From: Date: Mon, 05 Oct 2026 13:13:53 +0000
Subject: [php-src] PHP-8.5: Merge branch 'PHP-8.4' into PHP-8.5
Groups: php.cvs 
Request: Send a blank email to php-cvs+get-141961@lists.php.net to get a copy of this message
Author: Ilia Alshanetsky (iliaal) Date: 2026-10-05T08:59:43-04:00 Commit: https://github.com/php/php-src/commit/906356642d8f67f34817db6a9b2198ebaf83e230 Raw diff: https://github.com/php/php-src/commit/906356642d8f67f34817db6a9b2198ebaf83e230.diff Merge branch 'PHP-8.4' into PHP-8.5 * PHP-8.4: ext/intl: Use byte offsets in IntlDateFormatter parsing Changed paths: A ext/intl/tests/dateformat_parse_utf8_offset.phpt M NEWS M ext/intl/dateformat/dateformat_parse.c M ext/intl/formatter/formatter_parse.c M ext/intl/intl_convert.c M ext/intl/intl_convert.h Diff: diff --git a/NEWS b/NEWS index 6d537ffe99b3..4d949ee74209 100644 --- a/NEWS +++ b/NEWS @@ -77,6 +77,9 @@ PHP NEWS haystacks. (Weilin Du) . Fixed grapheme_strrpos() and grapheme_strripos() skipping overlapping matches when using a negative offset. (Weilin Du) + . Fixed IntlDateFormatter::parse(), localtime() and parseToCalendar() + treating the offset as UTF-16 code units instead of bytes. + (Ilia Alshanetsky) - Lexbor: . Merge patches lexbor/lexbor@8a14bc0 and lexbor/lexbor@f67ce4b, fixing a diff --git a/ext/intl/dateformat/dateformat_parse.c b/ext/intl/dateformat/dateformat_parse.c index b6e9f7c92eb6..cf80599d9202 100644 --- a/ext/intl/dateformat/dateformat_parse.c +++ b/ext/intl/dateformat/dateformat_parse.c @@ -41,10 +41,19 @@ static void internal_parse_to_timestamp(IntlDateFormatter_object *dfo, char* tex /* Convert timezone to UTF-16. */ intl_convert_utf8_to_utf16(&text_utf16, &text_utf16_len, text_to_parse, text_len, &INTL_DATA_ERROR_CODE(dfo)); INTL_METHOD_CHECK_STATUS(dfo, "Error converting timezone to UTF-16" ); + if (parse_pos && !intl_convert_utf8_offset_to_utf16(text_to_parse, text_len, parse_pos, &INTL_DATA_ERROR_CODE(dfo))) { + if (text_utf16) { + efree(text_utf16); + } + INTL_METHOD_CHECK_STATUS(dfo, "Invalid UTF-8 offset" ); + } if (UNEXPECTED(update_calendar)) { UCalendar *parsed_calendar = (UCalendar *)udat_getCalendar(DATE_FORMAT_OBJECT(dfo)); udat_parseCalendar(DATE_FORMAT_OBJECT(dfo), parsed_calendar, text_utf16, text_utf16_len, parse_pos, &INTL_DATA_ERROR_CODE(dfo)); + if (parse_pos) { + *parse_pos = intl_convert_utf16_offset_to_utf8(text_utf16, text_utf16_len, *parse_pos); + } if (text_utf16) { efree(text_utf16); } @@ -52,6 +61,9 @@ static void internal_parse_to_timestamp(IntlDateFormatter_object *dfo, char* tex timestamp = ucal_getMillis( parsed_calendar, &INTL_DATA_ERROR_CODE(dfo)); } else { timestamp = udat_parse(DATE_FORMAT_OBJECT(dfo), text_utf16, text_utf16_len, parse_pos, &INTL_DATA_ERROR_CODE(dfo)); + if (parse_pos) { + *parse_pos = intl_convert_utf16_offset_to_utf8(text_utf16, text_utf16_len, *parse_pos); + } if (text_utf16) { efree(text_utf16); } @@ -95,9 +107,18 @@ static void internal_parse_to_localtime(IntlDateFormatter_object *dfo, char* tex /* Convert timezone to UTF-16. */ intl_convert_utf8_to_utf16(&text_utf16, &text_utf16_len, text_to_parse, text_len, &INTL_DATA_ERROR_CODE(dfo)); INTL_METHOD_CHECK_STATUS(dfo, "Error converting timezone to UTF-16" ); + if (parse_pos && !intl_convert_utf8_offset_to_utf16(text_to_parse, text_len, parse_pos, &INTL_DATA_ERROR_CODE(dfo))) { + if (text_utf16) { + efree(text_utf16); + } + INTL_METHOD_CHECK_STATUS(dfo, "Invalid UTF-8 offset" ); + } parsed_calendar = (UCalendar *)udat_getCalendar(DATE_FORMAT_OBJECT(dfo)); udat_parseCalendar( DATE_FORMAT_OBJECT(dfo), parsed_calendar, text_utf16, text_utf16_len, parse_pos, &INTL_DATA_ERROR_CODE(dfo)); + if (parse_pos) { + *parse_pos = intl_convert_utf16_offset_to_utf8(text_utf16, text_utf16_len, *parse_pos); + } if (text_utf16) { efree(text_utf16); diff --git a/ext/intl/formatter/formatter_parse.c b/ext/intl/formatter/formatter_parse.c index f95696225e43..2b53ea33a680 100644 --- a/ext/intl/formatter/formatter_parse.c +++ b/ext/intl/formatter/formatter_parse.c @@ -27,42 +27,6 @@ #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 ) { @@ -97,7 +61,7 @@ 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))) { + if (zposition && !intl_convert_utf8_offset_to_utf16(str, str_len, &position, &INTL_DATA_ERROR_CODE(nfo))) { efree(sstr); INTL_METHOD_CHECK_STATUS(nfo, "Invalid UTF-8 offset"); } @@ -141,7 +105,7 @@ PHP_FUNCTION( numfmt_parse ) } if (zposition) { - position = numfmt_utf16_offset_to_utf8(sstr, sstr_len, position); + position = intl_convert_utf16_offset_to_utf8(sstr, sstr_len, position); ZEND_TRY_ASSIGN_REF_LONG(zposition, position); } @@ -191,7 +155,7 @@ 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))) { + if (!intl_convert_utf8_offset_to_utf16(str, str_len, &position, &INTL_DATA_ERROR_CODE(nfo))) { efree(sstr); INTL_METHOD_CHECK_STATUS(nfo, "Invalid UTF-8 offset"); } @@ -200,7 +164,7 @@ PHP_FUNCTION( numfmt_parse_currency ) 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); + position = intl_convert_utf16_offset_to_utf8(sstr, sstr_len, position); ZEND_TRY_ASSIGN_REF_LONG(zposition, position); } if (sstr) { diff --git a/ext/intl/intl_convert.c b/ext/intl/intl_convert.c index 39f70821e117..d813a7b33a19 100644 --- a/ext/intl/intl_convert.c +++ b/ext/intl/intl_convert.c @@ -22,6 +22,8 @@ #include "intl_common.h" #include "intl_convert.h" +#include <unicode/utf16.h> + /* {{{ intl_convert_utf8_to_utf16 * Convert given string from UTF-8 to UTF-16 to *target buffer. * @@ -198,3 +200,43 @@ zend_string* intl_convert_utf16_to_utf8( return dst; } /* }}} */ + +bool intl_convert_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; +} + +int32_t intl_convert_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; + } + + if (position > 0 && position < str_len && U16_IS_LEAD(str[position - 1]) && U16_IS_TRAIL(str[position])) { + position--; + } + + u_strToUTF8(NULL, 0, &utf8_position, str, position, &status); + if (status != U_BUFFER_OVERFLOW_ERROR && U_FAILURE(status)) { + return position; + } + + return utf8_position; +} diff --git a/ext/intl/intl_convert.h b/ext/intl/intl_convert.h index 5cc3a671333d..6a87806e556a 100644 --- a/ext/intl/intl_convert.h +++ b/ext/intl/intl_convert.h @@ -31,4 +31,12 @@ zend_string* intl_convert_utf16_to_utf8( const UChar* src, int32_t src_len, UErrorCode* status ); +bool intl_convert_utf8_offset_to_utf16( + const char* str, size_t str_len, + int32_t* position, UErrorCode* status ); + +int32_t intl_convert_utf16_offset_to_utf8( + const UChar* str, int32_t str_len, + int32_t position ); + #endif // INTL_CONVERT_H diff --git a/ext/intl/tests/dateformat_parse_utf8_offset.phpt b/ext/intl/tests/dateformat_parse_utf8_offset.phpt new file mode 100644 index 000000000000..aa80a39d8099 --- /dev/null +++ b/ext/intl/tests/dateformat_parse_utf8_offset.phpt @@ -0,0 +1,68 @@ +--TEST-- +IntlDateFormatter parsing uses UTF-8 byte offsets +--EXTENSIONS-- +intl +--FILE-- +<?php +$formatter = new IntlDateFormatter('en_US', IntlDateFormatter::NONE, IntlDateFormatter::NONE, 'UTC', IntlDateFormatter::GREGORIAN, 'yyyy-MM-dd'); +$prefix = "\u{1F600}"; +$text = $prefix . '2017-10-12 tail'; + +echo "parse():\n"; +$offset = strlen($prefix); +var_dump(gmdate('Y-m-d', $formatter->parse($text, $offset)), $offset); + +echo "localtime():\n"; +$offset = strlen($prefix); +$tm = $formatter->localtime($text, $offset); +var_dump($tm['tm_year'] + 1900, $tm['tm_mon'] + 1, $tm['tm_mday'], $offset); + +echo "parseToCalendar():\n"; +$offset = strlen($prefix); +var_dump(gmdate('Y-m-d', $formatter->parseToCalendar($text, $offset)), $offset); + +echo "failed parse:\n"; +$offset = strlen($prefix); +var_dump($formatter->parse($prefix . '2017-xx-12', $offset), $offset); + +echo "offset inside a character:\n"; +$offset = 1; +var_dump($formatter->parse($text, $offset), $offset, intl_get_error_message()); + +echo "partial match inside a surrogate pair:\n"; +$formatter = new IntlDateFormatter('en_US', IntlDateFormatter::NONE, IntlDateFormatter::NONE, 'UTC', IntlDateFormatter::GREGORIAN, "\u{20AC}\u{1F600}yyyy-MM-dd"); +$offset = 0; +var_dump($formatter->parse("\u{20AC}\u{1F601}2017-10-12", $offset), $offset); +$formatter = new IntlDateFormatter('en_US', IntlDateFormatter::NONE, IntlDateFormatter::NONE, 'UTC', IntlDateFormatter::GREGORIAN, "yyyy-MM-dd\u{1F600}"); +foreach (['parse', 'localtime', 'parseToCalendar'] as $method) { + $offset = 0; + $formatter->$method("2017-10-12\u{1F601}", $offset); + echo "$method(): "; + var_dump($offset); +} +?> +--EXPECT-- +parse(): +string(10) "2017-10-12" +int(14) +localtime(): +int(2017) +int(10) +int(12) +int(14) +parseToCalendar(): +string(10) "2017-10-12" +int(14) +failed parse: +bool(false) +int(9) +offset inside a character: +bool(false) +int(1) +string(70) "IntlDateFormatter::parse(): Invalid UTF-8 offset: U_INVALID_CHAR_FOUND" +partial match inside a surrogate pair: +bool(false) +int(3) +parse(): int(10) +localtime(): int(10) +parseToCalendar(): int(10)

« previous php.cvs (#141961) next »