github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/helper/funcs.go (about)

     1  package helper
     2  
     3  import (
     4  	"crypto/sha512"
     5  	"fmt"
     6  	"math"
     7  	"net/http"
     8  	"path/filepath"
     9  	"reflect"
    10  	"regexp"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	multierror "github.com/hashicorp/go-multierror"
    16  	"github.com/hashicorp/go-set"
    17  	"github.com/hashicorp/hcl/hcl/ast"
    18  	"golang.org/x/exp/constraints"
    19  	"golang.org/x/exp/maps"
    20  	"golang.org/x/exp/slices"
    21  )
    22  
    23  // validUUID is used to check if a given string looks like a UUID
    24  var validUUID = regexp.MustCompile(`(?i)^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$`)
    25  
    26  // validInterpVarKey matches valid dotted variable names for interpolation. The
    27  // string must begin with one or more non-dot characters which may be followed
    28  // by sequences containing a dot followed by a one or more non-dot characters.
    29  var validInterpVarKey = regexp.MustCompile(`^[^.]+(\.[^.]+)*$`)
    30  
    31  // invalidFilename is the minimum set of characters which must be removed or
    32  // replaced to produce a valid filename
    33  var invalidFilename = regexp.MustCompile(`[/\\<>:"|?*]`)
    34  
    35  // invalidFilenameNonASCII = invalidFilename plus all non-ASCII characters
    36  var invalidFilenameNonASCII = regexp.MustCompile(`[[:^ascii:]/\\<>:"|?*]`)
    37  
    38  // invalidFilenameStrict = invalidFilename plus additional punctuation
    39  var invalidFilenameStrict = regexp.MustCompile(`[/\\<>:"|?*$()+=[\];#@~,&']`)
    40  
    41  type Copyable[T any] interface {
    42  	Copy() T
    43  }
    44  
    45  // IsUUID returns true if the given string is a valid UUID.
    46  func IsUUID(str string) bool {
    47  	const uuidLen = 36
    48  	if len(str) != uuidLen {
    49  		return false
    50  	}
    51  
    52  	return validUUID.MatchString(str)
    53  }
    54  
    55  // IsValidInterpVariable returns true if a valid dotted variable names for
    56  // interpolation. The string must begin with one or more non-dot characters
    57  // which may be followed by sequences containing a dot followed by a one or more
    58  // non-dot characters.
    59  func IsValidInterpVariable(str string) bool {
    60  	return validInterpVarKey.MatchString(str)
    61  }
    62  
    63  // HashUUID takes an input UUID and returns a hashed version of the UUID to
    64  // ensure it is well distributed.
    65  func HashUUID(input string) (output string, hashed bool) {
    66  	if !IsUUID(input) {
    67  		return "", false
    68  	}
    69  
    70  	// Hash the input
    71  	buf := sha512.Sum512([]byte(input))
    72  	output = fmt.Sprintf("%08x-%04x-%04x-%04x-%12x",
    73  		buf[0:4],
    74  		buf[4:6],
    75  		buf[6:8],
    76  		buf[8:10],
    77  		buf[10:16])
    78  
    79  	return output, true
    80  }
    81  
    82  // Min returns the minimum of a and b.
    83  func Min[T constraints.Ordered](a, b T) T {
    84  	if a < b {
    85  		return a
    86  	}
    87  	return b
    88  }
    89  
    90  // Max returns the maximum of a and b.
    91  func Max[T constraints.Ordered](a, b T) T {
    92  	if a > b {
    93  		return a
    94  	}
    95  	return b
    96  }
    97  
    98  // UniqueMapSliceValues returns the union of values from each slice in a map[K][]V.
    99  func UniqueMapSliceValues[K, V comparable](m map[K][]V) []V {
   100  	s := set.New[V](0)
   101  	for _, slice := range m {
   102  		s.InsertAll(slice)
   103  	}
   104  	return s.List()
   105  }
   106  
   107  // IsSubset returns whether the smaller set of items is a subset of
   108  // the larger. If the smaller set is not a subset, the offending elements are
   109  // returned.
   110  func IsSubset[T comparable](larger, smaller []T) (bool, []T) {
   111  	l := set.From(larger)
   112  	if l.ContainsAll(smaller) {
   113  		return true, nil
   114  	}
   115  	s := set.From(smaller)
   116  	return false, s.Difference(l).List()
   117  }
   118  
   119  // StringHasPrefixInSlice returns true if s starts with any prefix in list.
   120  func StringHasPrefixInSlice(s string, prefixes []string) bool {
   121  	for _, prefix := range prefixes {
   122  		if strings.HasPrefix(s, prefix) {
   123  			return true
   124  		}
   125  	}
   126  	return false
   127  }
   128  
   129  // IsDisjoint returns whether first and second are disjoint sets, and the set of
   130  // offending elements if not.
   131  func IsDisjoint[T comparable](first, second []T) (bool, []T) {
   132  	f, s := set.From(first), set.From(second)
   133  	intersection := f.Intersect(s)
   134  	if intersection.Size() > 0 {
   135  		return false, intersection.List()
   136  	}
   137  	return true, nil
   138  }
   139  
   140  // DeepCopyMap creates a copy of m by calling Copy() on each value.
   141  //
   142  // If m is nil the return value is nil.
   143  func DeepCopyMap[M ~map[K]V, K comparable, V Copyable[V]](m M) M {
   144  	if m == nil {
   145  		return nil
   146  	}
   147  
   148  	result := make(M, len(m))
   149  	for k, v := range m {
   150  		result[k] = v.Copy()
   151  	}
   152  	return result
   153  }
   154  
   155  // CopySlice creates a deep copy of s. For slices with elements that do not
   156  // implement Copy(), use slices.Clone.
   157  func CopySlice[S ~[]E, E Copyable[E]](s S) S {
   158  	if s == nil {
   159  		return nil
   160  	}
   161  
   162  	result := make(S, len(s))
   163  	for i, v := range s {
   164  		result[i] = v.Copy()
   165  	}
   166  	return result
   167  }
   168  
   169  // MergeMapStringString will merge two maps into one. If a duplicate key exists
   170  // the value in the second map will replace the value in the first map. If both
   171  // maps are empty or nil this returns an empty map.
   172  func MergeMapStringString(m map[string]string, n map[string]string) map[string]string {
   173  	if len(m) == 0 && len(n) == 0 {
   174  		return map[string]string{}
   175  	}
   176  	if len(m) == 0 {
   177  		return n
   178  	}
   179  	if len(n) == 0 {
   180  		return m
   181  	}
   182  
   183  	result := maps.Clone(m)
   184  
   185  	for k, v := range n {
   186  		result[k] = v
   187  	}
   188  
   189  	return result
   190  }
   191  
   192  // CopyMapOfSlice creates a copy of m, making copies of each []V.
   193  func CopyMapOfSlice[K comparable, V any](m map[K][]V) map[K][]V {
   194  	l := len(m)
   195  	if l == 0 {
   196  		return nil
   197  	}
   198  
   199  	c := make(map[K][]V, l)
   200  	for k, v := range m {
   201  		c[k] = slices.Clone(v)
   202  	}
   203  	return c
   204  }
   205  
   206  // CleanEnvVar replaces all occurrences of illegal characters in an environment
   207  // variable with the specified byte.
   208  func CleanEnvVar(s string, r byte) string {
   209  	b := []byte(s)
   210  	for i, c := range b {
   211  		switch {
   212  		case c == '_':
   213  		case c == '.':
   214  		case c >= 'a' && c <= 'z':
   215  		case c >= 'A' && c <= 'Z':
   216  		case i > 0 && c >= '0' && c <= '9':
   217  		default:
   218  			// Replace!
   219  			b[i] = r
   220  		}
   221  	}
   222  	return string(b)
   223  }
   224  
   225  // CleanFilename replaces invalid characters in filename
   226  func CleanFilename(filename string, replace string) string {
   227  	clean := invalidFilename.ReplaceAllLiteralString(filename, replace)
   228  	return clean
   229  }
   230  
   231  // CleanFilenameASCIIOnly replaces invalid and non-ASCII characters in filename
   232  func CleanFilenameASCIIOnly(filename string, replace string) string {
   233  	clean := invalidFilenameNonASCII.ReplaceAllLiteralString(filename, replace)
   234  	return clean
   235  }
   236  
   237  // CleanFilenameStrict replaces invalid and punctuation characters in filename
   238  func CleanFilenameStrict(filename string, replace string) string {
   239  	clean := invalidFilenameStrict.ReplaceAllLiteralString(filename, replace)
   240  	return clean
   241  }
   242  
   243  func CheckHCLKeys(node ast.Node, valid []string) error {
   244  	var list *ast.ObjectList
   245  	switch n := node.(type) {
   246  	case *ast.ObjectList:
   247  		list = n
   248  	case *ast.ObjectType:
   249  		list = n.List
   250  	default:
   251  		return fmt.Errorf("cannot check HCL keys of type %T", n)
   252  	}
   253  
   254  	validMap := make(map[string]struct{}, len(valid))
   255  	for _, v := range valid {
   256  		validMap[v] = struct{}{}
   257  	}
   258  
   259  	var result error
   260  	for _, item := range list.Items {
   261  		key := item.Keys[0].Token.Value().(string)
   262  		if _, ok := validMap[key]; !ok {
   263  			result = multierror.Append(result, fmt.Errorf(
   264  				"invalid key: %s", key))
   265  		}
   266  	}
   267  
   268  	return result
   269  }
   270  
   271  // UnusedKeys returns a pretty-printed error if any `hcl:",unusedKeys"` is not empty
   272  func UnusedKeys(obj interface{}) error {
   273  	val := reflect.ValueOf(obj)
   274  	if val.Kind() == reflect.Ptr {
   275  		val = reflect.Indirect(val)
   276  	}
   277  	return unusedKeysImpl([]string{}, val)
   278  }
   279  
   280  func unusedKeysImpl(path []string, val reflect.Value) error {
   281  	stype := val.Type()
   282  	for i := 0; i < stype.NumField(); i++ {
   283  		ftype := stype.Field(i)
   284  		fval := val.Field(i)
   285  		tags := strings.Split(ftype.Tag.Get("hcl"), ",")
   286  		name := tags[0]
   287  		tags = tags[1:]
   288  
   289  		if fval.Kind() == reflect.Ptr {
   290  			fval = reflect.Indirect(fval)
   291  		}
   292  
   293  		// struct? recurse. Add the struct's key to the path
   294  		if fval.Kind() == reflect.Struct {
   295  			err := unusedKeysImpl(append([]string{name}, path...), fval)
   296  			if err != nil {
   297  				return err
   298  			}
   299  			continue
   300  		}
   301  
   302  		// Search the hcl tags for "unusedKeys"
   303  		unusedKeys := false
   304  		for _, p := range tags {
   305  			if p == "unusedKeys" {
   306  				unusedKeys = true
   307  				break
   308  			}
   309  		}
   310  
   311  		if unusedKeys {
   312  			ks, ok := fval.Interface().([]string)
   313  			if ok && len(ks) != 0 {
   314  				ps := ""
   315  				if len(path) > 0 {
   316  					ps = strings.Join(path, ".") + " "
   317  				}
   318  				return fmt.Errorf("%sunexpected keys %s",
   319  					ps,
   320  					strings.Join(ks, ", "))
   321  			}
   322  		}
   323  	}
   324  	return nil
   325  }
   326  
   327  // RemoveEqualFold removes the first string that EqualFold matches. It updates xs in place
   328  func RemoveEqualFold(xs *[]string, search string) {
   329  	sl := *xs
   330  	for i, x := range sl {
   331  		if strings.EqualFold(x, search) {
   332  			sl = append(sl[:i], sl[i+1:]...)
   333  			if len(sl) == 0 {
   334  				*xs = nil
   335  			} else {
   336  				*xs = sl
   337  			}
   338  			return
   339  		}
   340  	}
   341  }
   342  
   343  // CheckNamespaceScope ensures that the provided namespace is equal to
   344  // or a parent of the requested namespaces. Returns requested namespaces
   345  // which are not equal to or a child of the provided namespace.
   346  func CheckNamespaceScope(provided string, requested []string) []string {
   347  	var offending []string
   348  	for _, ns := range requested {
   349  		rel, err := filepath.Rel(provided, ns)
   350  		if err != nil {
   351  			offending = append(offending, ns)
   352  			// If relative path requires ".." it's not a child
   353  		} else if strings.Contains(rel, "..") {
   354  			offending = append(offending, ns)
   355  		}
   356  	}
   357  	if len(offending) > 0 {
   358  		return offending
   359  	}
   360  	return nil
   361  }
   362  
   363  // StopFunc is used to stop a time.Timer created with NewSafeTimer
   364  type StopFunc func()
   365  
   366  // NewSafeTimer creates a time.Timer but does not panic if duration is <= 0.
   367  //
   368  // Using a time.Timer is recommended instead of time.After when it is necessary
   369  // to avoid leaking goroutines (e.g. in a select inside a loop).
   370  //
   371  // Returns the time.Timer and also a StopFunc, forcing the caller to deal
   372  // with stopping the time.Timer to avoid leaking a goroutine.
   373  //
   374  // Note: If creating a Timer that should do nothing until Reset is called, use
   375  // NewStoppedTimer instead for safely creating the timer in a stopped state.
   376  func NewSafeTimer(duration time.Duration) (*time.Timer, StopFunc) {
   377  	if duration <= 0 {
   378  		// Avoid panic by using the smallest positive value. This is close enough
   379  		// to the behavior of time.After(0), which this helper is intended to
   380  		// replace.
   381  		// https://go.dev/play/p/EIkm9MsPbHY
   382  		duration = 1
   383  	}
   384  
   385  	t := time.NewTimer(duration)
   386  	cancel := func() {
   387  		t.Stop()
   388  	}
   389  
   390  	return t, cancel
   391  }
   392  
   393  // NewStoppedTimer creates a time.Timer in a stopped state. This is useful when
   394  // the actual wait time will computed and set later via Reset.
   395  func NewStoppedTimer() (*time.Timer, StopFunc) {
   396  	t, f := NewSafeTimer(math.MaxInt64)
   397  	t.Stop()
   398  	return t, f
   399  }
   400  
   401  // ConvertSlice takes the input slice and generates a new one using the
   402  // supplied conversion function to covert the element. This is useful when
   403  // converting a slice of strings to a slice of structs which wraps the string.
   404  func ConvertSlice[A, B any](original []A, conversion func(a A) B) []B {
   405  	result := make([]B, len(original))
   406  	for i, element := range original {
   407  		result[i] = conversion(element)
   408  	}
   409  	return result
   410  }
   411  
   412  // IsMethodHTTP returns whether s is a known HTTP method, ignoring case.
   413  func IsMethodHTTP(s string) bool {
   414  	switch strings.ToUpper(s) {
   415  	case http.MethodGet:
   416  	case http.MethodHead:
   417  	case http.MethodPost:
   418  	case http.MethodPut:
   419  	case http.MethodPatch:
   420  	case http.MethodDelete:
   421  	case http.MethodConnect:
   422  	case http.MethodOptions:
   423  	case http.MethodTrace:
   424  	default:
   425  		return false
   426  	}
   427  	return true
   428  }
   429  
   430  // EqualFunc represents a type implementing the Equal method.
   431  type EqualFunc[A any] interface {
   432  	Equal(A) bool
   433  }
   434  
   435  // ElementsEqual returns true if slices a and b contain the same elements (in
   436  // no particular order) using the Equal function defined on their type for
   437  // comparison.
   438  func ElementsEqual[T EqualFunc[T]](a, b []T) bool {
   439  	if len(a) != len(b) {
   440  		return false
   441  	}
   442  OUTER:
   443  	for _, item := range a {
   444  		for _, other := range b {
   445  			if item.Equal(other) {
   446  				continue OUTER
   447  			}
   448  		}
   449  		return false
   450  	}
   451  	return true
   452  }
   453  
   454  // SliceSetEq returns true if slices a and b contain the same elements (in no
   455  // particular order), using '==' for comparison.
   456  //
   457  // Note: for pointers, consider implementing an Equal method and using
   458  // ElementsEqual instead.
   459  func SliceSetEq[T comparable](a, b []T) bool {
   460  	lenA, lenB := len(a), len(b)
   461  	if lenA != lenB {
   462  		return false
   463  	}
   464  
   465  	if lenA > 10 {
   466  		// avoid quadratic comparisons over large input
   467  		return set.From(a).EqualSlice(b)
   468  	}
   469  
   470  OUTER:
   471  	for _, item := range a {
   472  		for _, other := range b {
   473  			if item == other {
   474  				continue OUTER
   475  			}
   476  		}
   477  		return false
   478  	}
   479  	return true
   480  }
   481  
   482  // WithLock executes a function while holding a lock.
   483  func WithLock(lock sync.Locker, f func()) {
   484  	lock.Lock()
   485  	defer lock.Unlock()
   486  	f()
   487  }