fix(12-05): cast floats to strings with PHP's 14-digit precision in request validation
PHP 8's (string) cast of a float formats with the precision ini (14 significant digits, %.14G), not the shortest round-trip form: 1/3 is 0.33333333333333, 1e14 is 1.0E+14 and 5e-324 is 4.9406564584125E-324. phpFloatString, used for the string form of JSON floats in size, in, regex and integer checks, printed up to 17 digits and switched to the exponent form only from 1e15. TestPHPFloatStringMatchesPHPCast pins 27 values to php -r output.
This commit is contained in:
@@ -618,8 +618,11 @@ func phpScalarString(value any) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// phpFloatString formats a float as PHP 8 casts it to string: the shortest
|
||||
// round-trip digits, in exponent form (1.0E+15) from 1e15 up and below 1e-4.
|
||||
// phpFloatString formats a float as PHP 8 casts it to string: %.14G with
|
||||
// the default precision ini of 14 (zend_gcvt). The value is correctly
|
||||
// rounded to 14 significant digits and trailing zeros are dropped; it is
|
||||
// written in exponent form (1.0E+15, 1.5E-5) when the decimal point would
|
||||
// sit more than 14 digits right or more than 3 zeros left of the digits.
|
||||
func phpFloatString(f float64) string {
|
||||
switch {
|
||||
case math.IsNaN(f):
|
||||
@@ -634,21 +637,38 @@ func phpFloatString(f float64) string {
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
e := strconv.FormatFloat(f, 'e', -1, 64)
|
||||
const precision = 14
|
||||
e := strconv.FormatFloat(math.Abs(f), 'e', precision-1, 64)
|
||||
mant, expStr, _ := strings.Cut(e, "e")
|
||||
exp, _ := strconv.Atoi(expStr)
|
||||
if exp >= -4 && exp < 15 {
|
||||
return strconv.FormatFloat(f, 'f', -1, 64)
|
||||
digits := strings.TrimRight(strings.Replace(mant, ".", "", 1), "0")
|
||||
if digits == "" {
|
||||
digits = "0"
|
||||
}
|
||||
if !strings.Contains(mant, ".") {
|
||||
mant += ".0"
|
||||
decpt := exp + 1
|
||||
var out string
|
||||
switch {
|
||||
case decpt < -3 || decpt > precision:
|
||||
m := digits[:1] + ".0"
|
||||
if len(digits) > 1 {
|
||||
m = digits[:1] + "." + digits[1:]
|
||||
}
|
||||
sign := "+"
|
||||
if exp < 0 {
|
||||
sign = "-"
|
||||
exp = -exp
|
||||
sign, exp = "-", -exp
|
||||
}
|
||||
return mant + "E" + sign + strconv.Itoa(exp)
|
||||
out = m + "E" + sign + strconv.Itoa(exp)
|
||||
case decpt <= 0:
|
||||
out = "0." + strings.Repeat("0", -decpt) + digits
|
||||
case decpt >= len(digits):
|
||||
out = digits + strings.Repeat("0", decpt-len(digits))
|
||||
default:
|
||||
out = digits[:decpt] + "." + digits[decpt:]
|
||||
}
|
||||
if f < 0 {
|
||||
out = "-" + out
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// phpTrim trims the characters PHP's trim() does.
|
||||
|
||||
@@ -2,6 +2,7 @@ package lagoon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -209,3 +210,45 @@ func TestValidateRulesMatchLaravel(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPHPFloatStringMatchesPHPCast pins phpFloatString to PHP 8's (string)
|
||||
// cast of a float, which formats with the precision ini (14 significant
|
||||
// digits, %.14G): the wants are php -r '(string)$x' output.
|
||||
func TestPHPFloatStringMatchesPHPCast(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
in float64
|
||||
want string
|
||||
}{
|
||||
{0.1 + 0.2, "0.3"},
|
||||
{1e14, "1.0E+14"},
|
||||
{1e15, "1.0E+15"},
|
||||
{1e20, "1.0E+20"},
|
||||
{123456789012345.0, "1.2345678901234E+14"},
|
||||
{12345678901234.0, "12345678901234"},
|
||||
{1.5e-5, "1.5E-5"},
|
||||
{0.0001, "0.0001"},
|
||||
{0.00012345678901234567, "0.00012345678901235"},
|
||||
{-2.5, "-2.5"},
|
||||
{3.0, "3"},
|
||||
{1.0 / 3, "0.33333333333333"},
|
||||
{2.0 / 3, "0.66666666666667"},
|
||||
{-1e-10, "-1.0E-10"},
|
||||
{99999999999999.9, "1.0E+14"},
|
||||
{0.1, "0.1"},
|
||||
{999999999999999.0, "1.0E+15"},
|
||||
{5e-324, "4.9406564584125E-324"},
|
||||
{1.7976931348623157e308, "1.7976931348623E+308"},
|
||||
{100.0, "100"},
|
||||
{1234.5678, "1234.5678"},
|
||||
{-0.000099999, "-9.9999E-5"},
|
||||
{0, "0"},
|
||||
{math.Copysign(0, -1), "-0"},
|
||||
{math.Inf(1), "INF"},
|
||||
{math.Inf(-1), "-INF"},
|
||||
{math.NaN(), "NAN"},
|
||||
} {
|
||||
if got := phpFloatString(tc.in); got != tc.want {
|
||||
t.Errorf("phpFloatString(%v) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user