]> granicus.if.org Git - php/commitdiff
Make float to string casts locale-independent
authorMáté Kocsis <kocsismate@woohoolabs.com>
Sat, 29 Feb 2020 21:47:04 +0000 (22:47 +0100)
committerMáté Kocsis <kocsismate@woohoolabs.com>
Fri, 8 May 2020 08:52:23 +0000 (10:52 +0200)
From now on, float to string casting will always behave locale-independently.
RFC: https://wiki.php.net/rfc/locale_independent_float_to_string
Closes GH-5224

Co-authored-by: George Peter Banyard <girgias@php.net>
15 files changed:
UPGRADING
Zend/zend_operators.c
Zend/zend_operators.h
ext/intl/tests/bug67052-win32.phpt
ext/intl/tests/bug67052.phpt
ext/json/tests/bug41403.phpt
ext/pdo/pdo_stmt.c
ext/pgsql/pgsql.c
ext/soap/tests/bugs/bug39815.phpt
ext/standard/tests/strings/locale_independent_float_to_string.phpt [new file with mode: 0644]
ext/standard/var.c
main/main.c
main/php.h
tests/basic/consistent_float_string_casts.phpt [new file with mode: 0644]
tests/lang/034.phpt

index 84b345ff9cf228e6bccfc8ea16dc5981499eddcd..5380b1058d58ba8abe184ad8f8ebf0110d92dfe6 100644 (file)
--- a/UPGRADING
+++ b/UPGRADING
@@ -189,6 +189,8 @@ PHP 8.0 UPGRADE NOTES
     array, resource or non-overloaded object. The only exception to this is the
     array + array merge operation, which remains supported.
     RFC: https://wiki.php.net/rfc/arithmetic_operator_type_checks
+  . Float to string casting will now always behave locale-independently.
+    RFC: https://wiki.php.net/rfc/locale_independent_float_to_string
 
 - COM:
   . Removed the ability to import case-insensitive constants from type
index 012e95d3b2593f9be0d6237015dcb469a1469e33..562a0f6d5418c4d5299d1b5ca1e7abeebe7a4a60 100644 (file)
@@ -605,20 +605,6 @@ try_again:
 }
 /* }}} */
 
-ZEND_API void ZEND_FASTCALL _convert_to_cstring(zval *op) /* {{{ */
-{
-       if (Z_TYPE_P(op) == IS_DOUBLE) {
-               zend_string *str;
-               double dval = Z_DVAL_P(op);
-
-               str = zend_strpprintf_unchecked(0, "%.*H", (int) EG(precision), dval);
-               ZVAL_NEW_STR(op, str);
-       } else {
-               _convert_to_string(op);
-       }
-}
-/* }}} */
-
 ZEND_API void ZEND_FASTCALL _convert_to_string(zval *op) /* {{{ */
 {
 try_again:
@@ -648,8 +634,8 @@ try_again:
                        zend_string *str;
                        double dval = Z_DVAL_P(op);
 
-                       str = zend_strpprintf(0, "%.*G", (int) EG(precision), dval);
-                       /* %G already handles removing trailing zeros from the fractional part, yay */
+                       str = zend_strpprintf_unchecked(0, "%.*H", (int) EG(precision), dval);
+
                        ZVAL_NEW_STR(op, str);
                        break;
                }
@@ -953,7 +939,7 @@ try_again:
                        return zend_long_to_str(Z_LVAL_P(op));
                }
                case IS_DOUBLE: {
-                       return zend_strpprintf(0, "%.*G", (int) EG(precision), Z_DVAL_P(op));
+                       return zend_strpprintf_unchecked(0, "%.*H", (int) EG(precision), Z_DVAL_P(op));
                }
                case IS_ARRAY:
                        zend_error(E_WARNING, "Array to string conversion");
index 13f236bbaa0ff641f531c8cb89cabbaf9deb0a5a..2c3766fd3896510bbb72e5236ef86e1f33a75b34 100644 (file)
@@ -257,7 +257,6 @@ ZEND_API int ZEND_FASTCALL increment_function(zval *op1);
 ZEND_API int ZEND_FASTCALL decrement_function(zval *op2);
 
 ZEND_API void ZEND_FASTCALL convert_scalar_to_number(zval *op);
