From eac9651a58bbd0a80bd75c22d3b68fc8130f941d Mon Sep 17 00:00:00 2001 From: AdamMagued Date: Fri, 2 Oct 2026 14:22:46 +0000 Subject: [PATCH] fix: prevent silent wraparound in out-of-range numeric conversions Add range bounds checking to toNumber and toUnsignedNumber before converting typed numeric values (e.g. uint64, float64). Previously, unchecked Go conversions directly evaluated T(s), causing out-of-range values such as uint64(math.MaxUint64) to wrap to -1 and float64(1e300) or NaN to yield math.MinInt64 with a nil error, inconsistent with string inputs which correctly returned range errors. Introduce helper functions numberFitsFromInt64, numberFitsFromUint64, and numberFitsFromFloat64 to ensure values fit the target numeric type prior to conversion, returning an error when out-of-range. Add regression tests for typed numeric overflow and NaN conversions. Fixes #356 --- cast_test.go | 62 +++++++++++++++ number.go | 207 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 269 insertions(+) diff --git a/cast_test.go b/cast_test.go index f69a05b3..2f49cf81 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 a58dc4d1..e531429f 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 {