diff --git a/parseutil/parseutil.go b/parseutil/parseutil.go index 1679531..b3a932e 100644 --- a/parseutil/parseutil.go +++ b/parseutil/parseutil.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "regexp" "strconv" "strings" @@ -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) @@ -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) { @@ -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) @@ -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 @@ -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 @@ -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 { @@ -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 { @@ -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 { @@ -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) @@ -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 +} diff --git a/parseutil/parseutil_test.go b/parseutil/parseutil_test.go index 0d2de4d..9cbf564 100644 --- a/parseutil/parseutil_test.go +++ b/parseutil/parseutil_test.go @@ -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 { @@ -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 } } }