[php-src] PHP-8.5: Merge branch 'PHP-8.4' into PHP-8.5
| From: | Ilia Alshanetsky | 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)