Skip to content

Commit f41ae52

Browse files
liamcervanteapparentlymart
authored andcommitted
msgpack: Never marshal integers as MessagePack float
Previously we were using MessagePack's float family for anything that could convert losslessly to float64. Unfortunately, cty's interpretation of numbers uses binary floating point internally but decimal strings as the presentation, including when numbers are converted to strings, and so we need to make some assumptions about precision when we're choosing a string representation of a number, to avoid confusing situations where slight inaccuracy in the binary interpretation of a number presented in decimal would appear as unwanted extra digits in the string representation. Slight differences caused by precision handling are acceptable for the fractional parts of numbers, but callers expect integers to be presented exactly as originally given. Round-tripping a large integer through a float64, even if the binary representation is exact, causes us to end up with a rounded string representation after decoding, because the decoded number has 52-bit precision instead of the 512 we use when dealing with string representations of large integers. Therefore we'll now use MessagePack float encodings only for numbers that have a fractional component. If an integer cannot fit in any of MessagePack's integer encodings then we'll encode it as a string containing decimal digits instead, so that it'll decode back to a number with the same precision and thus the same decimal string representation.
1 parent 0e3c880 commit f41ae52

File tree

3 files changed

+120
-1
lines changed

3 files changed

+120
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# 1.14.4 (Unreleased)
22

3+
* `msgpack`: Now uses string encoding instead of float encoding for a whole number that is too large to fit in any of MessagePack's integer types.
34

45
# 1.14.3 (February 29, 2024)
56

cty/msgpack/marshal.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func marshal(val cty.Value, ty cty.Type, path cty.Path, enc *msgpack.Encoder) er
8989
bf := val.AsBigFloat()
9090
if iv, acc := bf.Int64(); acc == big.Exact {
9191
err = enc.EncodeInt(iv)
92-
} else if fv, acc := bf.Float64(); acc == big.Exact {
92+
} else if fv, acc := bf.Float64(); acc == big.Exact && !bf.IsInt() {
9393
err = enc.EncodeFloat64(fv)
9494
} else {
9595
err = enc.EncodeString(bf.Text('f', -1))

cty/msgpack/roundtrip_test.go

+118
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"testing"
77

88
"github.com/zclconf/go-cty/cty"
9+
"github.com/zclconf/go-cty/cty/convert"
910
)
1011

1112
func TestRoundTrip(t *testing.T) {
@@ -96,6 +97,10 @@ func TestRoundTrip(t *testing.T) {
9697
cty.MustParseNumberVal("9223372036854775809"),
9798
cty.Number,
9899
},
100+
{
101+
cty.MustParseNumberVal("18446744073709551616"),
102+
cty.Number,
103+
},
99104
{
100105
awkwardFractionVal,
101106
cty.Number,
@@ -363,6 +368,119 @@ func TestRoundTrip(t *testing.T) {
363368
}
364369
}
365370

371+
func TestRoundTrip_fromString(t *testing.T) {
372+
tests := []struct {
373+
Value string
374+
Type cty.Type
375+
}{
376+
{
377+
"0",
378+
cty.Number,
379+
},
380+
{
381+
"1",
382+
cty.Number,
383+
},
384+
{
385+
"-1",
386+
cty.Number,
387+
},
388+
{
389+
"9223372036854775807",
390+
cty.Number,
391+
},
392+
{
393+
"9223372036854775808",
394+
cty.Number,
395+
},
396+
{
397+
"9223372036854775809",
398+
cty.Number,
399+
},
400+
{
401+
"18446744073709551616",
402+
cty.Number,
403+
},
404+
{
405+
"-9223372036854775807",
406+
cty.Number,
407+
},
408+
{
409+
"-9223372036854775808",
410+
cty.Number,
411+
},
412+
{
413+
"-9223372036854775809",
414+
cty.Number,
415+
},
416+
{
417+
"-18446744073709551616",
418+
cty.Number,
419+
},
420+
{
421+
"true",
422+
cty.Bool,
423+
},
424+
{
425+
"false",
426+
cty.Bool,
427+
},
428+
}
429+
for _, test := range tests {
430+
t.Run(fmt.Sprintf("%#v as %#v", test.Value, test.Type), func(t *testing.T) {
431+
stringVal := cty.StringVal(test.Value)
432+
433+
original, err := convert.Convert(stringVal, test.Type)
434+
if err != nil {
435+
t.Fatalf("input type must be convertible from string: %s", err)
436+
}
437+
438+
{
439+
// We'll first make sure that the conversion works even without
440+
// MessagePack involved, since otherwise we might falsely blame
441+
// the MessagePack encoding for bugs in package convert.
442+
stringGot, err := convert.Convert(original, cty.String)
443+
if err != nil {
444+
t.Fatalf("result must be convertible to string: %s", err)
445+
}
446+
447+
if !stringGot.RawEquals(stringVal) {
448+
t.Fatalf("value did not round-trip to string even without msgpack\ninput: %#v\nresult: %#v", test.Value, stringGot)
449+
}
450+
}
451+
452+
b, err := Marshal(original, test.Type)
453+
if err != nil {
454+
t.Fatal(err)
455+
}
456+
457+
t.Logf("encoded as %x", b)
458+
459+
got, err := Unmarshal(b, test.Type)
460+
if err != nil {
461+
t.Fatal(err)
462+
}
463+
464+
if !got.RawEquals(original) {
465+
t.Errorf(
466+
"value did not round-trip\ninput: %#v\nresult: %#v",
467+
test.Value, got,
468+
)
469+
}
470+
471+
stringGot, err := convert.Convert(got, cty.String)
472+
if err != nil {
473+
t.Fatalf("result must be convertible to string: %s", err)
474+
}
475+
476+
if !stringGot.RawEquals(stringVal) {
477+
t.Errorf("value did not round-trip to string\ninput: %#v\nresult: %#v", test.Value, stringGot)
478+
}
479+
480+
})
481+
}
482+
}
483+
366484
// Unknown values with very long string prefix refinements do not round-trip
367485
// losslessly. If the prefix is longer than 256 bytes it will be truncated to
368486
// a maximum of 256 bytes.

0 commit comments

Comments
 (0)