[php-src] master: Merge branch 'PHP-8.6'
| From: | Ilia Alshanetsky | Date: | Mon, 05 Oct 2026 13:13:53 +0000 |
| Subject: | [php-src] master: Merge branch 'PHP-8.6' | ||
| Groups: | php.cvs | ||
| Request: | Send a blank email to php-cvs+get-141963@lists.php.net to get a copy of this message | ||
Author: Ilia Alshanetsky (iliaal)
Date: 2026-10-05T09:06:26-04:00
Commit: https://github.com/php/php-src/commit/6fa256a2d17deb14698da6570aad78270774a9f2
Raw diff: https://github.com/php/php-src/commit/6fa256a2d17deb14698da6570aad78270774a9f2.diff
Merge branch 'PHP-8.6'
* PHP-8.6:
ext/intl: Use byte offsets in IntlDateFormatter parsing
Changed paths:
A ext/intl/tests/dateformat_parse_utf8_offset.phpt
M ext/intl/dateformat/dateformat_parse.cpp
M ext/intl/formatter/formatter_parse.cpp
M ext/intl/intl_convert.c
M ext/intl/intl_convert.h
Diff:
diff --git a/ext/intl/dateformat/dateformat_parse.cpp b/ext/intl/dateformat/dateformat_parse.cpp
index 0537b42feec5..8a554ae404de 100644
--- a/ext/intl/dateformat/dateformat_parse.cpp
+++ b/ext/intl/dateformat/dateformat_parse.cpp
@@ -43,10 +43,19 @@ static void internal_parse_to_timestamp(IntlDateFormatter_object *dfo, const cha
/* 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);
}
@@ -54,6 +63,9 @@ static void internal_parse_to_timestamp(IntlDateFormatter_object *dfo, const cha
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);
}
@@ -97,9 +109,18 @@ static void internal_parse_to_localtime(IntlDateFormatter_object *dfo, const cha
/* 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.cpp b/ext/intl/formatter/formatter_parse.cpp
index 7a233f012d46..db7463030b7a 100644
--- a/ext/intl/formatter/formatter_parse.cpp
+++ b/ext/intl/formatter/formatter_parse.cpp
@@ -25,6 +25,7 @@
extern "C" {
#include "php_intl.h"
+#include "intl_convert.h"
}
#include <locale.h>
@@ -32,42 +33,6 @@ extern "C" {
#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(nullptr, 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 icu::UnicodeString &str, int32_t position)
-{
- int32_t utf8_position;
- UErrorCode status = U_ZERO_ERROR;
-
- if (position < 0 || position > str.length()) {
- return position;
- }
-
- u_strToUTF8(nullptr, 0, &utf8_position, str.getBuffer(), position, &status);
- if (status != U_BUFFER_OVERFLOW_ERROR && U_FAILURE(status)) {
- return position;
- }
-
- return utf8_position;
-}
-
/* {{{ Parse a number. */
U_CFUNC PHP_FUNCTION( numfmt_parse )
{
@@ -102,7 +67,7 @@ U_CFUNC PHP_FUNCTION( numfmt_parse )
icu::UnicodeString ustr;
intl_stringFromChar(ustr, 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))) {
INTL_METHOD_CHECK_STATUS(nfo, "Invalid UTF-8 offset");
}
@@ -162,7 +127,7 @@ U_CFUNC PHP_FUNCTION( numfmt_parse )
}
if (zposition) {
- position = numfmt_utf16_offset_to_utf8(ustr, position);
+ position = intl_convert_utf16_offset_to_utf8(ustr.getBuffer(), ustr.length(), position);
ZEND_TRY_ASSIGN_REF_LONG(zposition, position);
}
@@ -208,7 +173,7 @@ U_CFUNC PHP_FUNCTION( numfmt_parse_currency )
RETURN_THROWS();
}
position = (int32_t) long_position;
- 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))) {
INTL_METHOD_CHECK_STATUS(nfo, "Invalid UTF-8 offset");
}
}
@@ -222,7 +187,7 @@ U_CFUNC PHP_FUNCTION( numfmt_parse_currency )
}
if(zposition) {
- position = numfmt_utf16_offset_to_utf8(ustr, pp.getIndex());
+ position = intl_convert_utf16_offset_to_utf8(ustr.getBuffer(), ustr.length(), pp.getIndex());
ZEND_TRY_ASSIGN_REF_LONG(zposition, position);
}
diff --git a/ext/intl/intl_convert.c b/ext/intl/intl_convert.c
index 5497e84508b4..5b97c354262c 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 443d373d55c2..f33746b00c92 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)