Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 46 additions & 1 deletion ext/intl/formatter/formatter_parse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 )
{
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
Expand All @@ -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));
Expand Down
47 changes: 47 additions & 0 deletions ext/intl/tests/gh23094.phpt
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 2 additions & 1 deletion ext/standard/array.c
Original file line number Diff line number Diff line change
Expand Up @@ -4037,12 +4037,13 @@ PHPAPI int php_array_merge_recursive(HashTable *dest, HashTable *src) /* {{{ */
GC_TRY_UNPROTECT_RECURSION(thash);
}
if (!ret) {
zval_ptr_dtor(&tmp);
return 0;
}
} else {
Z_TRY_ADDREF_P(src_zval);
zval *zv = zend_hash_next_index_insert(Z_ARRVAL_P(dest_zval), src_zval);
if (EXPECTED(!zv)) {
if (UNEXPECTED(!zv)) {
Z_TRY_DELREF_P(src_zval);
zend_cannot_add_element();
return 0;
Expand Down
55 changes: 55 additions & 0 deletions ext/standard/tests/array/array_merge_recursive_object_leak.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
--TEST--
array_merge_recursive() must not leak the array converted from an object when the merge below it fails
--FILE--
<?php

$dest = [];
$dest['k'] = &$dest;
try {
array_merge_recursive($dest, ['k' => (object) ['k' => 1]]);
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}

/* Control: same failing exit, array source, nothing to release. */
$control = [];
$control['k'] = &$control;
try {
array_merge_recursive($control, ['k' => ['k' => 1]]);
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}

/* Several nested levels convert an object before the failure unwinds through them. */
$ring = [[], [], []];
for ($i = 0; $i < 3; $i++) {
$ring[$i]['k'] = &$ring[($i + 1) % 3];
}
$src = (object) ['k' => 1];
for ($i = 1; $i < 3; $i++) {
$src = (object) ['k' => $src];
}
try {
array_merge_recursive($ring[0], ['k' => $src]);
} catch (\Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}

/* The successful path still releases it exactly once. */
$ok = ['k' => ['a']];
var_dump(array_merge_recursive($ok, ['k' => (object) ['b']]));

?>
--EXPECT--
Error: Recursion detected
Error: Recursion detected
Error: Recursion detected
array(1) {
["k"]=>
array(2) {
[0]=>
string(1) "a"
[1]=>
string(1) "b"
}
}