mirror of https://github.com/hashicorp/boundary
Update usage of shared-secure-libs (#1393)
parent
a0c0d17c1d
commit
2cbcf9a563
@ -1,174 +0,0 @@
|
||||
package parseutil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/sdk/strutil"
|
||||
"github.com/hashicorp/errwrap"
|
||||
sockaddr "github.com/hashicorp/go-sockaddr"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
func ParseDurationSecond(in interface{}) (time.Duration, error) {
|
||||
var dur time.Duration
|
||||
jsonIn, ok := in.(json.Number)
|
||||
if ok {
|
||||
in = jsonIn.String()
|
||||
}
|
||||
switch inp := in.(type) {
|
||||
case nil:
|
||||
// return default of zero
|
||||
case string:
|
||||
if inp == "" {
|
||||
return dur, nil
|
||||
}
|
||||
var err error
|
||||
// Look for a suffix otherwise its a plain second value
|
||||
if strings.HasSuffix(inp, "s") || strings.HasSuffix(inp, "m") || strings.HasSuffix(inp, "h") || strings.HasSuffix(inp, "ms") {
|
||||
dur, err = time.ParseDuration(inp)
|
||||
if err != nil {
|
||||
return dur, err
|
||||
}
|
||||
} else {
|
||||
// Plain integer
|
||||
secs, err := strconv.ParseInt(inp, 10, 64)
|
||||
if err != nil {
|
||||
return dur, err
|
||||
}
|
||||
dur = time.Duration(secs) * time.Second
|
||||
}
|
||||
case int:
|
||||
dur = time.Duration(inp) * time.Second
|
||||
case int32:
|
||||
dur = time.Duration(inp) * time.Second
|
||||
case int64:
|
||||
dur = time.Duration(inp) * time.Second
|
||||
case uint:
|
||||
dur = time.Duration(inp) * time.Second
|
||||
case uint32:
|
||||
dur = time.Duration(inp) * time.Second
|
||||
case uint64:
|
||||
dur = time.Duration(inp) * time.Second
|
||||
case float32:
|
||||
dur = time.Duration(inp) * time.Second
|
||||
case float64:
|
||||
dur = time.Duration(inp) * time.Second
|
||||
case time.Duration:
|
||||
dur = inp
|
||||
default:
|
||||
return 0, errors.New("could not parse duration from input")
|
||||
}
|
||||
|
||||
return dur, nil
|
||||
}
|
||||
|
||||
func ParseInt(in interface{}) (int64, error) {
|
||||
var ret int64
|
||||
jsonIn, ok := in.(json.Number)
|
||||
if ok {
|
||||
in = jsonIn.String()
|
||||
}
|
||||
switch in.(type) {
|
||||
case string:
|
||||
inp := in.(string)
|
||||
if inp == "" {
|
||||
return 0, nil
|
||||
}
|
||||
var err error
|
||||
left, err := strconv.ParseInt(inp, 10, 64)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
ret = left
|
||||
case int:
|
||||
ret = int64(in.(int))
|
||||
case int32:
|
||||
ret = int64(in.(int32))
|
||||
case int64:
|
||||
ret = in.(int64)
|
||||
case uint:
|
||||
ret = int64(in.(uint))
|
||||
case uint32:
|
||||
ret = int64(in.(uint32))
|
||||
case uint64:
|
||||
ret = int64(in.(uint64))
|
||||
default:
|
||||
return 0, errors.New("could not parse value from input")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func ParseBool(in interface{}) (bool, error) {
|
||||
var result bool
|
||||
if err := mapstructure.WeakDecode(in, &result); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func ParseCommaStringSlice(in interface{}) ([]string, error) {
|
||||
rawString, ok := in.(string)
|
||||
if ok && rawString == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
var result []string
|
||||
config := &mapstructure.DecoderConfig{
|
||||
Result: &result,
|
||||
WeaklyTypedInput: true,
|
||||
DecodeHook: mapstructure.StringToSliceHookFunc(","),
|
||||
}
|
||||
decoder, err := mapstructure.NewDecoder(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := decoder.Decode(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return strutil.TrimStrings(result), nil
|
||||
}
|
||||
|
||||
func ParseAddrs(addrs interface{}) ([]*sockaddr.SockAddrMarshaler, error) {
|
||||
out := make([]*sockaddr.SockAddrMarshaler, 0)
|
||||
stringAddrs := make([]string, 0)
|
||||
|
||||
switch addrs.(type) {
|
||||
case string:
|
||||
stringAddrs = strutil.ParseArbitraryStringSlice(addrs.(string), ",")
|
||||
if len(stringAddrs) == 0 {
|
||||
return nil, fmt.Errorf("unable to parse addresses from %v", addrs)
|
||||
}
|
||||
|
||||
case []string:
|
||||
stringAddrs = addrs.([]string)
|
||||
|
||||
case []interface{}:
|
||||
for _, v := range addrs.([]interface{}) {
|
||||
stringAddr, ok := v.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error parsing %v as string", v)
|
||||
}
|
||||
stringAddrs = append(stringAddrs, stringAddr)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown address input type %T", addrs)
|
||||
}
|
||||
|
||||
for _, addr := range stringAddrs {
|
||||
sa, err := sockaddr.NewSockAddr(addr)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(fmt.Sprintf("error parsing address %q: {{err}}", addr), err)
|
||||
}
|
||||
out = append(out, &sockaddr.SockAddrMarshaler{
|
||||
SockAddr: sa,
|
||||
})
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
@ -1,476 +0,0 @@
|
||||
package strutil
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
glob "github.com/ryanuber/go-glob"
|
||||
)
|
||||
|
||||
// StrListContainsGlob looks for a string in a list of strings and allows
|
||||
// globs.
|
||||
func StrListContainsGlob(haystack []string, needle string) bool {
|
||||
for _, item := range haystack {
|
||||
if glob.Glob(item, needle) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// StrListContains looks for a string in a list of strings.
|
||||
func StrListContains(haystack []string, needle string) bool {
|
||||
for _, item := range haystack {
|
||||
if item == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// StrListSubset checks if a given list is a subset
|
||||
// of another set
|
||||
func StrListSubset(super, sub []string) bool {
|
||||
for _, item := range sub {
|
||||
if !StrListContains(super, item) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ParseDedupAndSortStrings parses a comma separated list of strings
|
||||
// into a slice of strings. The return slice will be sorted and will
|
||||
// not contain duplicate or empty items.
|
||||
func ParseDedupAndSortStrings(input string, sep string) []string {
|
||||
input = strings.TrimSpace(input)
|
||||
parsed := []string{}
|
||||
if input == "" {
|
||||
// Don't return nil
|
||||
return parsed
|
||||
}
|
||||
return RemoveDuplicates(strings.Split(input, sep), false)
|
||||
}
|
||||
|
||||
// ParseDedupLowercaseAndSortStrings parses a comma separated list of
|
||||
// strings into a slice of strings. The return slice will be sorted and
|
||||
// will not contain duplicate or empty items. The values will be converted
|
||||
// to lower case.
|
||||
func ParseDedupLowercaseAndSortStrings(input string, sep string) []string {
|
||||
input = strings.TrimSpace(input)
|
||||
parsed := []string{}
|
||||
if input == "" {
|
||||
// Don't return nil
|
||||
return parsed
|
||||
}
|
||||
return RemoveDuplicates(strings.Split(input, sep), true)
|
||||
}
|
||||
|
||||
// ParseKeyValues parses a comma separated list of `<key>=<value>` tuples
|
||||
// into a map[string]string.
|
||||
func ParseKeyValues(input string, out map[string]string, sep string) error {
|
||||
if out == nil {
|
||||
return fmt.Errorf("'out is nil")
|
||||
}
|
||||
|
||||
keyValues := ParseDedupLowercaseAndSortStrings(input, sep)
|
||||
if len(keyValues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, keyValue := range keyValues {
|
||||
shards := strings.Split(keyValue, "=")
|
||||
if len(shards) != 2 {
|
||||
return fmt.Errorf("invalid <key,value> format")
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(shards[0])
|
||||
value := strings.TrimSpace(shards[1])
|
||||
if key == "" || value == "" {
|
||||
return fmt.Errorf("invalid <key,value> pair: key: %q value: %q", key, value)
|
||||
}
|
||||
out[key] = value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseArbitraryKeyValues parses arbitrary <key,value> tuples. The input
|
||||
// can be one of the following:
|
||||
// * JSON string
|
||||
// * Base64 encoded JSON string
|
||||
// * Comma separated list of `<key>=<value>` pairs
|
||||
// * Base64 encoded string containing comma separated list of
|
||||
// `<key>=<value>` pairs
|
||||
//
|
||||
// Input will be parsed into the output parameter, which should
|
||||
// be a non-nil map[string]string.
|
||||
func ParseArbitraryKeyValues(input string, out map[string]string, sep string) error {
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
return nil
|
||||
}
|
||||
if out == nil {
|
||||
return fmt.Errorf("'out' is nil")
|
||||
}
|
||||
|
||||
// Try to base64 decode the input. If successful, consider the decoded
|
||||
// value as input.
|
||||
inputBytes, err := base64.StdEncoding.DecodeString(input)
|
||||
if err == nil {
|
||||
input = string(inputBytes)
|
||||
}
|
||||
|
||||
// Try to JSON unmarshal the input. If successful, consider that the
|
||||
// metadata was supplied as JSON input.
|
||||
err = json.Unmarshal([]byte(input), &out)
|
||||
if err != nil {
|
||||
// If JSON unmarshaling fails, consider that the input was
|
||||
// supplied as a comma separated string of 'key=value' pairs.
|
||||
if err = ParseKeyValues(input, out, sep); err != nil {
|
||||
return errwrap.Wrapf("failed to parse the input: {{err}}", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the parsed input
|
||||
for key, value := range out {
|
||||
if key != "" && value == "" {
|
||||
return fmt.Errorf("invalid value for key %q", key)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseStringSlice parses a `sep`-separated list of strings into a
|
||||
// []string with surrounding whitespace removed.
|
||||
//
|
||||
// The output will always be a valid slice but may be of length zero.
|
||||
func ParseStringSlice(input string, sep string) []string {
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
splitStr := strings.Split(input, sep)
|
||||
ret := make([]string, len(splitStr))
|
||||
for i, val := range splitStr {
|
||||
ret[i] = strings.TrimSpace(val)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// ParseArbitraryStringSlice parses arbitrary string slice. The input
|
||||
// can be one of the following:
|
||||
// * JSON string
|
||||
// * Base64 encoded JSON string
|
||||
// * `sep` separated list of values
|
||||
// * Base64-encoded string containing a `sep` separated list of values
|
||||
//
|
||||
// Note that the separator is ignored if the input is found to already be in a
|
||||
// structured format (e.g., JSON)
|
||||
//
|
||||
// The output will always be a valid slice but may be of length zero.
|
||||
func ParseArbitraryStringSlice(input string, sep string) []string {
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Try to base64 decode the input. If successful, consider the decoded
|
||||
// value as input.
|
||||
inputBytes, err := base64.StdEncoding.DecodeString(input)
|
||||
if err == nil {
|
||||
input = string(inputBytes)
|
||||
}
|
||||
|
||||
ret := []string{}
|
||||
|
||||
// Try to JSON unmarshal the input. If successful, consider that the
|
||||
// metadata was supplied as JSON input.
|
||||
err = json.Unmarshal([]byte(input), &ret)
|
||||
if err != nil {
|
||||
// If JSON unmarshaling fails, consider that the input was
|
||||
// supplied as a separated string of values.
|
||||
return ParseStringSlice(input, sep)
|
||||
}
|
||||
|
||||
if ret == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// TrimStrings takes a slice of strings and returns a slice of strings
|
||||
// with trimmed spaces
|
||||
func TrimStrings(items []string) []string {
|
||||
ret := make([]string, len(items))
|
||||
for i, item := range items {
|
||||
ret[i] = strings.TrimSpace(item)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// RemoveDuplicates removes duplicate and empty elements from a slice of
|
||||
// strings. This also may convert the items in the slice to lower case and
|
||||
// returns a sorted slice.
|
||||
func RemoveDuplicates(items []string, lowercase bool) []string {
|
||||
itemsMap := map[string]bool{}
|
||||
for _, item := range items {
|
||||
item = strings.TrimSpace(item)
|
||||
if lowercase {
|
||||
item = strings.ToLower(item)
|
||||
}
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
itemsMap[item] = true
|
||||
}
|
||||
items = make([]string, 0, len(itemsMap))
|
||||
for item := range itemsMap {
|
||||
items = append(items, item)
|
||||
}
|
||||
sort.Strings(items)
|
||||
return items
|
||||
}
|
||||
|
||||
// RemoveDuplicatesStable removes duplicate and empty elements from a slice of
|
||||
// strings, preserving order (and case) of the original slice.
|
||||
// In all cases, strings are compared after trimming whitespace
|
||||
// If caseInsensitive, strings will be compared after ToLower()
|
||||
func RemoveDuplicatesStable(items []string, caseInsensitive bool) []string {
|
||||
itemsMap := make(map[string]bool, len(items))
|
||||
deduplicated := make([]string, 0, len(items))
|
||||
|
||||
for _, item := range items {
|
||||
key := strings.TrimSpace(item)
|
||||
if caseInsensitive {
|
||||
key = strings.ToLower(key)
|
||||
}
|
||||
if key == "" || itemsMap[key] {
|
||||
continue
|
||||
}
|
||||
itemsMap[key] = true
|
||||
deduplicated = append(deduplicated, item)
|
||||
}
|
||||
return deduplicated
|
||||
}
|
||||
|
||||
// RemoveEmpty removes empty elements from a slice of
|
||||
// strings
|
||||
func RemoveEmpty(items []string) []string {
|
||||
if len(items) == 0 {
|
||||
return items
|
||||
}
|
||||
itemsSlice := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
itemsSlice = append(itemsSlice, item)
|
||||
}
|
||||
return itemsSlice
|
||||
}
|
||||
|
||||
// EquivalentSlices checks whether the given string sets are equivalent, as in,
|
||||
// they contain the same values.
|
||||
func EquivalentSlices(a, b []string) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// First we'll build maps to ensure unique values
|
||||
mapA := map[string]bool{}
|
||||
mapB := map[string]bool{}
|
||||
for _, keyA := range a {
|
||||
mapA[keyA] = true
|
||||
}
|
||||
for _, keyB := range b {
|
||||
mapB[keyB] = true
|
||||
}
|
||||
|
||||
// Now we'll build our checking slices
|
||||
var sortedA, sortedB []string
|
||||
for keyA := range mapA {
|
||||
sortedA = append(sortedA, keyA)
|
||||
}
|
||||
for keyB := range mapB {
|
||||
sortedB = append(sortedB, keyB)
|
||||
}
|
||||
sort.Strings(sortedA)
|
||||
sort.Strings(sortedB)
|
||||
|
||||
// Finally, compare
|
||||
if len(sortedA) != len(sortedB) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range sortedA {
|
||||
if sortedA[i] != sortedB[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// EqualStringMaps tests whether two map[string]string objects are equal.
|
||||
// Equal means both maps have the same sets of keys and values. This function
|
||||
// is 6-10x faster than a call to reflect.DeepEqual().
|
||||
func EqualStringMaps(a, b map[string]string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
for k := range a {
|
||||
v, ok := b[k]
|
||||
if !ok || a[k] != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// StrListDelete removes the first occurrence of the given item from the slice
|
||||
// of strings if the item exists.
|
||||
func StrListDelete(s []string, d string) []string {
|
||||
if s == nil {
|
||||
return s
|
||||
}
|
||||
|
||||
for index, element := range s {
|
||||
if element == d {
|
||||
return append(s[:index], s[index+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// GlobbedStringsMatch compares item to val with support for a leading and/or
|
||||
// trailing wildcard '*' in item.
|
||||
func GlobbedStringsMatch(item, val string) bool {
|
||||
if len(item) < 2 {
|
||||
return val == item
|
||||
}
|
||||
|
||||
hasPrefix := strings.HasPrefix(item, "*")
|
||||
hasSuffix := strings.HasSuffix(item, "*")
|
||||
|
||||
if hasPrefix && hasSuffix {
|
||||
return strings.Contains(val, item[1:len(item)-1])
|
||||
} else if hasPrefix {
|
||||
return strings.HasSuffix(val, item[1:])
|
||||
} else if hasSuffix {
|
||||
return strings.HasPrefix(val, item[:len(item)-1])
|
||||
}
|
||||
|
||||
return val == item
|
||||
}
|
||||
|
||||
// AppendIfMissing adds a string to a slice if the given string is not present
|
||||
func AppendIfMissing(slice []string, i string) []string {
|
||||
if StrListContains(slice, i) {
|
||||
return slice
|
||||
}
|
||||
return append(slice, i)
|
||||
}
|
||||
|
||||
// MergeSlices adds an arbitrary number of slices together, uniquely
|
||||
func MergeSlices(args ...[]string) []string {
|
||||
all := map[string]struct{}{}
|
||||
for _, slice := range args {
|
||||
for _, v := range slice {
|
||||
all[v] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(all))
|
||||
for k := range all {
|
||||
result = append(result, k)
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// Difference returns the set difference (A - B) of the two given slices. The
|
||||
// result will also remove any duplicated values in set A regardless of whether
|
||||
// that matches any values in set B.
|
||||
func Difference(a, b []string, lowercase bool) []string {
|
||||
if len(a) == 0 {
|
||||
return a
|
||||
}
|
||||
if len(b) == 0 {
|
||||
if !lowercase {
|
||||
return a
|
||||
}
|
||||
newA := make([]string, len(a))
|
||||
for i, v := range a {
|
||||
newA[i] = strings.ToLower(v)
|
||||
}
|
||||
return newA
|
||||
}
|
||||
|
||||
a = RemoveDuplicates(a, lowercase)
|
||||
b = RemoveDuplicates(b, lowercase)
|
||||
|
||||
itemsMap := map[string]bool{}
|
||||
for _, aVal := range a {
|
||||
itemsMap[aVal] = true
|
||||
}
|
||||
|
||||
// Perform difference calculation
|
||||
for _, bVal := range b {
|
||||
if _, ok := itemsMap[bVal]; ok {
|
||||
itemsMap[bVal] = false
|
||||
}
|
||||
}
|
||||
|
||||
items := []string{}
|
||||
for item, exists := range itemsMap {
|
||||
if exists {
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
sort.Strings(items)
|
||||
return items
|
||||
}
|
||||
|
||||
// Printable returns true if all characters in the string are printable
|
||||
// according to Unicode
|
||||
func Printable(s string) bool {
|
||||
return strings.IndexFunc(s, func(c rune) bool {
|
||||
return !unicode.IsPrint(c)
|
||||
}) == -1
|
||||
}
|
||||
|
||||
// StringListToInterfaceList simply takes a []string and turns it into a
|
||||
// []interface{} to satisfy the input requirements for other library functions
|
||||
func StringListToInterfaceList(in []string) []interface{} {
|
||||
ret := make([]interface{}, len(in))
|
||||
for i, v := range in {
|
||||
ret[i] = v
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Reverse reverses the input string
|
||||
func Reverse(in string) string {
|
||||
l := len(in)
|
||||
out := make([]byte, l)
|
||||
for i := 0; i <= l/2; i++ {
|
||||
out[i], out[l-1-i] = in[l-1-i], in[i]
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
@ -1,657 +0,0 @@
|
||||
package strutil
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStrUtil_StrListDelete(t *testing.T) {
|
||||
output := StrListDelete([]string{"item1", "item2", "item3"}, "item1")
|
||||
if StrListContains(output, "item1") {
|
||||
t.Fatal("bad: 'item1' should not have been present")
|
||||
}
|
||||
|
||||
output = StrListDelete([]string{"item1", "item2", "item3"}, "item2")
|
||||
if StrListContains(output, "item2") {
|
||||
t.Fatal("bad: 'item2' should not have been present")
|
||||
}
|
||||
|
||||
output = StrListDelete([]string{"item1", "item2", "item3"}, "item3")
|
||||
if StrListContains(output, "item3") {
|
||||
t.Fatal("bad: 'item3' should not have been present")
|
||||
}
|
||||
|
||||
output = StrListDelete([]string{"item1", "item1", "item3"}, "item1")
|
||||
if !StrListContains(output, "item1") {
|
||||
t.Fatal("bad: 'item1' should have been present")
|
||||
}
|
||||
|
||||
output = StrListDelete(output, "item1")
|
||||
if StrListContains(output, "item1") {
|
||||
t.Fatal("bad: 'item1' should not have been present")
|
||||
}
|
||||
|
||||
output = StrListDelete(output, "random")
|
||||
if len(output) != 1 {
|
||||
t.Fatalf("bad: expected: 1, actual: %d", len(output))
|
||||
}
|
||||
|
||||
output = StrListDelete(output, "item3")
|
||||
if StrListContains(output, "item3") {
|
||||
t.Fatal("bad: 'item3' should not have been present")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrutil_EquivalentSlices(t *testing.T) {
|
||||
slice1 := []string{"test2", "test1", "test3"}
|
||||
slice2 := []string{"test3", "test2", "test1"}
|
||||
if !EquivalentSlices(slice1, slice2) {
|
||||
t.Fatalf("bad: expected a match")
|
||||
}
|
||||
|
||||
slice2 = append(slice2, "test4")
|
||||
if EquivalentSlices(slice1, slice2) {
|
||||
t.Fatalf("bad: expected a mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrutil_ListContainsGlob(t *testing.T) {
|
||||
haystack := []string{
|
||||
"dev",
|
||||
"ops*",
|
||||
"root/*",
|
||||
"*-dev",
|
||||
"_*_",
|
||||
}
|
||||
if StrListContainsGlob(haystack, "tubez") {
|
||||
t.Fatalf("Value shouldn't exist")
|
||||
}
|
||||
if !StrListContainsGlob(haystack, "root/test") {
|
||||
t.Fatalf("Value should exist")
|
||||
}
|
||||
if !StrListContainsGlob(haystack, "ops_test") {
|
||||
t.Fatalf("Value should exist")
|
||||
}
|
||||
if !StrListContainsGlob(haystack, "ops") {
|
||||
t.Fatalf("Value should exist")
|
||||
}
|
||||
if !StrListContainsGlob(haystack, "dev") {
|
||||
t.Fatalf("Value should exist")
|
||||
}
|
||||
if !StrListContainsGlob(haystack, "test-dev") {
|
||||
t.Fatalf("Value should exist")
|
||||
}
|
||||
if !StrListContainsGlob(haystack, "_test_") {
|
||||
t.Fatalf("Value should exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrutil_ListContains(t *testing.T) {
|
||||
haystack := []string{
|
||||
"dev",
|
||||
"ops",
|
||||
"prod",
|
||||
"root",
|
||||
}
|
||||
if StrListContains(haystack, "tubez") {
|
||||
t.Fatalf("Bad")
|
||||
}
|
||||
if !StrListContains(haystack, "root") {
|
||||
t.Fatalf("Bad")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrutil_ListSubset(t *testing.T) {
|
||||
parent := []string{
|
||||
"dev",
|
||||
"ops",
|
||||
"prod",
|
||||
"root",
|
||||
}
|
||||
child := []string{
|
||||
"prod",
|
||||
"ops",
|
||||
}
|
||||
if !StrListSubset(parent, child) {
|
||||
t.Fatalf("Bad")
|
||||
}
|
||||
if !StrListSubset(parent, parent) {
|
||||
t.Fatalf("Bad")
|
||||
}
|
||||
if !StrListSubset(child, child) {
|
||||
t.Fatalf("Bad")
|
||||
}
|
||||
if !StrListSubset(child, nil) {
|
||||
t.Fatalf("Bad")
|
||||
}
|
||||
if StrListSubset(child, parent) {
|
||||
t.Fatalf("Bad")
|
||||
}
|
||||
if StrListSubset(nil, child) {
|
||||
t.Fatalf("Bad")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrutil_ParseKeyValues(t *testing.T) {
|
||||
actual := make(map[string]string)
|
||||
expected := map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
}
|
||||
var input string
|
||||
var err error
|
||||
|
||||
input = "key1=value1,key2=value2"
|
||||
err = ParseKeyValues(input, actual, ",")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Fatalf("bad: expected: %#v\nactual: %#v", expected, actual)
|
||||
}
|
||||
for k := range actual {
|
||||
delete(actual, k)
|
||||
}
|
||||
|
||||
input = "key1 = value1, key2 = value2"
|
||||
err = ParseKeyValues(input, actual, ",")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Fatalf("bad: expected: %#v\nactual: %#v", expected, actual)
|
||||
}
|
||||
for k := range actual {
|
||||
delete(actual, k)
|
||||
}
|
||||
|
||||
input = "key1 = value1, key2 = "
|
||||
err = ParseKeyValues(input, actual, ",")
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error")
|
||||
}
|
||||
for k := range actual {
|
||||
delete(actual, k)
|
||||
}
|
||||
|
||||
input = "key1 = value1, = value2 "
|
||||
err = ParseKeyValues(input, actual, ",")
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error")
|
||||
}
|
||||
for k := range actual {
|
||||
delete(actual, k)
|
||||
}
|
||||
|
||||
input = "key1"
|
||||
err = ParseKeyValues(input, actual, ",")
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrutil_ParseArbitraryKeyValues(t *testing.T) {
|
||||
actual := make(map[string]string)
|
||||
expected := map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
}
|
||||
var input string
|
||||
var err error
|
||||
|
||||
// Test <key>=<value> as comma separated string
|
||||
input = "key1=value1,key2=value2"
|
||||
err = ParseArbitraryKeyValues(input, actual, ",")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Fatalf("bad: expected: %#v\nactual: %#v", expected, actual)
|
||||
}
|
||||
for k := range actual {
|
||||
delete(actual, k)
|
||||
}
|
||||
|
||||
// Test <key>=<value> as base64 encoded comma separated string
|
||||
input = base64.StdEncoding.EncodeToString([]byte(input))
|
||||
err = ParseArbitraryKeyValues(input, actual, ",")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Fatalf("bad: expected: %#v\nactual: %#v", expected, actual)
|
||||
}
|
||||
for k := range actual {
|
||||
delete(actual, k)
|
||||
}
|
||||
|
||||
// Test JSON encoded <key>=<value> tuples
|
||||
input = `{"key1":"value1", "key2":"value2"}`
|
||||
err = ParseArbitraryKeyValues(input, actual, ",")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Fatalf("bad: expected: %#v\nactual: %#v", expected, actual)
|
||||
}
|
||||
for k := range actual {
|
||||
delete(actual, k)
|
||||
}
|
||||
|
||||
// Test base64 encoded JSON string of <key>=<value> tuples
|
||||
input = base64.StdEncoding.EncodeToString([]byte(input))
|
||||
err = ParseArbitraryKeyValues(input, actual, ",")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Fatalf("bad: expected: %#v\nactual: %#v", expected, actual)
|
||||
}
|
||||
for k := range actual {
|
||||
delete(actual, k)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrutil_ParseArbitraryStringSlice(t *testing.T) {
|
||||
input := `CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';GRANT "foo-role" TO "{{name}}";ALTER ROLE "{{name}}" SET search_path = foo;GRANT CONNECT ON DATABASE "postgres" TO "{{name}}";`
|
||||
|
||||
jsonExpected := []string{
|
||||
`DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT * FROM pg_catalog.pg_roles WHERE rolname='foo-role') THEN
|
||||
CREATE ROLE "foo-role";
|
||||
CREATE SCHEMA IF NOT EXISTS foo AUTHORIZATION "foo-role";
|
||||
ALTER ROLE "foo-role" SET search_path = foo;
|
||||
GRANT TEMPORARY ON DATABASE "postgres" TO "foo-role";
|
||||
GRANT ALL PRIVILEGES ON SCHEMA foo TO "foo-role";
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA foo TO "foo-role";
|
||||
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA foo TO "foo-role";
|
||||
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA foo TO "foo-role";
|
||||
END IF;
|
||||
END
|
||||
$$`,
|
||||
`CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'`,
|
||||
`GRANT "foo-role" TO "{{name}}"`,
|
||||
`ALTER ROLE "{{name}}" SET search_path = foo`,
|
||||
`GRANT CONNECT ON DATABASE "postgres" TO "{{name}}"`,
|
||||
``,
|
||||
}
|
||||
|
||||
nonJSONExpected := jsonExpected[1:]
|
||||
|
||||
var actual []string
|
||||
var inputB64 string
|
||||
var err error
|
||||
|
||||
// Test non-JSON string
|
||||
actual = ParseArbitraryStringSlice(input, ";")
|
||||
if !reflect.DeepEqual(nonJSONExpected, actual) {
|
||||
t.Fatalf("bad: expected:\n%#v\nactual:\n%#v", nonJSONExpected, actual)
|
||||
}
|
||||
|
||||
// Test base64-encoded non-JSON string
|
||||
inputB64 = base64.StdEncoding.EncodeToString([]byte(input))
|
||||
actual = ParseArbitraryStringSlice(inputB64, ";")
|
||||
if !reflect.DeepEqual(nonJSONExpected, actual) {
|
||||
t.Fatalf("bad: expected:\n%#v\nactual:\n%#v", nonJSONExpected, actual)
|
||||
}
|
||||
|
||||
// Test JSON encoded
|
||||
inputJSON, err := json.Marshal(jsonExpected)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
actual = ParseArbitraryStringSlice(string(inputJSON), ";")
|
||||
if !reflect.DeepEqual(jsonExpected, actual) {
|
||||
t.Fatalf("bad: expected:\n%#v\nactual:\n%#v", string(inputJSON), actual)
|
||||
}
|
||||
|
||||
// Test base64 encoded JSON string of <key>=<value> tuples
|
||||
inputB64 = base64.StdEncoding.EncodeToString(inputJSON)
|
||||
actual = ParseArbitraryStringSlice(inputB64, ";")
|
||||
if !reflect.DeepEqual(jsonExpected, actual) {
|
||||
t.Fatalf("bad: expected:\n%#v\nactual:\n%#v", jsonExpected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobbedStringsMatch(t *testing.T) {
|
||||
type tCase struct {
|
||||
item string
|
||||
val string
|
||||
expect bool
|
||||
}
|
||||
|
||||
tCases := []tCase{
|
||||
{"", "", true},
|
||||
{"*", "*", true},
|
||||
{"**", "**", true},
|
||||
{"*t", "t", true},
|
||||
{"*t", "test", true},
|
||||
{"t*", "test", true},
|
||||
{"*test", "test", true},
|
||||
{"*test", "a test", true},
|
||||
{"test", "a test", false},
|
||||
{"*test", "tests", false},
|
||||
{"test*", "test", true},
|
||||
{"test*", "testsss", true},
|
||||
{"test**", "testsss", false},
|
||||
{"test**", "test*", true},
|
||||
{"**test", "*test", true},
|
||||
{"TEST", "test", false},
|
||||
{"test", "test", true},
|
||||
}
|
||||
|
||||
for _, tc := range tCases {
|
||||
actual := GlobbedStringsMatch(tc.item, tc.val)
|
||||
|
||||
if actual != tc.expect {
|
||||
t.Fatalf("Bad testcase %#v, expected %t, got %t", tc, tc.expect, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrimStrings(t *testing.T) {
|
||||
input := []string{"abc", "123", "abcd ", "123 "}
|
||||
expected := []string{"abc", "123", "abcd", "123"}
|
||||
actual := TrimStrings(input)
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Fatalf("Bad TrimStrings: expected:%#v, got:%#v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveEmpty(t *testing.T) {
|
||||
input := []string{"abc", "", "abc", ""}
|
||||
expected := []string{"abc", "abc"}
|
||||
actual := RemoveEmpty(input)
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Fatalf("Bad TrimStrings: expected:%#v, got:%#v", expected, actual)
|
||||
}
|
||||
|
||||
input = []string{""}
|
||||
expected = []string{}
|
||||
actual = RemoveEmpty(input)
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Fatalf("Bad TrimStrings: expected:%#v, got:%#v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrutil_AppendIfMissing(t *testing.T) {
|
||||
keys := []string{}
|
||||
|
||||
keys = AppendIfMissing(keys, "foo")
|
||||
|
||||
if len(keys) != 1 {
|
||||
t.Fatalf("expected slice to be length of 1: %v", keys)
|
||||
}
|
||||
if keys[0] != "foo" {
|
||||
t.Fatalf("expected slice to contain key 'foo': %v", keys)
|
||||
}
|
||||
|
||||
keys = AppendIfMissing(keys, "bar")
|
||||
|
||||
if len(keys) != 2 {
|
||||
t.Fatalf("expected slice to be length of 2: %v", keys)
|
||||
}
|
||||
if keys[0] != "foo" {
|
||||
t.Fatalf("expected slice to contain key 'foo': %v", keys)
|
||||
}
|
||||
if keys[1] != "bar" {
|
||||
t.Fatalf("expected slice to contain key 'bar': %v", keys)
|
||||
}
|
||||
|
||||
keys = AppendIfMissing(keys, "foo")
|
||||
|
||||
if len(keys) != 2 {
|
||||
t.Fatalf("expected slice to still be length of 2: %v", keys)
|
||||
}
|
||||
if keys[0] != "foo" {
|
||||
t.Fatalf("expected slice to still contain key 'foo': %v", keys)
|
||||
}
|
||||
if keys[1] != "bar" {
|
||||
t.Fatalf("expected slice to still contain key 'bar': %v", keys)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrUtil_RemoveDuplicates(t *testing.T) {
|
||||
type tCase struct {
|
||||
input []string
|
||||
expect []string
|
||||
lowercase bool
|
||||
}
|
||||
|
||||
tCases := []tCase{
|
||||
{[]string{}, []string{}, false},
|
||||
{[]string{}, []string{}, true},
|
||||
{[]string{"a", "b", "a"}, []string{"a", "b"}, false},
|
||||
{[]string{"A", "b", "a"}, []string{"A", "a", "b"}, false},
|
||||
{[]string{"A", "b", "a"}, []string{"a", "b"}, true},
|
||||
}
|
||||
|
||||
for _, tc := range tCases {
|
||||
actual := RemoveDuplicates(tc.input, tc.lowercase)
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.expect) {
|
||||
t.Fatalf("Bad testcase %#v, expected %v, got %v", tc, tc.expect, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrUtil_RemoveDuplicatesStable(t *testing.T) {
|
||||
type tCase struct {
|
||||
input []string
|
||||
expect []string
|
||||
caseInsensitive bool
|
||||
}
|
||||
|
||||
tCases := []tCase{
|
||||
{[]string{}, []string{}, false},
|
||||
{[]string{}, []string{}, true},
|
||||
{[]string{"a", "b", "a"}, []string{"a", "b"}, false},
|
||||
{[]string{"A", "b", "a"}, []string{"A", "b", "a"}, false},
|
||||
{[]string{"A", "b", "a"}, []string{"A", "b"}, true},
|
||||
{[]string{" ", "d", "c", "d"}, []string{"d", "c"}, false},
|
||||
{[]string{"Z ", " z", " z ", "y"}, []string{"Z ", "y"}, true},
|
||||
{[]string{"Z ", " z", " z ", "y"}, []string{"Z ", " z", "y"}, false},
|
||||
}
|
||||
|
||||
for _, tc := range tCases {
|
||||
actual := RemoveDuplicatesStable(tc.input, tc.caseInsensitive)
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.expect) {
|
||||
t.Fatalf("Bad testcase %#v, expected %v, got %v", tc, tc.expect, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrUtil_ParseStringSlice(t *testing.T) {
|
||||
type tCase struct {
|
||||
input string
|
||||
sep string
|
||||
expect []string
|
||||
}
|
||||
|
||||
tCases := []tCase{
|
||||
{"", "", []string{}},
|
||||
{" ", ",", []string{}},
|
||||
{", ", ",", []string{"", ""}},
|
||||
{"a", ",", []string{"a"}},
|
||||
{" a, b, c ", ",", []string{"a", "b", "c"}},
|
||||
{" a; b; c ", ";", []string{"a", "b", "c"}},
|
||||
{" a :: b :: c ", "::", []string{"a", "b", "c"}},
|
||||
}
|
||||
|
||||
for _, tc := range tCases {
|
||||
actual := ParseStringSlice(tc.input, tc.sep)
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.expect) {
|
||||
t.Fatalf("Bad testcase %#v, expected %v, got %v", tc, tc.expect, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrUtil_MergeSlices(t *testing.T) {
|
||||
res := MergeSlices([]string{"a", "c", "d"}, []string{}, []string{"c", "f", "a"}, nil, []string{"foo"})
|
||||
|
||||
expect := []string{"a", "c", "d", "f", "foo"}
|
||||
|
||||
if !reflect.DeepEqual(res, expect) {
|
||||
t.Fatalf("expected %v, got %v", expect, res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDifference(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Name string
|
||||
SetA []string
|
||||
SetB []string
|
||||
Lowercase bool
|
||||
ExpectedResult []string
|
||||
}{
|
||||
{
|
||||
Name: "case_sensitive",
|
||||
SetA: []string{"a", "b", "c"},
|
||||
SetB: []string{"b", "c"},
|
||||
Lowercase: false,
|
||||
ExpectedResult: []string{"a"},
|
||||
},
|
||||
{
|
||||
Name: "case_insensitive",
|
||||
SetA: []string{"a", "B", "c"},
|
||||
SetB: []string{"b", "C"},
|
||||
Lowercase: true,
|
||||
ExpectedResult: []string{"a"},
|
||||
},
|
||||
{
|
||||
Name: "no_match",
|
||||
SetA: []string{"a", "b", "c"},
|
||||
SetB: []string{"d"},
|
||||
Lowercase: false,
|
||||
ExpectedResult: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
Name: "empty_set_a",
|
||||
SetA: []string{},
|
||||
SetB: []string{"d", "e"},
|
||||
Lowercase: false,
|
||||
ExpectedResult: []string{},
|
||||
},
|
||||
{
|
||||
Name: "empty_set_b",
|
||||
SetA: []string{"a", "b"},
|
||||
SetB: []string{},
|
||||
Lowercase: false,
|
||||
ExpectedResult: []string{"a", "b"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
actualResult := Difference(tc.SetA, tc.SetB, tc.Lowercase)
|
||||
|
||||
if !reflect.DeepEqual(actualResult, tc.ExpectedResult) {
|
||||
t.Fatalf("expected %v, got %v", tc.ExpectedResult, actualResult)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrUtil_EqualStringMaps(t *testing.T) {
|
||||
m1 := map[string]string{
|
||||
"foo": "a",
|
||||
}
|
||||
m2 := map[string]string{
|
||||
"foo": "a",
|
||||
"bar": "b",
|
||||
}
|
||||
var m3 map[string]string
|
||||
|
||||
m4 := map[string]string{
|
||||
"dog": "",
|
||||
}
|
||||
|
||||
m5 := map[string]string{
|
||||
"cat": "",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
a map[string]string
|
||||
b map[string]string
|
||||
result bool
|
||||
}{
|
||||
{m1, m1, true},
|
||||
{m2, m2, true},
|
||||
{m1, m2, false},
|
||||
{m2, m1, false},
|
||||
{m2, m2, true},
|
||||
{m3, m1, false},
|
||||
{m3, m3, true},
|
||||
{m4, m5, false},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
actual := EqualStringMaps(test.a, test.b)
|
||||
if actual != test.result {
|
||||
t.Fatalf("case %d, expected %v, got %v", i, test.result, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintable(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
input: "/valid",
|
||||
},
|
||||
{
|
||||
input: "foobarvalid",
|
||||
},
|
||||
{
|
||||
input: "/invalid\u000A",
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
input: "/invalid\u000D",
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
input: "/invalid\u0000",
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
t.Run(fmt.Sprint(i), func(t *testing.T) {
|
||||
require.True(t, Printable(tc.input) != tc.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReverse(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{
|
||||
in: "abc",
|
||||
out: "cba",
|
||||
},
|
||||
{
|
||||
in: "abcd",
|
||||
out: "dcba",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
t.Run(fmt.Sprint(i), func(t *testing.T) {
|
||||
require.Equal(t, Reverse(tc.in), tc.out)
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue