Files
mgmt/util/util.go
James Shubin d30ff6cfae legal: Remove year
Instead of constantly making these updates, let's just remove the year
since things are stored in git anyways, and this is not an actual modern
legal risk anymore.
2025-01-26 16:24:51 -05:00

809 lines
23 KiB
Go

// Mgmt
// Copyright (C) James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
// Package util contains a collection of miscellaneous utility functions.
package util
import (
"bytes"
"context"
"encoding/base64"
"encoding/gob"
"fmt"
"path"
"sort"
"strings"
"time"
"github.com/godbus/dbus/v5"
"github.com/purpleidea/mgmt/util/errwrap"
)
// NumToAlpha returns a lower case string of letters representing a number. If
// you specify 0, you'll get `a`, 25 gives you `z`, and 26 gives you `aa` and so
// on...
func NumToAlpha(idx int) string {
if idx < 0 {
return "" // don't error or produce junk
}
var mod = idx % 26
var div = idx / 26
if div > 0 {
return NumToAlpha(div-1) + string(rune(mod+int('a')))
}
return string(rune(mod + int('a')))
}
// FirstToUpper returns the string with the first character capitalized.
func FirstToUpper(str string) string {
if str == "" {
return ""
}
return strings.ToUpper(str[0:1]) + str[1:]
}
// StrInList returns true if a string exists inside a list, otherwise false.
func StrInList(needle string, haystack []string) bool {
for _, x := range haystack {
if needle == x {
return true
}
}
return false
}
// Uint64KeyFromStrInMap returns true if needle is found in haystack of keys
// that have uint64 type.
func Uint64KeyFromStrInMap(needle string, haystack map[uint64]string) (uint64, bool) {
for k, v := range haystack {
if v == needle {
return k, true
}
}
return 0, false
}
// StrRemoveDuplicatesInList removes any duplicate values in the list. This
// implementation is possibly sub-optimal (O(n^2)?) but preserves ordering.
func StrRemoveDuplicatesInList(list []string) []string {
unique := []string{}
for _, x := range list {
if !StrInList(x, unique) {
unique = append(unique, x)
}
}
return unique
}
// StrFilterElementsInList removes any of the elements in filter, if they exist
// in the list.
func StrFilterElementsInList(filter []string, list []string) []string {
result := []string{}
for _, x := range list {
if !StrInList(x, filter) {
result = append(result, x)
}
}
return result
}
// StrListIntersection removes any of the elements in filter, if they don't
// exist in the list. This is an in order intersection of two lists.
func StrListIntersection(list1 []string, list2 []string) []string {
result := []string{}
for _, x := range list1 {
if StrInList(x, list2) {
result = append(result, x)
}
}
return result
}
// ReverseStringList reverses a list of strings.
func ReverseStringList(in []string) []string {
var out []string // empty list
l := len(in)
for i := range in {
out = append(out, in[l-i-1])
}
return out
}
// StrMapKeys return the sorted list of string keys in a map with string keys.
// NOTE: i thought it would be nice for this to use: map[string]interface{} but
// it turns out that's not allowed. I know we don't have generics, but come on!
func StrMapKeys(m map[string]string) []string {
result := []string{}
for k := range m {
result = append(result, k)
}
sort.Strings(result) // deterministic order
return result
}
// StrMapKeysUint64 return the sorted list of string keys in a map with string
// keys but uint64 values.
func StrMapKeysUint64(m map[string]uint64) []string {
result := []string{}
for k := range m {
result = append(result, k)
}
sort.Strings(result) // deterministic order
return result
}
// BoolMapValues returns the sorted list of bool values in a map with string
// values.
func BoolMapValues(m map[string]bool) []bool {
result := []bool{}
for _, v := range m {
result = append(result, v)
}
//sort.Bools(result) // TODO: deterministic order
return result
}
// StrMapValues returns the sorted list of string values in a map with string
// values.
func StrMapValues(m map[string]string) []string {
result := []string{}
for _, v := range m {
result = append(result, v)
}
sort.Strings(result) // deterministic order
return result
}
// StrMapValuesUint64 return the sorted list of string values in a map with
// string values.
func StrMapValuesUint64(m map[uint64]string) []string {
result := []string{}
for _, v := range m {
result = append(result, v)
}
sort.Strings(result) // deterministic order
return result
}
// BoolMapTrue returns true if everyone in the list is true.
func BoolMapTrue(l []bool) bool {
for _, b := range l {
if !b {
return false
}
}
return true
}
// Dirname is similar to the GNU dirname command.
func Dirname(p string) string {
if p == "/" {
return ""
}
d, _ := path.Split(path.Clean(p))
return d
}
// Basename is the base of a path string.
func Basename(p string) string {
_, b := path.Split(path.Clean(p))
if p == "" {
return ""
}
if p[len(p)-1:] == "/" { // don't lose the tail slash
b += "/"
}
return b
}
// PathSplit splits a path into an array of tokens excluding any trailing empty
// tokens.
func PathSplit(p string) []string {
if p == "/" { // TODO: can't this all be expressed nicely in one line?
return []string{""}
}
return strings.Split(path.Clean(p), "/")
}
// HasPathPrefix tells us if a path string contain the given path prefix in it.
func HasPathPrefix(p, prefix string) bool {
patharray := PathSplit(p)
prefixarray := PathSplit(prefix)
if len(prefixarray) > len(patharray) {
return false
}
for i := 0; i < len(prefixarray); i++ {
if prefixarray[i] != patharray[i] {
return false
}
}
return true
}
// StrInPathPrefixList returns true if the needle is a PathPrefix in the
// haystack.
func StrInPathPrefixList(needle string, haystack []string) bool {
for _, x := range haystack {
if HasPathPrefix(x, needle) {
return true
}
}
return false
}
// RemoveCommonFilePrefixes removes redundant file path prefixes that are under
// the tree of other files.
func RemoveCommonFilePrefixes(paths []string) []string {
var result = make([]string, len(paths))
for i := 0; i < len(paths); i++ { // copy, b/c append can modify the args!!
result[i] = paths[i]
}
// is there a string path which is common everywhere?
// if so, remove it, and iterate until nothing common is left
// return what's left over, that's the most common superset
loop:
for {
if len(result) <= 1 {
return result
}
for i := 0; i < len(result); i++ {
var copied = make([]string, len(result))
for j := 0; j < len(result); j++ { // copy, b/c append can modify the args!!
copied[j] = result[j]
}
noi := append(copied[:i], copied[i+1:]...) // rm i
if StrInPathPrefixList(result[i], noi) {
// delete the element common to everyone
result = noi
continue loop
}
}
break
}
return result
}
// SafePathClean does path.Clean, but it preserves any trailing slash if it was
// present in the initial path.
func SafePathClean(s string) string {
hasSlash := strings.HasSuffix(s, "/")
clean := path.Clean(s) // removes trailing slashes
if hasSlash { // add it back if it was taken off
clean = clean + "/"
}
return clean
}
// SegmentedPathSplit splits an absolute path into chunks including their slash.
// This is similar to the PathSplit function, but the slashes are not lost here!
// TODO: There is likely a more efficient implementation of this function.
func SegmentedPathSplit(p string) []string {
out := []string{}
sponge := ""
for i := 0; i < len(p); i++ {
sponge = sponge + string(p[i])
if string(p[i]) == "/" {
out = append(out, sponge) // found one!
sponge = "" // reset
}
}
if sponge != "" {
out = append(out, sponge) // the last piece
}
return out
}
// CommonPathPrefix returns the longest common prefix directory out of all the
// input paths! This is always a directory and thus ends with a slash, unless
// all of the paths are identical and are files, or if only one path is given in
// which case that is returned, or unless no paths are given in which case the
// empty string is returned. If any of your input paths are not absolute, and as
// such do not begin with a slash, then the behaviour is undefined.
func CommonPathPrefix(paths ...string) string {
// XXX: I am not a good algorithmist, there is probably a more efficient
// way to write this algorithm. Patches are welcome!
if len(paths) == 0 {
return ""
}
if len(paths) == 1 {
return paths[0]
}
sps := make([][]string, 0)
for _, x := range paths {
if !strings.HasPrefix(x, "/") {
// TODO: panic? error?
return "" // undefined behaviour!
}
//x = SafePathClean(x) // TODO: Should we "safe clean" each path?
sp := SegmentedPathSplit(x) // compute once in advance
sps = append(sps, sp)
}
z := sps[0] // SegmentedPathSplit(paths[0]) // arbitrarily choose the first one
ix := 0
ret := ""
for {
for i := range paths {
sp := sps[i] // SegmentedPathSplit(paths[i])
if len(sp) <= ix { // one path is longer
return ret
}
if sp[ix] != z[ix] { // the chunk differs!
return ret
}
}
ret = ret + z[ix] // append what we've got so far!
ix++
}
}
// PathPrefixDelta returns the delta of the path prefix, which tells you how
// many path tokens different the prefix is.
func PathPrefixDelta(p, prefix string) int {
if !HasPathPrefix(p, prefix) {
return -1
}
patharray := PathSplit(p)
prefixarray := PathSplit(prefix)
return len(patharray) - len(prefixarray)
}
// PathSplitFullReversed returns the full list of "dependency" paths for a given
// path in reverse order.
func PathSplitFullReversed(p string) []string {
var result []string
split := PathSplit(p)
count := len(split)
var x string
for i := 0; i < count; i++ {
x = "/" + path.Join(split[0:i+1]...)
if i != 0 && !(i+1 == count && !strings.HasSuffix(p, "/")) {
x += "/" // add trailing slash
}
result = append(result, x)
}
return ReverseStringList(result)
}
// DirifyFileList adds trailing slashes to any likely dirs in a package manager
// fileList if removeDirs is true, otherwise, don't keep the dirs in our output.
func DirifyFileList(fileList []string, removeDirs bool) []string {
dirs := []string{}
for _, file := range fileList {
dir, _ := path.Split(file) // dir
dir = path.Clean(dir) // clean so cmp is easier
if !StrInList(dir, dirs) {
dirs = append(dirs, dir)
}
}
result := []string{}
for _, file := range fileList {
cleanFile := path.Clean(file)
if !StrInList(cleanFile, dirs) { // we're not a directory!
result = append(result, file) // pass through
} else if !removeDirs {
result = append(result, cleanFile+"/")
}
}
return result
}
// FlattenListWithSplit flattens a list of input by splitting each element by
// any and all of the strings listed in the split array
func FlattenListWithSplit(input []string, split []string) []string {
if len(split) == 0 { // nothing to split by
return input
}
out := []string{}
for _, x := range input {
var s []string
if len(split) == 1 {
s = strings.Split(x, split[0]) // split by only string
} else {
s = []string{x} // initial
for i := range split {
s = FlattenListWithSplit(s, []string{split[i]}) // recurse
}
}
out = append(out, s...)
}
return out
}
// RemoveBasePath removes an absolute base path (directory prefix) from an
// absolute path that is any file or directory.
// Eg: RemoveBasePath("/usr/bin/foo", "/usr/") -> "bin/foo"
// Eg: RemoveBasePath("/usr/bin/project/", "/usr/") -> "bin/project/".
func RemoveBasePath(path, base string) (string, error) {
if !strings.HasSuffix(base, "/") { // should end with a slash
return "", fmt.Errorf("base is not a directory")
}
if !strings.HasPrefix(path, base) {
return "", fmt.Errorf("path does not have base prefix")
}
return strings.TrimPrefix(path, base), nil
}
// Rebase takes an absolute base path (directory prefix) and removes it from an
// absolute path and then returns that path with a new root as an absolute path
// if root is an absolute dir, and as a relative path if root is a relative dir.
// Eg: Rebase("/usr/bin/foo", "/usr/", "/usr/local/") -> "/usr/local/bin/foo"
// Eg: Rebase("/var/lib/dir/file.conf", "/var/lib/", "") -> "dir/file.conf"
func Rebase(path, base, root string) (string, error) {
// allow empty root's to rebase into a relative path if not a directory!
if root != "" && !strings.HasSuffix(root, "/") { // should end with a slash
return "", fmt.Errorf("root is not a directory")
}
s, err := RemoveBasePath(path, base)
if err != nil {
return "", err
}
return root + s, nil
}
// RemovePathPrefix takes an absolute path and removes the first chunk. It
// returns the remainder as an absolute path. This function is a bit of a hack,
// and could probably be re-written to support any kind of path, and return a
// relative path.
func RemovePathPrefix(s string) (string, error) {
if !strings.HasPrefix(s, "/") {
return "", fmt.Errorf("must be absolute")
}
// this is the PathSplit logic...
x := []string{""} // assumes s == "/"
if s != "/" {
x = strings.Split(s, "/")
}
x = x[2:] // get rid of first two chunks, first is / and second is a dir name
return "/" + strings.Join(x, "/"), nil
}
// RemovePathSuffix takes an absolute path and removes the last chunk. It
// returns the remainder as an absolute path. This function is a bit of a hack,
// and could probably be re-written to support any kind of path, and return a
// relative path.
func RemovePathSuffix(s string) (string, error) {
if !strings.HasPrefix(s, "/") {
return "", fmt.Errorf("must be absolute")
}
// this is the PathSplit logic...
if s == "/" {
//return "", nil // TODO: return this instead?
return "", fmt.Errorf("input is /")
}
x := strings.Split(s, "/")
// get rid of the last two chunks, last is / and second to last is a dir
if strings.HasSuffix(s, "/") {
_, x = x[len(x)-1], x[:len(x)-1] // pop the last slash
}
_, x = x[len(x)-1], x[:len(x)-1] // pop the last chunk
return strings.Join(x, "/") + "/", nil
}
// DirParents returns a list of the parent directories in a given path. If you
// pass it an empty string, or a single slash, then you will get an empty list.
// If you pass it a malformed path, then you might get unexpected results.
func DirParents(p string) []string {
if p == "" {
return nil // TODO: should we error?
}
if p == "/" {
return []string{}
}
d := Dirname(p)
x := DirParents(d)
x = append(x, d)
return x
}
// MissingMkdirs takes a list of paths, and returns a list of any missing paths
// that would be needed to avoid having to `mkdir -p` to prevent missing parent
// directory errors from happening. This adds paths all the way up to the root,
// but without including it, because it's implied.
// TODO: do we want to include the root?
// TODO: this could probably be implemented in a more efficient way...
func MissingMkdirs(input []string) ([]string, error) {
dirs := []string{}
for _, p := range input {
if p == "/" {
continue
}
d := Dirname(p)
dirs = append(dirs, d)
if strings.HasSuffix(p, "/") { // it's a dir
dirs = append(dirs, p)
}
}
// TODO: remove duplicates for efficiency?
result := []string{}
for _, d := range dirs {
p := DirParents(d) // TODO: memoize
p = append(p, d) // include self
for _, x := range p {
if StrInList(x, input) {
continue
}
result = append(result, x)
}
}
out := StrRemoveDuplicatesInList(result) // avoid duplicates
sort.Sort(PathSlice(out))
return out, nil
}
// TimeAfterOrBlock is aspecial version of time.After that blocks when given a
// negative integer. When used in a case statement, the timer restarts on each
// select call to it.
func TimeAfterOrBlock(t int) <-chan time.Time {
if t < 0 {
return make(chan time.Time) // blocks forever
}
return time.After(time.Duration(t) * time.Second)
}
// TimeAfterOrBlockCtx returns a channel that closes after a timeout. If you use
// a negative timeout, it will block forever. It can also unblock using context.
// Make sure to cancel the context when you're done, or you'll leak a goroutine.
func TimeAfterOrBlockCtx(ctx context.Context, t int) <-chan struct{} {
ch := make(chan struct{})
go func() {
defer close(ch)
if t < 0 {
select {
case <-ctx.Done():
}
return
}
select {
case <-time.After(time.Duration(t) * time.Second):
case <-ctx.Done():
}
}()
return ch
}
// CloseAfter takes a duration, similarly to `time.After`, and returns a channel
// that closes when either the context is done, or the duration expires.
func CloseAfter(ctx context.Context, d time.Duration) <-chan struct{} {
ch := make(chan struct{})
go func() {
defer close(ch)
select {
case <-time.After(d):
// done
case <-ctx.Done():
}
}()
return ch
}
// SystemBusPrivateUsable makes using the private bus usable.
// TODO: should be upstream: https://github.com/godbus/dbus/issues/15
func SystemBusPrivateUsable() (conn *dbus.Conn, err error) {
conn, err = dbus.SystemBusPrivate()
if err != nil {
return nil, err
}
if err = conn.Auth(nil); err != nil {
conn.Close()
conn = nil
return
}
if err = conn.Hello(); err != nil {
conn.Close()
conn = nil
}
return conn, nil // success
}
// SessionBusPrivateUsable makes using the private bus usable.
// TODO: should be upstream: https://github.com/godbus/dbus/issues/15
func SessionBusPrivateUsable() (conn *dbus.Conn, err error) {
conn, err = dbus.SessionBusPrivate()
if err != nil {
return nil, err
}
if err = conn.Auth(nil); err != nil {
conn.Close()
conn = nil
return
}
if err = conn.Hello(); err != nil {
conn.Close()
conn = nil
}
return conn, nil // success
}
// PriorityStrSliceSort filters any elements matching fn to the end of the list.
// You can reverse the match result with a not to filter to the front instead! A
// copy of the list is returned, the original is not modified.
func PriorityStrSliceSort(input []string, fn func(string) bool) []string {
output := []string{}
found := []string{}
for _, x := range input {
if fn(x) { // if we find the key, don't include it just yet
found = append(found, x) // save for later
continue
}
output = append(output, x)
}
// include the keys at the end (if found)
output = append(output, found...)
return output
}
// SortedStrSliceCompare takes two lists of strings and returns whether or not
// they are equivalent. It will return nil if both sets contain the same
// elements, regardless of order, and an error if they do not.
func SortedStrSliceCompare(a, b []string) error {
if len(a) != len(b) {
return fmt.Errorf("slices have different lengths: %d vs %d", len(a), len(b))
}
// make a copy of each to sort, so we don't reorder the inputs
x := make([]string, len(a))
y := make([]string, len(b))
copy(x, a)
copy(y, b)
sort.Strings(x)
sort.Strings(y)
for i := range x {
if x[i] != y[i] {
return fmt.Errorf("values do not match: %s vs %s", x[i], y[i])
}
}
return nil
}
// PathSlice is a type used to implement sort.Interface on a slice of strings,
// where each string is a path. This allows you to call sort.Sort() on a list of
// paths, after casting the []string{} to this type. Paths will be sorted by
// depth in alphabetical order.
type PathSlice []string
// Len returns the length of obj. It is required to satisfy sort.Interface.
func (obj PathSlice) Len() int {
return len(obj)
}
// Swap swaps obj[i] and obj[j]. it is required to satisfy sort.Interface.
func (obj PathSlice) Swap(i, j int) {
obj[i], obj[j] = obj[j], obj[i]
}
// Less returns whether obj[i] is less than obj[j]. It performs the logic
// required to satisfy sort.Interface.
func (obj PathSlice) Less(i, j int) bool {
x := PathSplitFullReversed(obj[i])
y := PathSplitFullReversed(obj[j])
if x[0] != y[0] {
return x[0] < y[0]
}
if len(x) > len(y) {
return false
}
for i := range x {
if x[i] > y[i] {
return false
}
}
return true
}
// Sort is a convenience method.
func (obj PathSlice) Sort() { sort.Sort(obj) }
// UInt64Slice attaches the methods of sort.Interface to []uint64, sorting in
// increasing order.
type UInt64Slice []uint64
// Len returns the length of the slice of uint64's.
func (obj UInt64Slice) Len() int { return len(obj) }
// Swap swaps two elements in the slice.
func (obj UInt64Slice) Swap(i, j int) { obj[i], obj[j] = obj[j], obj[i] }
// Less returns the smaller element in the sort order.
func (obj UInt64Slice) Less(i, j int) bool { return obj[i] < obj[j] }
// Sort is a convenience method.
func (obj UInt64Slice) Sort() { sort.Sort(obj) }
// SortMapStringValuesByUInt64Keys builds a list of strings, sorted by the
// corresponding key that is associated with that value.
// TODO: add some tests
func SortMapStringValuesByUInt64Keys(m map[uint64]string) []string {
//if m == nil { // no need to special case this, range handles it safely
// return []string{}
//}
keys := []uint64{}
for i := range m {
keys = append(keys, i)
}
sort.Sort(UInt64Slice(keys))
result := []string{}
for _, key := range keys {
s := m[key]
result = append(result, s)
}
return result
}
// ValueToB64 encodes a value to a base64 encoded string (after serialization).
func ValueToB64(value interface{}) (string, error) {
b := bytes.Buffer{}
e := gob.NewEncoder(&b)
if err := e.Encode(&value); err != nil { // pass with &
return "", errwrap.Wrapf(err, "gob failed to encode")
}
return base64.StdEncoding.EncodeToString(b.Bytes()), nil
}
// B64ToValue decodes a value from a base64 encoded string (after
// deserialization).
func B64ToValue(str string) (interface{}, error) {
var output interface{}
bb, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return nil, errwrap.Wrapf(err, "base64 failed to decode")
}
b := bytes.NewBuffer(bb)
d := gob.NewDecoder(b)
if err := d.Decode(&output); err != nil { // pass with &
return nil, errwrap.Wrapf(err, "gob failed to decode")
}
value, ok := output.(interface{})
if !ok {
return nil, fmt.Errorf("output `%v` is not a value", output)
}
return value, nil
}