Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Bug #46408: Fix double formatting for PostgreSQL bound parameters #186

Closed
wants to merge 1 commit into from

5 participants

@asmecher

See https://bugs.php.net/bug.php?id=46408 for extensive discussion of this.

ext/pgsql/pgsql.c
@@ -1736,7 +1737,15 @@ static void _php_pgsql_free_params(char **params, int num_params)
} else {
zval tmp_val = **tmp;
zval_copy_ctor(&tmp_val);
+
+ // PSQL requires . for radix; convert to string,
@reeze
reeze added a note

please use '/* */' instead of '//'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@lstrojny

While this fix sure does what it needs to do to address this problem, I’m not sure we should work around this design flaw on every single occasion. Additionally the bugfix misses a test to make sure we don’t get a regression at some point.

ext/pgsql/pgsql.c
@@ -1736,7 +1737,13 @@ static void _php_pgsql_free_params(char **params, int num_params)
} else {
zval tmp_val = **tmp;
zval_copy_ctor(&tmp_val);
+
+ /* PSQL requires . for radix; convert to string, avoiding problems with doubles
+ and locales using , as a radix character instead (see bug #46408) */
+ char *current_locale = setlocale(LC_NUMERIC, "C");

As we use C89/C90, declarations may not be mixed with code. Please move the declaration to the beginning of the block.

Additionally: are you certain this works on any platform we support?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@asmecher

Thanks for the hints, Lars -- I've moved the var declaration to the head of the block, added #ifdef checks to make sure HAVE_SETLOCALE is respected, and added a regression test (ext/pgsql/tests/bug46408.phpt). I haven't contributed code to PHP before so your scrutiny is appreciated.

@asmecher

As for whether or not this problem should be worked around in PHP, it's necessary either to do it there or to do it in my own PHP scripts (and for everyone else who needs both setlocale and pg_query_params). I agree that it's not ideal but it does rehabilitate that function; I wouldn't know what to suggest as a broader strategy.

Note that the test will require the hr_HR locale to be generated on the test system (see /etc/locale.gen).

@lstrojny

Alright, digged a little deeper into this issue and here is my first analysis. The code flow basically goes like this

#define convert_to_string(zval *op) (defined in Zend/zend_operators.h)
    void _convert_to_string(zval *op ZEND_FILE_LINE_DC)  (defined in Zend/zend_operators.c)
        zend_spprintf(&Z_STRVAL_P(op), 0, "%.*G", (int) EG(precision), dval) (which is assigned in Zend/zend.c)
            zend_vspprintf = utility_functions->vspprintf_function (which is actually assigned in main/main.c)
                zuf.vspprintf_function = vspprintf (which is defined in main/spprintf.c)
                    PHPAPI int vspprintf(char **pbuf, size_t max_len, const char *format, va_list ap) (defined in main/spprintf.c)
                        static void xbuf_format_converter(smart_str *xbuf, const char *fmt, va_list ap) (defined in main/spprintf.c)

case 'G' defines the behavior for %.*G. If I read the code correctly, '%.*H' wouldn’t be locale sensitive.

This means: we need something like _convert_to_string_but_ignore_locale_settings() (no, this is not a serious function name proposal) which does the same as _convert_to_string() but uses '%.*H' for floats.

@asmecher

Thanks for the feedback, Lars -- I've introduced convert_to_string_unlocalized and used it instead. I've attempted to keep code duplication to a minimum -- personally, I would have preferred to introduce an optional parameter to the existing _convert_to_string but I suspect that would not follow PHP coding conventions.

@lstrojny

What about convert_to_cstring (like in c locale?)

Btw: regarding locale, you could also try to iterate over a few variants (de_DE e.g. also uses "," as a decimal separator).

@asmecher

Agreed; changed.

@lstrojny

Just so that you know, I've written a mail to internals@php.net to get more people into this discussion: http://marc.info/?l=php-internals&m=134807980716416&w=2

@asmecher

Thanks, Lars. This has been helpful and informative for me and I hope it results in an accepted fix.

@asmecher

Unfortunately that round on the php-internals list wasn't too encouraging. What would you suggest for next steps? I'd like to see a general fix applied, but the idea didn't seem popular. Alternately, I could alter this to just affect pg_query_params, but that won't help maintainability going forward.

ext/pgsql/pgsql.c
@@ -23,6 +23,7 @@
/* $Id$ */
#include <stdlib.h>
+#include <locale.h>
@smalyshev Owner

why you need locale.h?

Yep, that's no longer needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@lstrojny

Could you squash the commits. I will apply your fix for the PHP 5.5 branch.

@asmecher

Done. Thanks for all your help, Lars.

@php-pulls
Collaborator

Comment on behalf of lstrojny at php.net:

Merged into 5.5 and master. Thanks for your contribution and your patience!

@php-pulls php-pulls closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
18 Zend/zend_operators.c
@@ -572,6 +572,24 @@ ZEND_API void convert_to_boolean(zval *op) /* {{{ */
}
/* }}} */
+ZEND_API void _convert_to_cstring(zval *op ZEND_FILE_LINE_DC) /* {{{ */
+{
+ double dval;
+ switch (Z_TYPE_P(op)) {
+ case IS_DOUBLE: {
+ TSRMLS_FETCH();
+ dval = Z_DVAL_P(op);
+ Z_STRLEN_P(op) = zend_spprintf(&Z_STRVAL_P(op), 0, "%.*H", (int) EG(precision), dval);
+ /* %H already handles removing trailing zeros from the fractional part, yay */
+ break;
+ }
+ default:
+ return _convert_to_string(op);
+ }
+ Z_TYPE_P(op) = IS_STRING;
+}
+/* }}} */
+
ZEND_API void _convert_to_string(zval *op ZEND_FILE_LINE_DC) /* {{{ */
{
long lval;
View
2  Zend/zend_operators.h
@@ -301,6 +301,7 @@ ZEND_API int increment_function(zval *op1);
ZEND_API int decrement_function(zval *op2);
ZEND_API void convert_scalar_to_number(zval *op TSRMLS_DC);
+ZEND_API void _convert_to_cstring(zval *op ZEND_FILE_LINE_DC);
ZEND_API void _convert_to_string(zval *op ZEND_FILE_LINE_DC);
ZEND_API void convert_to_long(zval *op);
ZEND_API void convert_to_double(zval *op);
@@ -314,6 +315,7 @@ ZEND_API void multi_convert_to_double_ex(int argc, ...);
ZEND_API void multi_convert_to_string_ex(int argc, ...);
ZEND_API int add_char_to_string(zval *result, const zval *op1, const zval *op2);
ZEND_API int add_string_to_string(zval *result, const zval *op1, const zval *op2);
+#define convert_to_cstring(op) if ((op)->type != IS_STRING) { _convert_to_cstring((op) ZEND_FILE_LINE_CC); }
#define convert_to_string(op) if ((op)->type != IS_STRING) { _convert_to_string((op) ZEND_FILE_LINE_CC); }
ZEND_API double zend_string_to_double(const char *number, zend_uint length);
View
2  ext/pgsql/pgsql.c
@@ -1736,7 +1736,7 @@ PHP_FUNCTION(pg_query_params)
} else {
zval tmp_val = **tmp;
zval_copy_ctor(&tmp_val);
- convert_to_string(&tmp_val);
+ convert_to_cstring(&tmp_val);
if (Z_TYPE(tmp_val) != IS_STRING) {
php_error_docref(NULL TSRMLS_CC, E_WARNING,"Error converting parameter");
zval_dtor(&tmp_val);
View
21 ext/pgsql/tests/bug46408.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Bug #46408 (Locale number format settings can cause pg_query_params to break with numerics)
+--SKIPIF--
+<?php
+require_once('skipif.inc');
+?>
+--FILE--
+<?php
+
+require_once('config.inc');
+
+$dbh = pg_connect($conn_str);
+setlocale(LC_ALL, 'hr_HR.utf-8', 'hr_HR');
+pg_query_params("SELECT $1::numeric", array(3.5));
+pg_close($dbh);
+
+echo "Done".PHP_EOL;
+
+?>
+--EXPECTF--
+Done
Something went wrong with that request. Please try again.