From 397244ca55dcc6649accbc28a9ed49f0caab28d8 Mon Sep 17 00:00:00 2001 From: walle250ai Date: Wed, 29 Apr 2026 21:41:07 +0800 Subject: [PATCH 1/2] Fix overflow handling in ToUint8E and ToUint16E - Fix parseUint/parseInt to pass correct bitSize to strconv, so string inputs like '256' correctly return an error for uint8 - Add uint64Overflow and float64Overflow helper functions with per-type range checks (uint8: 0-255, uint16: 0-65535, etc.) - Add overflow checks in toUnsignedNumber for all numeric input types - Fix test framework bug: overflow string tests now run for all uint types, not just uint64 --- number.go | 118 ++++++++++++++++++++++++++++++++++++++++++++++++- number_test.go | 2 +- 2 files changed, 117 insertions(+), 3 deletions(-) diff --git a/number.go b/number.go index a58dc4d..b3c94f2 100644 --- a/number.go +++ b/number.go @@ -9,6 +9,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "regexp" "strconv" "strings" @@ -202,52 +203,93 @@ func toUnsignedNumber[T Number](i any) (T, bool, bool) { if s < 0 { return 0, false, false } + if uint64Overflow[T](uint64(s)) { + return 0, true, false + } return T(s), true, true case int8: if s < 0 { return 0, false, false } + if uint64Overflow[T](uint64(s)) { + return 0, true, false + } return T(s), true, true case int16: if s < 0 { return 0, false, false } + if uint64Overflow[T](uint64(s)) { + return 0, true, false + } return T(s), true, true case int32: if s < 0 { return 0, false, false } + if uint64Overflow[T](uint64(s)) { + return 0, true, false + } return T(s), true, true case int64: if s < 0 { return 0, false, false } + if uint64Overflow[T](uint64(s)) { + return 0, true, false + } return T(s), true, true case uint: + if uint64Overflow[T](uint64(s)) { + return 0, true, false + } + return T(s), true, true case uint8: + if uint64Overflow[T](uint64(s)) { + return 0, true, false + } + return T(s), true, true case uint16: + if uint64Overflow[T](uint64(s)) { + return 0, true, false + } + return T(s), true, true case uint32: + if uint64Overflow[T](uint64(s)) { + return 0, true, false + } + return T(s), true, true case uint64: + if uint64Overflow[T](s) { + return 0, true, false + } + return T(s), true, true case float32: if s < 0 { return 0, false, false } + if float64Overflow[T](float64(s)) { + return 0, true, false + } return T(s), true, true case float64: if s < 0 { return 0, false, false } + if float64Overflow[T](s) { + return 0, true, false + } return T(s), true, true case bool: @@ -262,12 +304,18 @@ func toUnsignedNumber[T Number](i any) (T, bool, bool) { if s < 0 { return 0, false, false } + if uint64Overflow[T](uint64(s)) { + return 0, true, false + } return T(s), true, true case time.Month: if s < 0 { return 0, false, false } + if uint64Overflow[T](uint64(s)) { + return 0, true, false + } return T(s), true, true } @@ -275,6 +323,40 @@ func toUnsignedNumber[T Number](i any) (T, bool, bool) { return 0, true, false } +func uint64Overflow[T Number](v uint64) bool { + var t T + switch any(t).(type) { + case uint8: + return v > math.MaxUint8 + case uint16: + return v > math.MaxUint16 + case uint32: + return v > math.MaxUint32 + case uint64: + return false + case uint: + return v > uint64(^uint(0)) + } + return false +} + +func float64Overflow[T Number](v float64) bool { + var t T + switch any(t).(type) { + case uint8: + return v > float64(math.MaxUint8) || v < 0 + case uint16: + return v > float64(math.MaxUint16) || v < 0 + case uint32: + return v > float64(math.MaxUint32) || v < 0 + case uint64: + return v > float64(math.MaxUint64) || v < 0 + case uint: + return v > float64(^uint(0)) || v < 0 + } + return false +} + func toUnsignedNumberE[T Number](i any, parseFn func(string) (T, error)) (T, error) { n, valid, ok := toUnsignedNumber[T](i) if ok { @@ -405,7 +487,23 @@ func parseNumber[T Number](s string) (T, error) { } func parseInt[T integer](s string) (T, error) { - v, err := strconv.ParseInt(trimDecimal(s), 0, 0) + var t T + var bitSize int + + switch any(t).(type) { + case int: + bitSize = 0 + case int8: + bitSize = 8 + case int16: + bitSize = 16 + case int32: + bitSize = 32 + case int64: + bitSize = 64 + } + + v, err := strconv.ParseInt(trimDecimal(s), 0, bitSize) if err != nil { return 0, err } @@ -414,7 +512,23 @@ func parseInt[T integer](s string) (T, error) { } func parseUint[T unsigned](s string) (T, error) { - v, err := strconv.ParseUint(strings.TrimLeft(trimDecimal(s), "+"), 0, 0) + var t T + var bitSize int + + switch any(t).(type) { + case uint: + bitSize = 0 + case uint8: + bitSize = 8 + case uint16: + bitSize = 16 + case uint32: + bitSize = 32 + case uint64: + bitSize = 64 + } + + v, err := strconv.ParseUint(strings.TrimLeft(trimDecimal(s), "+"), 0, bitSize) if err != nil { return 0, err } diff --git a/number_test.go b/number_test.go index 9d84d18..207846b 100644 --- a/number_test.go +++ b/number_test.go @@ -262,7 +262,7 @@ func generateNumberTestCases(samples []any) []testCase { testCases = append(testCases, testCase{underflowString, zero, true}) } - if kind == reflect.Uint64 && isUint && overflowString != nil { + if isUint && overflowString != nil { testCases = append(testCases, testCase{overflowString, zero, true}) } From 7c526dfb5558843c99a4b182d22a08627ab4256c Mon Sep 17 00:00:00 2001 From: walle250ai Date: Wed, 29 Apr 2026 21:57:53 +0800 Subject: [PATCH 2/2] fix: add overflow detection for signed integer casts (int8/int16/int32) ToInt8E(128) previously returned -128 with no error due to silent integer wrap. Adds int64Overflow helper and gates all signed-int paths in toNumber behind it; extends the test framework to run overflow/underflow string cases for signed types, matching the existing coverage for unsigned types. Co-Authored-By: Claude Sonnet 4.6 --- number.go | 33 +++++++++++++++++++++++++++++++++ number_test.go | 4 ++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/number.go b/number.go index b3c94f2..d09f575 100644 --- a/number.go +++ b/number.go @@ -96,14 +96,29 @@ func toNumber[T Number](i any) (T, bool) { case T: return s, true case int: + if int64Overflow[T](int64(s)) { + return 0, false + } return T(s), true case int8: + if int64Overflow[T](int64(s)) { + return 0, false + } return T(s), true case int16: + if int64Overflow[T](int64(s)) { + return 0, false + } return T(s), true case int32: + if int64Overflow[T](int64(s)) { + return 0, false + } return T(s), true case int64: + if int64Overflow[T](s) { + return 0, false + } return T(s), true case uint: return T(s), true @@ -357,6 +372,24 @@ func float64Overflow[T Number](v float64) bool { return false } +func int64Overflow[T Number](v int64) bool { + var t T + switch any(t).(type) { + case int8: + return v > math.MaxInt8 || v < math.MinInt8 + case int16: + return v > math.MaxInt16 || v < math.MinInt16 + case int32: + return v > math.MaxInt32 || v < math.MinInt32 + case int64: + return false + case int: + maxInt := int64(^uint(0) >> 1) + return v > maxInt || v < -maxInt-1 + } + return false +} + func toUnsignedNumberE[T Number](i any, parseFn func(string) (T, error)) (T, error) { n, valid, ok := toUnsignedNumber[T](i) if ok { diff --git a/number_test.go b/number_test.go index 207846b..f1aa0b0 100644 --- a/number_test.go +++ b/number_test.go @@ -258,11 +258,11 @@ func generateNumberTestCases(samples []any) []testCase { testCases = append(testCases, testCase{"10.0E9", float64(10000000000.000000), false}) } - if isUint && underflowString != nil { + if (isUint || isSint) && underflowString != nil { testCases = append(testCases, testCase{underflowString, zero, true}) } - if isUint && overflowString != nil { + if (isUint || isSint) && overflowString != nil { testCases = append(testCases, testCase{overflowString, zero, true}) }