-ZEND_API void ZEND_FASTCALL _convert_to_cstring(zval *op);
 ZEND_API void ZEND_FASTCALL _convert_to_string(zval *op);
 ZEND_API void ZEND_FASTCALL convert_to_long(zval *op);
 ZEND_API void ZEND_FASTCALL convert_to_double(zval *op);
@@ -340,7 +339,6 @@ static zend_always_inline zend_bool try_convert_to_string(zval *op) {
 #define _zval_get_double_func(op) zval_get_double_func(op)
 #define _zval_get_string_func(op) zval_get_string_func(op)
 
-#define convert_to_cstring(op) if (Z_TYPE_P(op) != IS_STRING) { _convert_to_cstring((op)); }
 #define convert_to_string(op) if (Z_TYPE_P(op) != IS_STRING) { _convert_to_string((op)); }
 
 
index 2c276245622812b9214ef75aa0d3676943be9694..060a98b9fb342c59ed213acf0d62729058c9bc56 100644 (file)
@@ -25,5 +25,5 @@ ut_run();
 
 ?>
 --EXPECT--
-1234567,891
+1234567.891
 de-de
index 6cdfd9f5a9401dd3cfaf48e8902f9d6bf2207b8e..56d825aa5d34826e3b7f5bdddde3c700059fe2cc 100644 (file)
@@ -30,5 +30,5 @@ ut_run();
 
 ?>
 --EXPECT--
-1234567,891
+1234567.891
 de_DE.UTF-8
index 685c8318387ffae3652aac1e28effca97d6dce7b..91b4de7ead4e8f1b012599b2a120e195c1e04dc9 100644 (file)
@@ -23,15 +23,15 @@ echo "Done\n";
 --EXPECT--
 array(1) {
   [0]=>
-  float(2,1)
+  float(2.1)
 }
 array(1) {
   [0]=>
-  float(0,15)
+  float(0.15)
 }
 array(1) {
   [0]=>
-  float(123,13452345)
+  float(123.13452345)
 }
 array(2) {
   [0]=>
index 42cc447608e24c578d123b8a31df2a19be8dd439..b095425cafd7afd117825d2571ff6b1a7219913d 100644 (file)
@@ -230,15 +230,8 @@ static int really_register_bound_param(struct pdo_bound_param_data *param, pdo_s
        }
 
        if (PDO_PARAM_TYPE(param->param_type) == PDO_PARAM_STR && param->max_value_len <= 0 && !Z_ISNULL_P(parameter)) {
-               if (Z_TYPE_P(parameter) == IS_DOUBLE) {
-                       char *p;
-                       int len = zend_spprintf_unchecked(&p, 0, "%.*H", (int) EG(precision), Z_DVAL_P(parameter));
-                       ZVAL_STRINGL(parameter, p, len);
-                       efree(p);
-               } else {
-                       if (!try_convert_to_string(parameter)) {
-                               return 0;
-                       }
+               if (!try_convert_to_string(parameter)) {
+                       return 0;
                }
        } else if (PDO_PARAM_TYPE(param->param_type) == PDO_PARAM_INT && (Z_TYPE_P(parameter) == IS_FALSE || Z_TYPE_P(parameter) == IS_TRUE)) {
                convert_to_long(parameter);
index 9450bf0f611beef44783bb9d84d7798ff1192ddf..3a4d6fd578a5cc4dab38137b2692a00c6686f5f5 100644 (file)
@@ -1987,7 +1987,7 @@ PHP_FUNCTION(pg_query_params)
                                zval tmp_val;
 
                                ZVAL_COPY(&tmp_val, tmp);
-                               convert_to_cstring(&tmp_val);
+                               convert_to_string(&tmp_val);
                                if (Z_TYPE(tmp_val) != IS_STRING) {
                                        php_error_docref(NULL, E_WARNING,"Error converting parameter");
                                        zval_ptr_dtor(&tmp_val);
index 57b429ec432db16e11dce9158789d2331c9092b3..e8786ada5acbfe7aa95aae7e5df517eb6844fbb1 100644 (file)
@@ -41,7 +41,7 @@ setlocale(LC_ALL,"en_US","en_US.ISO8859-1");
 var_dump($x->test());
 echo $x->__getLastResponse();
 --EXPECT--
-float(123,456)
+float(123.456)
 <?xml version="1.0" encoding="UTF-8"?>
 <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://testuri.org" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:testResponse><return xsi:type="xsd:float">123.456</return></ns1:testResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>
 float(123.456)
diff --git a/ext/standard/tests/strings/locale_independent_float_to_string.phpt b/ext/standard/tests/strings/locale_independent_float_to_string.phpt
new file mode 100644 (file)
index 0000000..634d2ce
--- /dev/null
@@ -0,0 +1,107 @@
+--TEST--
+Test that floats are converted to string locale independently
+--SKIPIF--
+<?php
+
+if (!setlocale
+    (LC_ALL,
+    "german", "de", "de_DE", "de_DE.ISO8859-1", "de_DE.ISO_8859-1", "de_DE.UTF-8",
+    "french", "fr", "fr_FR", "fr_FR.ISO8859-1", "fr_FR.ISO_8859-1", "fr_FR.UTF-8",
+    )) {
+    die("skip - locale needed for this test is not supported on this platform");
+}
+
+if (!extension_loaded("json")) {
+    print "skip - test requires the json extension";
+}
+
+?>
+--FILE--
+<?php
+
+function print_float(float $f)
+{
+    echo "- casting:\n";
+    echo $f . "\n";
+    echo strval($f) . "\n";
+    $g = $f;
+    settype($g, "string");
+    echo $g . "\n";
+
+    echo "- *printf functions:\n";
+    printf("%.2f\n", $f);
+    printf("%.2F\n", $f);
+    echo sprintf("%.2f", $f) . "\n";
+    echo sprintf("%.2F", $f) . "\n";
+
+    echo "- export/import:\n";
+    echo var_export($f, true)  . "\n";
+    echo serialize($f) . "\n";
+    echo json_encode($f) . "\n";
+
+    echo "- debugging:\n";
+    echo print_r($f, true) . "\n";
+    var_dump($f);
+    debug_zval_dump($f);
+
+    echo "- other:\n";
+    echo implode([$f]) . "\n";
+}
+
+setlocale(LC_ALL, "C");
+echo "C locale:\n";
+
+print_float(3.14);
+
+setlocale(
+    LC_ALL,
+    "german", "de", "de_DE", "de_DE.ISO8859-1", "de_DE.ISO_8859-1", "de_DE.UTF-8",
+    "french", "fr", "fr_FR", "fr_FR.ISO8859-1", "fr_FR.ISO_8859-1", "fr_FR.UTF-8",
+);
+echo "\nde_DE locale:\n";
+
+print_float(3.14);
+
+?>
+--EXPECT--
+C locale:
+- casting:
+3.14
+3.14
+3.14
+- *printf functions:
+3.14
+3.14
+3.14
+3.14
+- export/import:
+3.14
+d:3.14;
+3.14
+- debugging:
+3.14
+float(3.14)
+float(3.14)
+- other:
+3.14
+
+de_DE locale:
+- casting:
+3.14
+3.14
+3.14
+- *printf functions:
+3,14
+3.14
+3,14
+3.14
+- export/import:
+3.14
+d:3.14;
+3.14
+- debugging:
+3.14
+float(3.14)
+float(3.14)
+- other:
+3.14
index 609acf449e1fa0118571d3d6afc77cf052ce15ab..5b4fd8fe54fd7786a3641635089ca2577b8573e2 100644 (file)
@@ -114,7 +114,7 @@ again:
                        php_printf("%sint(" ZEND_LONG_FMT ")\n", COMMON, Z_LVAL_P(struc));
                        break;
                case IS_DOUBLE:
-                       php_printf("%sfloat(%.*G)\n", COMMON, (int) PG(serialize_precision), Z_DVAL_P(struc));
+                       php_printf_unchecked("%sfloat(%.*H)\n", COMMON, (int) PG(serialize_precision), Z_DVAL_P(struc));
                        break;
                case IS_STRING:
                        php_printf("%sstring(%zd) \"", COMMON, Z_STRLEN_P(struc));
@@ -295,7 +295,7 @@ again:
                php_printf("%sint(" ZEND_LONG_FMT ")\n", COMMON, Z_LVAL_P(struc));
                break;
        case IS_DOUBLE:
-               php_printf("%sfloat(%.*G)\n", COMMON, (int) PG(serialize_precision), Z_DVAL_P(struc));
+               php_printf_unchecked("%sfloat(%.*H)\n", COMMON, (int) PG(serialize_precision), Z_DVAL_P(struc));
                break;
        case IS_STRING:
                php_printf("%sstring(%zd) \"", COMMON, Z_STRLEN_P(struc));
index 98aaa5beceb241e6dd3c4e7d4cff16ed7cf3b8f9..eef57c690fba787a608e5c30f767c0da8e946152 100644 (file)
@@ -912,6 +912,25 @@ PHPAPI size_t php_printf(const char *format, ...)
 }
 /* }}} */
 
+/* {{{ php_printf_unchecked
+ */
+PHPAPI size_t php_printf_unchecked(const char *format, ...)
+{
+       va_list args;
+       size_t ret;
+       char *buffer;
+       size_t size;
+
+       va_start(args, format);
+       size = vspprintf(&buffer, 0, format, args);
+       ret = PHPWRITE(buffer, size);
+       efree(buffer);
+       va_end(args);
+
+       return ret;
+}
+/* }}} */
+
 static zend_string *escape_html(const char *buffer, size_t buffer_len) {
        zend_string *result = php_escape_html_entities_ex(
                (const unsigned char *) buffer, buffer_len, 0, ENT_COMPAT,
index a20b4a3bfc551d324b6bc927d6bccc8281abd477..87f7d000f9fde90c49d0c83128b72919f817afb7 100644 (file)
@@ -309,8 +309,8 @@ ssize_t pread(int, void *, size_t, off64_t);
 BEGIN_EXTERN_C()
 void phperror(char *error);
 PHPAPI size_t php_write(void *buf, size_t size);
-PHPAPI size_t php_printf(const char *format, ...) PHP_ATTRIBUTE_FORMAT(printf, 1,
-               2);
+PHPAPI size_t php_printf(const char *format, ...) PHP_ATTRIBUTE_FORMAT(printf, 1, 2);
+PHPAPI size_t php_printf_unchecked(const char *format, ...);
 PHPAPI int php_get_module_initialized(void);
 #ifdef HAVE_SYSLOG_H
 #include "php_syslog.h"
diff --git a/tests/basic/consistent_float_string_casts.phpt b/tests/basic/consistent_float_string_casts.phpt
new file mode 100644 (file)
index 0000000..adbc1b2
--- /dev/null
@@ -0,0 +1,30 @@
+--TEST--
+Test that float to string and string to float casts are consistent
+--SKIPIF--
+<?php
+if (!setlocale(
+    LC_ALL,
+    "german", "de", "de_DE", "de_DE.ISO8859-1", "de_DE.ISO_8859-1", "de_DE.UTF-8",
+    "french", "fr", "fr_FR", "fr_FR.ISO8859-1", "fr_FR.ISO_8859-1", "fr_FR.UTF-8",
+)) {
+    die("skip locale needed for this test is not supported on this platform");
+}
+?>
+--FILE--
+<?php
+
+setlocale(
+    LC_ALL,
+    "german", "de", "de_DE", "de_DE.ISO8859-1", "de_DE.ISO_8859-1", "de_DE.UTF-8",
+    "french", "fr", "fr_FR", "fr_FR.ISO8859-1", "fr_FR.ISO_8859-1", "fr_FR.UTF-8",
+);
+
+$float = 1/3;
+$string = (string) $float;
+$float = (float) $string;
+
+printf("%.2f", $float);
+
+?>
+--EXPECT--
+0,33
index 38ecc0b4515632979cb670b636036a08418770b7..2ed6bce9d7b8b54f25876e3782dbe1213fbf9ea3 100644 (file)
@@ -16,4 +16,4 @@ setlocale(LC_NUMERIC, "de_DE.UTF-8", "de_DE", "de", "german", "ge", "de_DE.ISO-8
 echo (float)"3.14", "\n";
 ?>
 --EXPECT--
-3,14
+3.14