fix(engine): render whole-number floats as TOML integers

Helm values round-trip through JSON, so every YAML number enters the
template engine as a float64 regardless of whether the source was written
as "9" or "9.0". The BurntSushi TOML encoder inspects the Go type via
reflection and writes float64(9) as "9.0", which surprises users whose
values look like integers. encoding/json already omits the trailing ".0"
for whole-number float64 values, so toJson does not exhibit the same
issue; this change brings toToml in line with toJson.

The fix normalizes whole-number float64 values to int64 inside toToml
only, leaving NaN, Inf, non-whole floats, and out-of-range values
untouched. Scoping the change to the encoding path preserves the
in-template type of values returned by typeOf/typeIs, avoiding the
ecosystem regression that caused #13533 to be reverted in #30884.

Closes #32035

Signed-off-by: barry3406 <duan3406@gmail.com>
pull/32040/head
barry3406 3 months ago
parent bd061e0cd7
commit 0c8ab13223

@ -20,6 +20,7 @@ import (
"bytes"
"encoding/json"
"maps"
"math"
"strings"
"text/template"
@ -157,7 +158,7 @@ func fromYAMLArray(str string) []any {
func toTOML(v any) string {
b := bytes.NewBuffer(nil)
e := toml.NewEncoder(b)
err := e.Encode(v)
err := e.Encode(normalizeForTOML(v))
if err != nil {
return err.Error()
}
@ -172,13 +173,56 @@ func toTOML(v any) string {
func mustToTOML(v any) string {
b := bytes.NewBuffer(nil)
e := toml.NewEncoder(b)
err := e.Encode(v)
err := e.Encode(normalizeForTOML(v))
if err != nil {
panic(err)
}
return b.String()
}
// normalizeForTOML walks v and rewrites any float64 that is a whole number
// (within the int64 range) as an int64. Helm values round-trip through JSON,
// so every YAML number arrives in the template engine as a float64 regardless
// of whether the source was written as "9" or "9.0"; the BurntSushi TOML
// encoder then writes float64(9) as "9.0", which surprises users. This brings
// toToml in line with encoding/json (which already drops trailing ".0" for
// whole-number float64). Non-whole floats and values outside the int64 range
// are left untouched so that true floats still round-trip as floats.
//
// This fix is intentionally scoped to the TOML encoding path. #13533 solved
// the same issue by switching the values loader to json.Decoder.UseNumber,
// which changed every numeric value to json.Number globally and broke charts
// relying on typeOf/typeIs returning "float64" (#30880). Normalizing only
// inside toTOML preserves the in-template type of values so that regression
// cannot recur.
func normalizeForTOML(v any) any {
switch x := v.(type) {
case map[string]any:
out := make(map[string]any, len(x))
for k, val := range x {
out[k] = normalizeForTOML(val)
}
return out
case []any:
out := make([]any, len(x))
for i, val := range x {
out[i] = normalizeForTOML(val)
}
return out
case float64:
// Guard against NaN/Inf and against Go's implementation-defined
// behavior for out-of-range float-to-int conversions. 1<<63 is the
// first positive float64 strictly greater than math.MaxInt64.
if math.IsNaN(x) || math.IsInf(x, 0) || x != math.Trunc(x) ||
x >= 1<<63 || x < -(1<<63) {
return x
}
return int64(x)
default:
return v
}
}
// fromTOML converts a TOML document into a map[string]interface{}.
//
// This is not a general-purpose TOML parser, and will not parse all valid

@ -86,6 +86,32 @@ keyInElement1 = "valueInElement1"`,
tpl: `{{ toToml . }}`,
expect: "[mast]\n sail = \"white\"\n",
vars: map[string]map[string]string{"mast": {"sail": "white"}},
}, {
// Regression for https://github.com/helm/helm/issues/32035
// Helm values round-trip through JSON so YAML integers arrive as
// float64. toToml must still emit them as TOML integers, matching
// what toJson already does. Fractional floats must be preserved.
tpl: `{{ toToml . }}`,
expect: "bar = 9\nbaz = 9.5\n\n[[items]]\n id = 1\n\n" +
"[[items]]\n id = 2\n\n[nested]\n deep = 42\n",
vars: map[string]any{
"bar": float64(9),
"baz": float64(9.5),
"nested": map[string]any{
"deep": float64(42),
},
"items": []any{
map[string]any{"id": float64(1)},
map[string]any{"id": float64(2)},
},
},
}, {
// Regression for https://github.com/helm/helm/issues/32035
// Non-finite floats and floats outside the int64 range must stay
// floats so that true floats still round-trip as floats.
tpl: `{{ toToml . }}`,
expect: "big = 1e+20\n",
vars: map[string]any{"big": float64(1e20)},
}, {
tpl: `{{ fromYaml . }}`,
expect: "map[Error:error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go value of type map[string]interface {}]",

Loading…
Cancel
Save