diff --git a/cast_test.go b/cast_test.go index f69a05b..2f49cf8 100644 --- a/cast_test.go +++ b/cast_test.go @@ -7,6 +7,7 @@ package cast_test import ( "fmt" + "math" "testing" "time" @@ -239,3 +240,64 @@ type MyUint32 uint32 type MyUint64 uint64 type MyFloat32 float32 type MyFloat64 float64 + +func TestTypedNumericOverflow(t *testing.T) { + c := qt.New(t) + + // Issue #356 regression tests: out-of-range typed numeric conversions should return errors + _, err := cast.ToInt64E(uint64(math.MaxUint64)) + c.Assert(err, qt.IsNotNil) + + _, err = cast.ToInt64E(1e300) + c.Assert(err, qt.IsNotNil) + + _, err = cast.ToInt64E(math.NaN()) + c.Assert(err, qt.IsNotNil) + + _, err = cast.ToInt64E(math.Inf(1)) + c.Assert(err, qt.IsNotNil) + + _, err = cast.ToInt64E(math.Inf(-1)) + c.Assert(err, qt.IsNotNil) + + _, err = cast.ToInt64E(float64(math.MaxInt64)) + c.Assert(err, qt.IsNotNil) + + _, err = cast.ToInt8E(1e300) + c.Assert(err, qt.IsNotNil) + + _, err = cast.ToInt8E(int64(300)) + c.Assert(err, qt.IsNotNil) + + _, err = cast.ToUint64E(math.NaN()) + c.Assert(err, qt.IsNotNil) + + _, err = cast.ToUint64E(math.Inf(1)) + c.Assert(err, qt.IsNotNil) + + _, err = cast.ToUint64E(float64(-1e300)) + c.Assert(err, qt.IsNotNil) + + _, err = cast.ToUint8E(uint64(300)) + c.Assert(err, qt.IsNotNil) + + _, err = cast.ToIntE(uint64(math.MaxUint64)) + c.Assert(err, qt.IsNotNil) + + // In-range conversions should continue to succeed + v64, err := cast.ToInt64E(int64(42)) + c.Assert(err, qt.IsNil) + c.Assert(v64, qt.Equals, int64(42)) + + v64, err = cast.ToInt64E(3.7) + c.Assert(err, qt.IsNil) + c.Assert(v64, qt.Equals, int64(3)) + + v8, err := cast.ToInt8E(-128.9) + c.Assert(err, qt.IsNil) + c.Assert(v8, qt.Equals, int8(-128)) + + vu64, err := cast.ToUint64E(uint64(7)) + c.Assert(err, qt.IsNil) + c.Assert(vu64, qt.Equals, uint64(7)) +} diff --git a/number.go b/number.go index a58dc4d..e531429 100644 --- a/number.go +++ b/number.go @@ -9,6 +9,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "regexp" "strconv" "strings" @@ -95,28 +96,76 @@ func toNumber[T Number](i any) (T, bool) { case T: return s, true case int: + if !numberFitsFromInt64[T](int64(s)) { + return 0, false + } + return T(s), true case int8: + if !numberFitsFromInt64[T](int64(s)) { + return 0, false + } + return T(s), true case int16: + if !numberFitsFromInt64[T](int64(s)) { + return 0, false + } + return T(s), true case int32: + if !numberFitsFromInt64[T](int64(s)) { + return 0, false + } + return T(s), true case int64: + if !numberFitsFromInt64[T](s) { + return 0, false + } + return T(s), true case uint: + if !numberFitsFromUint64[T](uint64(s)) { + return 0, false + } + return T(s), true case uint8: + if !numberFitsFromUint64[T](uint64(s)) { + return 0, false + } + return T(s), true case uint16: + if !numberFitsFromUint64[T](uint64(s)) { + return 0, false + } + return T(s), true case uint32: + if !numberFitsFromUint64[T](uint64(s)) { + return 0, false + } + return T(s), true case uint64: + if !numberFitsFromUint64[T](s) { + return 0, false + } + return T(s), true case float32: + if !numberFitsFromFloat64[T](float64(s)) { + return 0, false + } + return T(s), true case float64: + if !numberFitsFromFloat64[T](s) { + return 0, false + } + return T(s), true case bool: if s { @@ -203,52 +252,100 @@ func toUnsignedNumber[T Number](i any) (T, bool, bool) { return 0, false, false } + if !numberFitsFromInt64[T](int64(s)) { + return 0, true, false + } + return T(s), true, true case int8: if s < 0 { return 0, false, false } + if !numberFitsFromInt64[T](int64(s)) { + return 0, true, false + } + return T(s), true, true case int16: if s < 0 { return 0, false, false } + if !numberFitsFromInt64[T](int64(s)) { + return 0, true, false + } + return T(s), true, true case int32: if s < 0 { return 0, false, false } + if !numberFitsFromInt64[T](int64(s)) { + return 0, true, false + } + return T(s), true, true case int64: if s < 0 { return 0, false, false } + if !numberFitsFromInt64[T](s) { + return 0, true, false + } + return T(s), true, true case uint: + if !numberFitsFromUint64[T](uint64(s)) { + return 0, true, false + } + return T(s), true, true case uint8: + if !numberFitsFromUint64[T](uint64(s)) { + return 0, true, false + } + return T(s), true, true case uint16: + if !numberFitsFromUint64[T](uint64(s)) { + return 0, true, false + } + return T(s), true, true case uint32: + if !numberFitsFromUint64[T](uint64(s)) { + return 0, true, false + } + return T(s), true, true case uint64: + if !numberFitsFromUint64[T](s) { + return 0, true, false + } + return T(s), true, true case float32: if s < 0 { return 0, false, false } + if !numberFitsFromFloat64[T](float64(s)) { + return 0, true, false + } + return T(s), true, true case float64: if s < 0 { return 0, false, false } + if !numberFitsFromFloat64[T](s) { + return 0, true, false + } + return T(s), true, true case bool: if s { @@ -275,6 +372,116 @@ func toUnsignedNumber[T Number](i any) (T, bool, bool) { return 0, true, false } +func numberFitsFromInt64[T Number](v int64) bool { + switch any(T(0)).(type) { + case int8: + return v >= math.MinInt8 && v <= math.MaxInt8 + case int16: + return v >= math.MinInt16 && v <= math.MaxInt16 + case int32: + return v >= math.MinInt32 && v <= math.MaxInt32 + case int64: + return true + case int: + if strconv.IntSize == 32 { + return v >= math.MinInt32 && v <= math.MaxInt32 + } + + return true + case uint8: + return v >= 0 && v <= math.MaxUint8 + case uint16: + return v >= 0 && v <= math.MaxUint16 + case uint32: + return v >= 0 && v <= math.MaxUint32 + case uint64: + return v >= 0 + case uint: + return v >= 0 && (strconv.IntSize == 64 || v <= math.MaxUint32) + case float32, float64: + return true + } + + return false +} + +func numberFitsFromUint64[T Number](v uint64) bool { + switch any(T(0)).(type) { + case int8: + return v <= math.MaxInt8 + case int16: + return v <= math.MaxInt16 + case int32: + return v <= math.MaxInt32 + case int64: + return v <= math.MaxInt64 + case int: + if strconv.IntSize == 32 { + return v <= math.MaxInt32 + } + + return v <= math.MaxInt64 + case uint8: + return v <= math.MaxUint8 + case uint16: + return v <= math.MaxUint16 + case uint32: + return v <= math.MaxUint32 + case uint64: + return true + case uint: + return strconv.IntSize == 64 || v <= math.MaxUint32 + case float32, float64: + return true + } + + return false +} + +func numberFitsFromFloat64[T Number](v float64) bool { + switch any(T(0)).(type) { + case float32, float64: + return true + } + + if math.IsNaN(v) || math.IsInf(v, 0) { + return false + } + + switch any(T(0)).(type) { + case int8: + return v > math.MinInt8-1 && v < math.MaxInt8+1 + case int16: + return v > math.MinInt16-1 && v < math.MaxInt16+1 + case int32: + return v > math.MinInt32-1 && v < math.MaxInt32+1 + case int64: + return v >= float64(math.MinInt64) && v < float64(1<<63) + case int: + if strconv.IntSize == 32 { + return v > math.MinInt32-1 && v < math.MaxInt32+1 + } + + return v >= float64(math.MinInt64) && v < float64(1<<63) + case uint8: + return v >= 0 && v < math.MaxUint8+1 + case uint16: + return v >= 0 && v < math.MaxUint16+1 + case uint32: + return v >= 0 && v < math.MaxUint32+1 + case uint64: + return v >= 0 && v < 18446744073709551616.0 + case uint: + if strconv.IntSize == 32 { + return v >= 0 && v < math.MaxUint32+1 + } + + return v >= 0 && v < 18446744073709551616.0 + } + + return false +} + func toUnsignedNumberE[T Number](i any, parseFn func(string) (T, error)) (T, error) { n, valid, ok := toUnsignedNumber[T](i) if ok {