Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(parseutil) Add Safe variants of ParseInt* #37

Merged
merged 1 commit into from Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
99 changes: 99 additions & 0 deletions parseutil/parseutil.go
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) {
sgmiller marked this conversation as resolved.
Show resolved Hide resolved
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
sgmiller marked this conversation as resolved.
Show resolved Hide resolved
// 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
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