From 82f6afd10a95a5034e2e74dc77d291d419d6f321 Mon Sep 17 00:00:00 2001 From: Alexander Scheel Date: Wed, 27 Apr 2022 09:29:06 -0400 Subject: [PATCH] feat(parseutil) Add Safe variants of ParseInt* 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 --- parseutil/parseutil.go | 99 +++++++++++++++++++++++++++++++++++++ parseutil/parseutil_test.go | 59 ++++++++++++++++++++++ 2 files changed, 158 insertions(+) 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 } } }