Commit 1f2319e42cb for php.net

commit 1f2319e42cbd99727c403c01ec22614f2356fe38
Merge: c621cbe27ff 8bc0b1e077c
Author: Weilin Du <weilindu@php.net>
Date:   Tue Aug 18 17:51:12 2026 +0800

    Merge branch 'PHP-8.5'

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

diff --cc NEWS
index 3db6bc520e8,79171a05fe1..9690787e59b
--- a/NEWS
+++ b/NEWS
@@@ -22,26 -12,18 +22,28 @@@ PH
    . Fixed a crash in DOMXPath when a php:function callback receives a nodeset
      and a later callback returns a node from another document. (iliaal)

 +- PDO_PGSQL:
 +  . Fixed several lazy fetch (PDO::ATTR_PREFETCH => 0) defects: an infinite
 +    loop when cleaning up a fetch left in a COPY, a use-after-free when a
 +    statement with emulated or disabled prepares is destroyed, a connection
 +    left busy for the next fetch, and rows delivered from a result another
 +    statement took over. (KentarouTakeda)
 +
  - Intl:
 +  . Fixed grapheme_strrev() treating UBRK_DONE as a byte index and leaving
 +    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)

 -- Opcache:
 -  . Fixed opcache.protect_memory race under ZTS. (realFlowControl)
 +- Phar:
 +  . Fixed Phar archives being automatically detected when ".phar" only occurs
 +    in a directory name or is not a filename extension in an included file's
 +    path. (Weilin Du)

  - Readline:
 -  . Fixed the interactive shell not waiting for the pager process to exit.
 -    (Weilin Du)
 +  . Fixed class constant completion in the interactive shell. (Weilin Du)

  - Zip:
    . Fixed bug GH-17787 (ZipArchive stream stops reading early when the archive
diff --cc ext/intl/formatter/formatter_parse.cpp
index 40107229e0a,f95696225e4..7a233f012d4
--- a/ext/intl/formatter/formatter_parse.cpp
+++ b/ext/intl/formatter/formatter_parse.cpp
@@@ -16,28 -16,65 +16,65 @@@
  #include <config.h>
  #endif

 -#include "php_intl.h"
 -
 +#include <unicode/fmtable.h>
 +#include <unicode/curramt.h>
+ #include <unicode/ustring.h>
 -#include <locale.h>
 -
 +#include "../intl_convertcpp.h"
  #include "formatter_class.h"
  #include "formatter_format.h"
 -#include "intl_convert.h"
 +
 +extern "C" {
 +#include "php_intl.h"
 +}
 +
 +#include <locale.h>
 +#include <memory>

  #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);
++	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 UChar *str, int32_t str_len, int32_t position)
++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_len) {
++	if (position < 0 || position > str.length()) {
+ 		return position;
+ 	}
+
 -	u_strToUTF8(NULL, 0, &utf8_position, str, position, &status);
++	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. */
 -PHP_FUNCTION( numfmt_parse )
 +U_CFUNC PHP_FUNCTION( numfmt_parse )
  {
  	zend_long type = FORMAT_TYPE_DOUBLE;
 -	UChar* sstr = NULL;
 -	int32_t sstr_len = 0;
  	char* str = NULL;
  	size_t str_len;
 -	int32_t val32, position = 0;
 -	int64_t val64;
 -	double val_double;
 -	int32_t* position_p = NULL;
 +	int32_t position = 0;
  	zval *zposition = NULL;
  	char *oldlocale;
  	FORMATTER_METHOD_INIT_VARS;
@@@ -62,9 -95,12 +99,12 @@@
  	FORMATTER_METHOD_FETCH_OBJECT;

  	/* Convert given string to UTF-16. */
 -	intl_convert_utf8_to_utf16(&sstr, &sstr_len, str, str_len, &INTL_DATA_ERROR_CODE(nfo));
 +	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))) {
 -		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 */
@@@ -122,6 -141,7 +162,7 @@@
  	}

  	if (zposition) {
 -		position = numfmt_utf16_offset_to_utf8(sstr, sstr_len, position);
++		position = numfmt_utf16_offset_to_utf8(ustr, position);
  		ZEND_TRY_ASSIGN_REF_LONG(zposition, position);
  	}

@@@ -156,39 -186,32 +197,43 @@@ U_CFUNC PHP_FUNCTION( numfmt_parse_curr
  	FORMATTER_METHOD_FETCH_OBJECT;

  	/* Convert given string to UTF-16. */
 -	intl_convert_utf8_to_utf16(&sstr, &sstr_len, str, str_len, &INTL_DATA_ERROR_CODE(nfo));
 +	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) {
 -		position = (int32_t) zval_get_long(zposition);
 +	if (zposition) {
 +		zend_long long_position = zval_get_long(zposition);
 +		if (UNEXPECTED(long_position < INT32_MIN || long_position > INT32_MAX)) {
 +			zend_argument_value_error(hasThis() ? 3 : 4, "must be between %d and %d", INT32_MIN, INT32_MAX);
 +			RETURN_THROWS();
 +		}
 +		position = (int32_t) long_position;
+ 		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));
 +	icu::ParsePosition pp(position);
 +	std::unique_ptr<icu::CurrencyAmount> currAmt(FORMATTER_OBJECT(nfo)->parseCurrency(ustr, pp));
 +
 +	if (currAmt == nullptr || pp.getErrorIndex() >= 0) {
 +		INTL_DATA_ERROR_CODE(nfo) = U_PARSE_ERROR;
 +		INTL_METHOD_CHECK_STATUS( nfo, "Number parsing failed" );
 +	}
 +
  	if(zposition) {
- 		ZEND_TRY_ASSIGN_REF_LONG(zposition, pp.getIndex());
 -		position = numfmt_utf16_offset_to_utf8(sstr, sstr_len, position);
++		position = numfmt_utf16_offset_to_utf8(ustr, pp.getIndex());
+ 		ZEND_TRY_ASSIGN_REF_LONG(zposition, position);
  	}
 -	if (sstr) {
 -		efree(sstr);
 -	}
 -	INTL_METHOD_CHECK_STATUS( nfo, "Number parsing failed" );
 +
 +	const double number = currAmt->getNumber().getDouble(INTL_DATA_ERROR_CODE(nfo));

  	/* Convert parsed currency to UTF-8 and pass it back to caller. */
 -	u8str = intl_convert_utf16_to_utf8(currency, u_strlen(currency), &INTL_DATA_ERROR_CODE(nfo));
 +	icu::UnicodeString ucurrency(currAmt->getISOCurrency());
 +
 +	zend_string *u8str = intl_charFromString(ucurrency, &INTL_DATA_ERROR_CODE(nfo));
  	INTL_METHOD_CHECK_STATUS( nfo, "Currency conversion to UTF-8 failed" );
 -	ZEND_TRY_ASSIGN_REF_NEW_STR(zcurrency, u8str);
 +	ZEND_TRY_ASSIGN_REF_STR(zcurrency, u8str);

  	RETVAL_DOUBLE( number );
  }