Skip to content

Commit

Permalink
fix: float overflow when converted to integer
Browse files Browse the repository at this point in the history
Only uint64 and uint was working as expected

Fixes #23
  • Loading branch information
ccoVeille committed Sep 9, 2024
1 parent fcee9f8 commit c6327a9
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 57 deletions.
42 changes: 34 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,46 @@ package main

import (
"fmt"
"math"

"github.com/ccoveille/go-safecast"
)

func main() {
_, err := safecast.ToInt8(uint16(1000))
fmt.Println(err) // integer overflow: 1000 (uint16) is greater than 127 (int8): exceed upper boundary for this type

_, err = safecast.ToUint16(int64(-1))
fmt.Println(err) // integer overflow: -1 (int64) is smaller than 0 (uint16): exceed lower boundary for this type
// when there is no overflow
//
fmt.Println(safecast.ToInt8(float64(42)))
// Output: 42, nil
fmt.Println(safecast.ToInt8(int64(-1)))
// Output: -1, nil

// when there is an overflow
//
fmt.Println(safecast.ToInt8(float64(20000)))
// Output: 0 conversion issue: 20000 is greater than 127
fmt.Println(safecast.ToUint8(int64(-1)))
// Output: 0 conversion issue: -1 is negative
fmt.Println(safecast.ToInt16(int32(40000)))
// Output: 0 conversion issue: 40000 is greater than 32767
fmt.Println(safecast.ToUint16(int64(-1)))
// Output: 0 conversion issue: -1 is negative
fmt.Println(safecast.ToInt32(math.MaxUint32 + 1))
// Output: 0 conversion issue: 4294967296 is greater than 2147483647
fmt.Println(safecast.ToUint32(int64(-1)))
// Output: 0 conversion issue: -1 is negative
fmt.Println(safecast.ToInt64(uint64(math.MaxInt64) + 1))
// Output: 0 conversion issue: 9223372036854775808 is greater than 9223372036854775807
fmt.Println(safecast.ToUint64(int8(-1)))
// Output: 0 conversion issue: -1 is negative
fmt.Println(safecast.ToInt(uint64(math.MaxInt) + 1))
// Output: 0 conversion issue: 9223372036854775808 is greater than 9223372036854775807
fmt.Println(safecast.ToUint(int8(-1)))
// Output: 0 conversion issue: -1 is negative
}
```

[Go Playground](https://go.dev/play/p/ciic-m0cdfb)
[Go Playground](https://go.dev/play/p/VCrv1aLJjMQ)

## Conversion overflows

Expand All @@ -54,11 +80,11 @@ func main() {

a = 255 + 1
b = uint8(a)
fmt.Println(b) // 0 integer overflow
fmt.Println(b) // 0 conversion overflow

a = -1
b = uint8(a)
fmt.Println(b) // 255 integer overflow
fmt.Println(b) // 255 conversion overflow
}
```

