Skip to content

Commit

Permalink
Allow lossless integer to float conversions
Browse files Browse the repository at this point in the history
According to the specification, 64-bit signed integers should be
accepted providing they can be represented losslessly.

At the moment, integers to be converted to a float immediately return an
error. This change permits integers and only returns an error if they
would overflow the float type they're being converted to as they have no
problem being represented losslessly.

In addition, this change returns an error if a float is provided but
cannot fit within the float type specified by the struct.
  • Loading branch information
saracen committed Nov 10, 2021
1 parent e0af6a2 commit 5e9c7c7
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 0 deletions.
30 changes: 30 additions & 0 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ type Primitive struct {
context Key
}

// The significand precision for float32 and float64 is 24 and
// 53 bits respectfully. 2^prec-1 gives us the range an integer
// can be stored within a float without loss of data.
const (
maxSafeFloat32Int = 16777215 // 2^24-1
maxSafeFloat64Int = 9007199254740991 // 2^53-1
)

// PrimitiveDecode is just like the other `Decode*` functions, except it
// decodes a TOML value that has already been parsed. Valid primitive values
// can *only* be obtained from values filled by the decoder functions,
Expand Down Expand Up @@ -357,6 +365,9 @@ func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error {
if num, ok := data.(float64); ok {
switch rv.Kind() {
case reflect.Float32:
if num < -math.MaxFloat32 || num > math.MaxFloat32 {
return e("value %f is out of range for float32", num)
}
fallthrough
case reflect.Float64:
rv.SetFloat(num)
Expand All @@ -365,6 +376,25 @@ func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error {
}
return nil
}

if num, ok := data.(int64); ok {
switch rv.Kind() {
case reflect.Float32:
if num < -maxSafeFloat32Int || num > maxSafeFloat32Int {
return e("value %d is out of range for float32", num)
}
fallthrough
case reflect.Float64:
if num < -maxSafeFloat64Int || num > maxSafeFloat64Int {
return e("value %d is out of range for float64", num)
}
rv.SetFloat(float64(num))
default:
panic("bug")
}
return nil
}

return badtype("float", data)
}

Expand Down
48 changes: 48 additions & 0 deletions decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"io/ioutil"
"math"
"os"
"reflect"
"strings"
Expand Down Expand Up @@ -267,6 +268,53 @@ func TestDecodeIntOverflow(t *testing.T) {
}
}

func TestDecodeFloatOverflow(t *testing.T) {
type table struct {
F32 float32
F64 float64
}

tests := []struct {
value string
overflow bool
}{
{fmt.Sprintf(`F32 = %f`, math.MaxFloat64), true},
{fmt.Sprintf(`F32 = %f`, -math.MaxFloat64), true},
{fmt.Sprintf(`F32 = %f`, math.MaxFloat32), false},
{fmt.Sprintf(`F32 = %f`, -math.MaxFloat32), false},
{fmt.Sprintf(`F32 = %f`, math.MaxFloat32*1.1), true},
{fmt.Sprintf(`F32 = %f`, -math.MaxFloat32*1.1), true},

{fmt.Sprintf(`F32 = %d`, maxSafeFloat32Int), false},
{fmt.Sprintf(`F32 = %d`, -maxSafeFloat32Int), false},
{fmt.Sprintf(`F32 = %d`, maxSafeFloat32Int+1), true},
{fmt.Sprintf(`F32 = %d`, -maxSafeFloat32Int-1), true},

{fmt.Sprintf(`F64 = %f`, math.MaxFloat64), false},
{fmt.Sprintf(`F64 = %f`, -math.MaxFloat64), false},
{fmt.Sprintf(`F64 = %f`, math.MaxFloat32), false},
{fmt.Sprintf(`F64 = %f`, -math.MaxFloat32), false},

{fmt.Sprintf(`F64 = %d`, maxSafeFloat64Int), false},
{fmt.Sprintf(`F64 = %d`, -maxSafeFloat64Int), false},
{fmt.Sprintf(`F64 = %d`, maxSafeFloat64Int+1), true},
{fmt.Sprintf(`F64 = %d`, -maxSafeFloat64Int-1), true},
}

for _, tc := range tests {
var tab table
_, err := Decode(tc.value, &tab)

if tc.overflow && err == nil {
t.Fatalf("expected error for %q", tc.value)
}

if !tc.overflow && err != nil {
t.Fatalf("unexpected error for %q: %v", tc.value, err)
}
}
}

func TestDecodeSizedInts(t *testing.T) {
type table struct {
U8 uint8
Expand Down

0 comments on commit 5e9c7c7

Please sign in to comment.