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

Weilin Du <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Weilin Du (LamentXU123)
Date: 2026-08-18T17:47:59+08:00

Commit: https://github.com/php/php-src/commit/8bc0b1e077cb0957ed512a294d0053962afd4357
Raw diff: https://github.com/php/php-src/commit/8bc0b1e077cb0957ed512a294d0053962afd4357.diff

Merge branch 'PHP-8.4' into PHP-8.5

* PHP-8.4:
  Fix GH-23094: Use byte offsets in NumberFormatter parsing (#23318)

Changed paths:
  A  ext/intl/tests/gh23094.phpt
  M  NEWS
  M  ext/intl/formatter/formatter_parse.c


Diff:

diff --git a/NEWS b/NEWS
index 081461b539ed..79171a05fe1b 100644
--- a/NEWS
+++ b/NEWS
@@ -15,6 +15,8 @@ PHP                                                                        NEWS
 - Intl:
   . Fixed a double-free when IntlGregorianCalendar construction fails after
     the ICU constructor adopts the TimeZone. (iliaal)
+  . 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 ba8307419b4c..f95696225e43 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--
+<?php
+
+$prefix = "\u{1F600}";
+
+$formatter = new NumberFormatter('en_US', NumberFormatter::DECIMAL);
+$offset = strlen($prefix);
+var_dump($formatter->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)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.