Expand All @@ -68,7 +94,7 @@ func main() {

The gosec project raised this to my attention when the gosec [G115 rule was added](https://github.com/securego/gosec/pull/1149)

> G115: Potential integer overflow when converting between integer types.
> G115: Potential overflow when converting between integer types.
This issue was way more complex than expected, and required multiple fixes.

Expand Down
62 changes: 59 additions & 3 deletions asserters.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,65 @@ package safecast

import "fmt"

func assertNotNegative[T Type](i T) error {
if i < 0 {
return fmt.Errorf("%w: %v is negative", ErrOutOfRange, i)
func assertNotNegative[T Type](value T) error {
if value < 0 {
return fmt.Errorf("%w: %v is negative", ErrConversionIssue, value)
}
return nil
}

func checkUpperBoundary[T Type](value T, boundary uint64) error {
if value <= 0 {
return nil
}

var greater bool
switch f := any(value).(type) {
case float64:
// for float64, everything fits in float64 without overflow.
// We are using a greater or equal because float cannot be compared easily because of precision loss.
greater = f >= float64(boundary)
case float32:
// everything fits in float32, except float64 greater than math.MaxFloat32.
// So, we must convert to float64 and check.
// We are using a greater or equal because float cannot be compared easily because of precision loss.
greater = float64(f) >= float64(boundary)
default:
// for all other integer types, it fits in an uint64 without overflow as we know value is positive.
greater = uint64(value) > boundary
}

if greater {
return fmt.Errorf("%w: %v is greater than %v", ErrConversionIssue, value, boundary)
}

return nil
}

func checkLowerBoundary[T Type](value T, boundary int64) error {
if value >= 0 {
return nil
}

var smaller bool
switch f := any(value).(type) {
case float64:
// everything fits in float64 without overflow.
// We are using a lower or equal because float cannot be compared easily because of precision loss.
smaller = f <= float64(boundary)
case float32:
// everything fits in float32, except float64 smaller than -math.MaxFloat32.
// So, we must convert to float64 and check.
// We are using a lower or equal because float cannot be compared easily because of precision loss.
smaller = float64(f) <= float64(boundary)
default:
// for all other integer types, it fits in an int64 without overflow as we know value is negative.
smaller = int64(value) < boundary
}

if smaller {
return fmt.Errorf("%w: %v is less than %v", ErrConversionIssue, value, boundary)
}

return nil
}
81 changes: 41 additions & 40 deletions conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,148 +4,149 @@

package safecast

import (
"fmt"
"math"
)
import "math"

// ToInt attempts to convert any [Type] value to an int.
// If the conversion results in a value outside the range of an int,
// an ErrOutOfRange error is returned.
// an ErrConversionOverflow error is returned.
func ToInt[T Type](i T) (int, error) {
if i > 0 && uint64(i) > math.MaxInt {
return 0, fmt.Errorf("%w: %v is greater than math.MaxInt", ErrOutOfRange, i)
if err := checkUpperBoundary(i, math.MaxInt); err != nil {
return 0, err
}

if err := checkLowerBoundary(i, math.MinInt); err != nil {
return 0, err
}

return int(i), nil
}

// ToUint attempts to convert any [Type] value to an uint.
// If the conversion results in a value outside the range of an uint,
// an ErrOutOfRange error is returned.
// an ErrConversionOverflow error is returned.
func ToUint[T Type](i T) (uint, error) {
if err := assertNotNegative(i); err != nil {
return 0, err
}

if float64(i) > math.MaxUint64 {
return 0, fmt.Errorf("%w: %v is greater than math.MaxUint64", ErrOutOfRange, i)
if err := checkUpperBoundary(i, math.MaxUint64); err != nil {
return 0, err
}

return uint(i), nil
}

// ToInt8 attempts to convert any [Type] value to an int8.
// If the conversion results in a value outside the range of an int8,
// an ErrOutOfRange error is returned.
// an ErrConversionOverflow error is returned.
func ToInt8[T Type](i T) (int8, error) {
if i > math.MaxInt8 {
return 0, fmt.Errorf("%w: %v is greater than math.MaxInt8", ErrOutOfRange, i)
if err := checkUpperBoundary(i, math.MaxInt8); err != nil {
return 0, err
}

if i < 0 && uint64(-i) > -math.MinInt8 {
return 0, fmt.Errorf("%w: %v is less than math.MinInt8", ErrOutOfRange, i)
if err := checkLowerBoundary(i, math.MinInt8); err != nil {
return 0, err
}

return int8(i), nil
}

// ToUint8 attempts to convert any [Type] value to an uint8.
// If the conversion results in a value outside the range of an uint8,
// an ErrOutOfRange error is returned.
// an ErrConversionOverflow error is returned.
func ToUint8[T Type](i T) (uint8, error) {
if err := assertNotNegative(i); err != nil {
return 0, err
}

if uint64(i) > math.MaxUint8 {
return 0, fmt.Errorf("%w: %v is greater than math.MaxUint8", ErrOutOfRange, i)
if err := checkUpperBoundary(i, math.MaxUint8); err != nil {
return 0, err
}

return uint8(i), nil
}

// ToInt16 attempts to convert any [Type] value to an int16.
// If the conversion results in a value outside the range of an int16,
// an ErrOutOfRange error is returned.
// an ErrConversionOverflow error is returned.
func ToInt16[T Type](i T) (int16, error) {
if i > 0 && uint64(i) > math.MaxInt16 {
return 0, fmt.Errorf("%w: %v is greater than math.MaxInt16", ErrOutOfRange, i)
if err := checkUpperBoundary(i, math.MaxInt16); err != nil {
return 0, err
}

if i < 0 && uint64(-i) > -math.MinInt16 {
return 0, fmt.Errorf("%w: %v is less than math.MinInt16", ErrOutOfRange, i)
if err := checkLowerBoundary(i, math.MinInt16); err != nil {
return 0, err
}

return int16(i), nil
}

// ToUint16 attempts to convert any [Type] value to an uint16.
// If the conversion results in a value outside the range of an uint16,
// an ErrOutOfRange error is returned.
// an ErrConversionOverflow error is returned.
func ToUint16[T Type](i T) (uint16, error) {
if err := assertNotNegative(i); err != nil {
return 0, err
}

if uint64(i) > math.MaxUint16 {
return 0, fmt.Errorf("%w: %v is greater than math.MaxUint16", ErrOutOfRange, i)
if err := checkUpperBoundary(i, math.MaxUint16); err != nil {
return 0, err
}

return uint16(i), nil
}

// ToInt32 attempts to convert any [Type] value to an int32.
// If the conversion results in a value outside the range of an int32,
// an ErrOutOfRange error is returned.
// an ErrConversionOverflow error is returned.
func ToInt32[T Type](i T) (int32, error) {
if i > 0 && uint64(i) > math.MaxInt32 {
return 0, fmt.Errorf("%w: %v is greater than math.MaxInt32", ErrOutOfRange, i)
if err := checkUpperBoundary(i, math.MaxInt32); err != nil {
return 0, err
}

if i < 0 && uint64(-i) > -math.MinInt32 {
return 0, fmt.Errorf("%w: %v is less than math.MinInt32", ErrOutOfRange, i)
if err := checkLowerBoundary(i, math.MinInt32); err != nil {
return 0, err
}

return int32(i), nil
}

// ToUint32 attempts to convert any [Type] value to an uint32.
// If the conversion results in a value outside the range of an uint32,
// an ErrOutOfRange error is returned.
// an ErrConversionOverflow error is returned.
func ToUint32[T Type](i T) (uint32, error) {
if err := assertNotNegative(i); err != nil {
return 0, err
}

if uint64(i) > math.MaxUint32 {
return 0, fmt.Errorf("%w: %v is greater than math.MaxUint32", ErrOutOfRange, i)
if err := checkUpperBoundary(i, math.MaxUint32); err != nil {
return 0, err
}

return uint32(i), nil
}

// ToInt64 attempts to convert any [Type] value to an int64.
// If the conversion results in a value outside the range of an int64,
// an ErrOutOfRange error is returned.
// an ErrConversionOverflow error is returned.
func ToInt64[T Type](i T) (int64, error) {
if i > 0 && uint64(i) > math.MaxInt64 {
return 0, fmt.Errorf("%w: %v is greater than math.MaxInt64", ErrOutOfRange, i)
if err := checkUpperBoundary(i, math.MaxInt64); err != nil {
return 0, err
}

return int64(i), nil
}

// ToUint64 attempts to convert any [Type] value to an uint64.
// If the conversion results in a value outside the range of an uint64,
// an ErrOutOfRange error is returned.
// an ErrConversionOverflow error is returned.
func ToUint64[T Type](i T) (uint64, error) {
if err := assertNotNegative(i); err != nil {
return 0, err
}

if float64(i) > math.MaxUint64 {
return 0, fmt.Errorf("%w: %v is greater than math.MaxUint64", ErrOutOfRange, i)
if err := checkUpperBoundary(i, uint64(math.MaxUint64)); err != nil {
return 0, err
}

return uint64(i), nil
Expand Down
Loading

0 comments on commit c6327a9

Please sign in to comment.