diff --git a/.github/workflows/include.yml b/.github/workflows/include.yml index b540a10..a1a5cd5 100644 --- a/.github/workflows/include.yml +++ b/.github/workflows/include.yml @@ -11,7 +11,7 @@ jobs: call-gochecks: uses: fortio/workflows/.github/workflows/gochecks.yml@main call-codecov: - uses: fortio/workflows/.github/workflows/codecov.yml@arm_coverage + uses: fortio/workflows/.github/workflows/codecov.yml@main secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} call-codeql: diff --git a/.gitignore b/.gitignore index 6f72f89..c61662c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,11 @@ go.work.sum # env file .env +# Apple stuff +.DS_Store +# Local linter run +.golangci.yml +# Grol save state +.gr +# vscode +tasks.json diff --git a/README.md b/README.md index d7ee7d3..ac52e0a 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,6 @@ This is usable from any go with generics (1.18 or later) though our CI uses the `safecast` is about avoiding [gosec G115](https://github.com/securego/gosec#available-rules) and [CWE-190: Integer Overflow or Wraparound](https://cwe.mitre.org/data/definitions/190.html) class of overflow and loss of precision bugs, extended to float64/float32 issues. -Credit for the idea (and a finding a bug in the first implementation) goes to [@ccoVeille](https://github.com/ccoVeille), Please see https://github.com/ccoVeille/go-safecast for an different style API and implementation to pick whichever fits your style best. +Credit for the idea (and a finding a bug in the first implementation) goes to [@ccoVeille](https://github.com/ccoVeille), Please see https://github.com/ccoVeille/go-safecast for an different style API and implementation to pick whichever fits your style best (though I believe this implementation and API is better both in simplicity of principle (round trip check) and performance and api surface). Please note that conversions from integer to float are suffering from CPU architecture differences and issues at the "edge" (max int) which are handled by Convert but you should use the new int only Conv/MustConv if possible, to avoid them. diff --git a/safecast.go b/safecast.go index 7754af3..cf5a34d 100644 --- a/safecast.go +++ b/safecast.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "math" + "reflect" "unsafe" ) @@ -28,7 +29,18 @@ type Number interface { var ErrOutOfRange = errors.New("out of range") -const all63bits = uint64(math.MaxInt64) +const ( + all63bits = uint64(math.MaxInt64) + all31bits = uint64(math.MaxInt32) +) + +func isFloat[Num Number](f Num) (isFloat bool) { + switch reflect.TypeOf(f).Kind() { //nolint:exhaustive // only 2 we want to check + case reflect.Float32, reflect.Float64: + isFloat = true + } + return +} // Convert converts a number from one type to another, // returning an error if the conversion would result in a loss of precision, @@ -43,16 +55,10 @@ const all63bits = uint64(math.MaxInt64) // error from say ~float32 to float64 while it shouldn't). func Convert[NumOut Number, NumIn Number](orig NumIn) (converted NumOut, err error) { origPositive := (orig >= 0) - // All bits set on uint64 or positive int63 are two of 3 special cases not detected by roundtrip (afaik). - if origPositive && (uint64(orig)&all63bits == all63bits) { - // If we started from float we don't have to special case these bits (handles +Inf case too) - switch any(orig).(type) { - case float32, float64: - break - default: - err = ErrOutOfRange - return - } + // All bits set on uint64 or positive int63 are two of 4 special cases not detected by roundtrip (afaik). + if origPositive && (uint64(orig)&all63bits == all63bits) && !isFloat(orig) { + err = ErrOutOfRange + return } converted = NumOut(orig) if origPositive != (converted >= 0) { @@ -63,8 +69,8 @@ func Convert[NumOut Number, NumIn Number](orig NumIn) (converted NumOut, err err err = ErrOutOfRange return } - // And this is the 3rd weird case, maxint32 conversion to float32. - if origPositive && (uint64(orig) == uint64(math.MaxInt32)) && unsafe.Sizeof(converted) == 4 { + // And this are the other 2 weird case, maxint32 and maxuint32 (on armhf) conversion to float32. + if origPositive && (uint64(orig)&all31bits == all31bits) && unsafe.Sizeof(converted) == 4 && !isFloat(orig) { err = ErrOutOfRange } return diff --git a/safecast_test.go b/safecast_test.go index 13bd1bc..2bea458 100644 --- a/safecast_test.go +++ b/safecast_test.go @@ -303,9 +303,11 @@ func TestNaNOk(t *testing.T) { } } -// Note this won't work is ~float because of the switch type. +type myFloat float64 + +// Also tests ~float (#19). func TestPlusInfiniteOk(t *testing.T) { - inf64 := math.Inf(1) + inf64 := myFloat(math.Inf(1)) inf32, err := safecast.Convert[float32](inf64) if err != nil { t.Errorf("unexpected 64->32 error %f -> %f: %v", inf64, inf32, err)