[php-src] master: Merge branch '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:51:12+08:00

Commit: https://github.com/php/php-src/commit/1f2319e42cbd99727c403c01ec22614f2356fe38
Raw diff: https://github.com/php/php-src/commit/1f2319e42cbd99727c403c01ec22614f2356fe38.diff

Merge branch 'PHP-8.5'

* PHP-8.5:
  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.cpp


Diff:

diff --git a/NEWS b/NEWS
index 3db6bc520e84..9690787e59bf 100644
--- a/NEWS
+++ b/NEWS
@@ -34,6 +34,8 @@ PHP                                                                        NEWS
     the result without a terminating NUL. (iliaal)
   . 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)
 
 - Phar:
   . Fixed Phar archives being automatically detected when ".phar" only occurs
diff --git a/ext/intl/formatter/formatter_parse.cpp b/ext/intl/formatter/formatter_parse.cpp
index 40107229e0ae..7a233f012d46 100644
--- a/ext/intl/formatter/formatter_parse.cpp
+++ b/ext/intl/formatter/formatter_parse.cpp
@@ -18,6 +18,7 @@
 
 #include <unicode/fmtable.h>
 #include <unicode/curramt.h>
+#include <unicode/ustring.h>
 #include "../intl_convertcpp.h"
 #include "formatter_class.h"
 #include "formatter_format.h"
@@ -31,6 +32,42 @@ 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 )
 {
@@ -65,6 +102,9 @@ 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))) {
+		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 */
@@ -122,6 +162,7 @@ U_CFUNC PHP_FUNCTION( numfmt_parse )
 	}
 
 	if (zposition) {
+		position = numfmt_utf16_offset_to_utf8(ustr, position);
 		ZEND_TRY_ASSIGN_REF_LONG(zposition, position);
 	}
 
@@ -167,6 +208,9 @@ 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))) {
+			INTL_METHOD_CHECK_STATUS(nfo, "Invalid UTF-8 offset");
+		}
 	}
 
 	icu::ParsePosition pp(position);
@@ -178,7 +222,8 @@ U_CFUNC PHP_FUNCTION( numfmt_parse_currency )
 	}
 
 	if(zposition) {
-		ZEND_TRY_ASSIGN_REF_LONG(zposition, pp.getIndex());
+		position = numfmt_utf16_offset_to_utf8(ustr, pp.getIndex());
+		ZEND_TRY_ASSIGN_REF_LONG(zposition, position);
 	}
 
 	const double number = currAmt->getNumber().getDouble(INTL_DATA_ERROR_CODE(nfo));
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.