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 }