Skip to content

Commit

Permalink
feat(parseutil) Add Safe variants of ParseInt* (#37)
Browse files Browse the repository at this point in the history
These proposed variants allow parsing smaller data types (such as ints)
from larger data types (the int64 returned by ParseInt{,Slice}(...)),
validating that they are within the requested range prior to casting.
With the SafeParseIntRange(...) helper, we also allow validation of the
maximum expected number of elements in the slice.

Added missing method doc strings to all methods.

Signed-off-by: Alexander Scheel <[email protected]>
  • Loading branch information
cipherboy authored Apr 27, 2022
1 parent 4cbc1f8 commit db74f13
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 0 deletions.
99 changes: 99 additions & 0 deletions parseutil/parseutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"regexp"
"strconv"
"strings"
Expand Down Expand Up @@ -92,6 +93,9 @@ func ParseCapacityString(in interface{}) (uint64, error) {
return cap, nil
}

// Parse a duration from an arbitrary value (a string or numeric value) into
// a time.Duration; when units are missing (such as when a numeric type is
// provided), the duration is assumed to be in seconds.
func ParseDurationSecond(in interface{}) (time.Duration, error) {
var dur time.Duration
jsonIn, ok := in.(json.Number)
Expand Down Expand Up @@ -147,6 +151,9 @@ func ParseDurationSecond(in interface{}) (time.Duration, error) {
return dur, nil
}

// Parse an absolute timestamp from the provided arbitrary value (string or
// numeric value). When an untyped numeric value is provided, it is assumed
// to be seconds from the Unix Epoch.
func ParseAbsoluteTime(in interface{}) (time.Time, error) {
var t time.Time
switch inp := in.(type) {
Expand Down Expand Up @@ -195,6 +202,13 @@ func ParseAbsoluteTime(in interface{}) (time.Time, error) {
return t, nil
}

// ParseInt takes an arbitrary value (either a string or numeric type) and
// parses it as an int64 value. This value is assumed to be larger than the
// provided type, but cannot safely be cast.
//
// When the end value is bounded (such as an int value), it is recommended
// to instead call SafeParseInt or SafeParseIntRange to safely cast to a
// more restrictive type.
func ParseInt(in interface{}) (int64, error) {
var ret int64
jsonIn, ok := in.(json.Number)
Expand Down Expand Up @@ -232,6 +246,11 @@ func ParseInt(in interface{}) (int64, error) {
return ret, nil
}

// ParseDirectIntSlice behaves similarly to ParseInt, but accepts typed
// slices, returning a slice of int64s.
//
// If the starting value may not be in slice form (e.g.. a bare numeric value
// could be provided), it is suggested to call ParseIntSlice instead.
func ParseDirectIntSlice(in interface{}) ([]int64, error) {
var ret []int64

Expand Down Expand Up @@ -290,6 +309,10 @@ func ParseDirectIntSlice(in interface{}) ([]int64, error) {
// nicely handle the common cases of providing only an int-ish, providing
// an actual slice of int-ishes, or providing a comma-separated list of
// numbers.
//
// When []int64 is not the desired final type (or the values should be
// range-bound), it is suggested to call SafeParseIntSlice or
// SafeParseIntSliceRange instead.
func ParseIntSlice(in interface{}) ([]int64, error) {
if ret, err := ParseInt(in); err == nil {
return []int64{ret}, nil
Expand Down Expand Up @@ -320,6 +343,7 @@ func ParseIntSlice(in interface{}) ([]int64, error) {
return nil, errors.New("could not parse value from input")
}

// Parses the provided arbitrary value as a boolean-like value.
func ParseBool(in interface{}) (bool, error) {
var result bool
if err := mapstructure.WeakDecode(in, &result); err != nil {
Expand All @@ -328,6 +352,7 @@ func ParseBool(in interface{}) (bool, error) {
return result, nil
}

// Parses the provided arbitrary value as a string.
func ParseString(in interface{}) (string, error) {
var result string
if err := mapstructure.WeakDecode(in, &result); err != nil {
Expand All @@ -336,6 +361,7 @@ func ParseString(in interface{}) (string, error) {
return result, nil
}

// Parses the provided string-like value as a comma-separated list of values.
func ParseCommaStringSlice(in interface{}) ([]string, error) {
jsonIn, ok := in.(json.Number)
if ok {
Expand All @@ -362,6 +388,7 @@ func ParseCommaStringSlice(in interface{}) ([]string, error) {
return strutil.TrimStrings(result), nil
}

// Parses the specified value as one or more addresses, separated by commas.
func ParseAddrs(addrs interface{}) ([]*sockaddr.SockAddrMarshaler, error) {
out := make([]*sockaddr.SockAddrMarshaler, 0)
stringAddrs := make([]string, 0)
Expand Down Expand Up @@ -401,3 +428,75 @@ func ParseAddrs(addrs interface{}) ([]*sockaddr.SockAddrMarshaler, error) {

return out, nil
}

// Parses the provided arbitrary value (see ParseInt), ensuring it is within
// the specified range (inclusive of bounds). If this range corresponds to a
// smaller type, the returned value can then be safely cast without risking
// overflow.
func SafeParseIntRange(in interface{}, min int64, max int64) (int64, error) {
raw, err := ParseInt(in)
if err != nil {
return 0, err
}

if raw < min || raw > max {
return 0, fmt.Errorf("error parsing int value; out of range [%v to %v]: %v", min, max, raw)
}

return raw, nil
}

// Parses the specified arbitrary value (see ParseInt), ensuring that the
// resulting value is within the range for an int value. If no error occurred,
// the caller knows no overflow occurred.
func SafeParseInt(in interface{}) (int, error) {
raw, err := SafeParseIntRange(in, math.MinInt, math.MaxInt)
return int(raw), err
}

// Parses the provided arbitrary value (see ParseIntSlice) into a slice of
// int64 values, ensuring each is within the specified range (inclusive of
// bounds). If this range corresponds to a smaller type, the returned value
// can then be safely cast without risking overflow.
//
// If elements is positive, it is used to ensure the resulting slice is
// bounded above by that many number of elements (inclusive).
func SafeParseIntSliceRange(in interface{}, minValue int64, maxValue int64, elements int) ([]int64, error) {
raw, err := ParseIntSlice(in)
if err != nil {
return nil, err
}

if elements > 0 && len(raw) > elements {
return nil, fmt.Errorf("error parsing value from input: got %v but expected at most %v elements", len(raw), elements)
}

for index, value := range raw {
if value < minValue || value > maxValue {
return nil, fmt.Errorf("error parsing value from input: element %v was outside of range [%v to %v]: %v", index, minValue, maxValue, value)
}
}

return raw, nil
}

// Parses the provided arbitrary value (see ParseIntSlice) into a slice of
// int values, ensuring the each resulting value in the slice is within the
// range for an int value. If no error occurred, the caller knows no overflow
// occurred.
//
// If elements is positive, it is used to ensure the resulting slice is
// bounded above by that many number of elements (inclusive).
func SafeParseIntSlice(in interface{}, elements int) ([]int, error) {
raw, err := SafeParseIntSliceRange(in, math.MinInt, math.MaxInt, elements)
if err != nil || raw == nil {
return nil, err
}

var result = make([]int, len(raw))
for _, element := range raw {
result = append(result, int(element))
}

return result, nil
}
59 changes: 59 additions & 0 deletions parseutil/parseutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,121 +354,174 @@ func Test_ParseIntSlice(t *testing.T) {
testCases := []struct {
inp interface{}
valid bool
ranged bool
expected []int64
}{
// ParseInt
{
int(-1),
true,
false,
[]int64{-1},
},
{
int32(-1),
true,
false,
[]int64{-1},
},
{
int64(-1),
true,
false,
[]int64{-1},
},
{
uint(1),
true,
true,
[]int64{1},
},
{
uint32(1),
true,
true,
[]int64{1},
},
{
uint64(1),
true,
true,
[]int64{1},
},
{
json.Number("1"),
true,
true,
[]int64{1},
},
{
"1",
true,
true,
[]int64{1},
},
// ParseDirectIntSlice
{
[]int{1, -2, 3},
true,
false,
[]int64{1, -2, 3},
},
{
[]int32{1, -2, 3},
true,
false,
[]int64{1, -2, 3},
},
{
[]int64{1, -2, 3},
true,
false,
[]int64{1, -2, 3},
},
{
[]uint{1, 2, 3},
true,
true,
[]int64{1, 2, 3},
},
{
[]uint32{1, 2, 3},
true,
true,
[]int64{1, 2, 3},
},
{
[]uint64{1, 2, 3},
true,
true,
[]int64{1, 2, 3},
},
{
[]json.Number{json.Number("1"), json.Number("2"), json.Number("3")},
true,
true,
[]int64{1, 2, 3},
},
{
[]string{"1", "2", "3"},
true,
true,
[]int64{1, 2, 3},
},
// Comma separated list
{
"1",
true,
true,
[]int64{1},
},
{
"1,",
true,
true,
[]int64{1},
},
{
",1",
true,
true,
[]int64{1},
},
{
",1,",
true,
true,
[]int64{1},
},
{
"1,2",
true,
true,
[]int64{1, 2},
},
{
"1,2,3",
true,
true,
[]int64{1, 2, 3},
},
{
"1,3,5",
true,
true,
[]int64{1, 3, 5},
},
{
"1,3,5,7",
true,
false,
[]int64{1, 3, 5, 7},
},
{
"1,2,3,4,5,6,7,8,9,0",
true,
false,
[]int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 0},
},
{
"1,1,1,1,1,1,1,1,1,1,1",
true,
false,
[]int64{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
},
{
"1,1,1,1,1,1,1,1,1,1",
true,
true,
[]int64{1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
},
}

for _, tc := range testCases {
Expand All @@ -485,6 +538,12 @@ func Test_ParseIntSlice(t *testing.T) {
}
if !equalInt64Slice(outp, tc.expected) {
t.Errorf("input %v parsed as %v, expected %v", tc.inp, outp, tc.expected)
continue
}
_, err = SafeParseIntSliceRange(tc.inp, 0 /* min */, 5 /* max */, 10 /* num elements */)
if err == nil != tc.ranged {
t.Errorf("no ranged slice error for %v", tc.inp)
continue
}
}
}
Expand Down

0 comments on commit db74f13

Please sign in to comment.