util.go raw

   1  // Copyright © 2014 Steve Francia <spf@spf13.com>.
   2  //
   3  // Use of this source code is governed by an MIT-style
   4  // license that can be found in the LICENSE file.
   5  
   6  // Viper is a application configuration system.
   7  // It believes that applications can be configured a variety of ways
   8  // via flags, ENVIRONMENT variables, configuration files retrieved
   9  // from the file system, or a remote key/value store.
  10  
  11  package viper
  12  
  13  import (
  14  	"fmt"
  15  	"os"
  16  	"path/filepath"
  17  	"runtime"
  18  	"strings"
  19  	"unicode"
  20  
  21  	slog "github.com/sagikazarmark/slog-shim"
  22  	"github.com/spf13/cast"
  23  )
  24  
  25  // ConfigParseError denotes failing to parse configuration file.
  26  type ConfigParseError struct {
  27  	err error
  28  }
  29  
  30  // Error returns the formatted configuration error.
  31  func (pe ConfigParseError) Error() string {
  32  	return fmt.Sprintf("While parsing config: %s", pe.err.Error())
  33  }
  34  
  35  // Unwrap returns the wrapped error.
  36  func (pe ConfigParseError) Unwrap() error {
  37  	return pe.err
  38  }
  39  
  40  // toCaseInsensitiveValue checks if the value is a  map;
  41  // if so, create a copy and lower-case the keys recursively.
  42  func toCaseInsensitiveValue(value any) any {
  43  	switch v := value.(type) {
  44  	case map[any]any:
  45  		value = copyAndInsensitiviseMap(cast.ToStringMap(v))
  46  	case map[string]any:
  47  		value = copyAndInsensitiviseMap(v)
  48  	}
  49  
  50  	return value
  51  }
  52  
  53  // copyAndInsensitiviseMap behaves like insensitiviseMap, but creates a copy of
  54  // any map it makes case insensitive.
  55  func copyAndInsensitiviseMap(m map[string]any) map[string]any {
  56  	nm := make(map[string]any)
  57  
  58  	for key, val := range m {
  59  		lkey := strings.ToLower(key)
  60  		switch v := val.(type) {
  61  		case map[any]any:
  62  			nm[lkey] = copyAndInsensitiviseMap(cast.ToStringMap(v))
  63  		case map[string]any:
  64  			nm[lkey] = copyAndInsensitiviseMap(v)
  65  		default:
  66  			nm[lkey] = v
  67  		}
  68  	}
  69  
  70  	return nm
  71  }
  72  
  73  func insensitiviseVal(val any) any {
  74  	switch v := val.(type) {
  75  	case map[any]any:
  76  		// nested map: cast and recursively insensitivise
  77  		val = cast.ToStringMap(val)
  78  		insensitiviseMap(val.(map[string]any))
  79  	case map[string]any:
  80  		// nested map: recursively insensitivise
  81  		insensitiviseMap(v)
  82  	case []any:
  83  		// nested array: recursively insensitivise
  84  		insensitiveArray(v)
  85  	}
  86  	return val
  87  }
  88  
  89  func insensitiviseMap(m map[string]any) {
  90  	for key, val := range m {
  91  		val = insensitiviseVal(val)
  92  		lower := strings.ToLower(key)
  93  		if key != lower {
  94  			// remove old key (not lower-cased)
  95  			delete(m, key)
  96  		}
  97  		// update map
  98  		m[lower] = val
  99  	}
 100  }
 101  
 102  func insensitiveArray(a []any) {
 103  	for i, val := range a {
 104  		a[i] = insensitiviseVal(val)
 105  	}
 106  }
 107  
 108  func absPathify(logger *slog.Logger, inPath string) string {
 109  	logger.Info("trying to resolve absolute path", "path", inPath)
 110  
 111  	if inPath == "$HOME" || strings.HasPrefix(inPath, "$HOME"+string(os.PathSeparator)) {
 112  		inPath = userHomeDir() + inPath[5:]
 113  	}
 114  
 115  	inPath = os.ExpandEnv(inPath)
 116  
 117  	if filepath.IsAbs(inPath) {
 118  		return filepath.Clean(inPath)
 119  	}
 120  
 121  	p, err := filepath.Abs(inPath)
 122  	if err == nil {
 123  		return filepath.Clean(p)
 124  	}
 125  
 126  	logger.Error(fmt.Errorf("could not discover absolute path: %w", err).Error())
 127  
 128  	return ""
 129  }
 130  
 131  func stringInSlice(a string, list []string) bool {
 132  	for _, b := range list {
 133  		if b == a {
 134  			return true
 135  		}
 136  	}
 137  	return false
 138  }
 139  
 140  func userHomeDir() string {
 141  	if runtime.GOOS == "windows" {
 142  		home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
 143  		if home == "" {
 144  			home = os.Getenv("USERPROFILE")
 145  		}
 146  		return home
 147  	}
 148  	return os.Getenv("HOME")
 149  }
 150  
 151  func safeMul(a, b uint) uint {
 152  	c := a * b
 153  	if a > 1 && b > 1 && c/b != a {
 154  		return 0
 155  	}
 156  	return c
 157  }
 158  
 159  // parseSizeInBytes converts strings like 1GB or 12 mb into an unsigned integer number of bytes.
 160  func parseSizeInBytes(sizeStr string) uint {
 161  	sizeStr = strings.TrimSpace(sizeStr)
 162  	lastChar := len(sizeStr) - 1
 163  	multiplier := uint(1)
 164  
 165  	if lastChar > 0 {
 166  		if sizeStr[lastChar] == 'b' || sizeStr[lastChar] == 'B' {
 167  			if lastChar > 1 {
 168  				switch unicode.ToLower(rune(sizeStr[lastChar-1])) {
 169  				case 'k':
 170  					multiplier = 1 << 10
 171  					sizeStr = strings.TrimSpace(sizeStr[:lastChar-1])
 172  				case 'm':
 173  					multiplier = 1 << 20
 174  					sizeStr = strings.TrimSpace(sizeStr[:lastChar-1])
 175  				case 'g':
 176  					multiplier = 1 << 30
 177  					sizeStr = strings.TrimSpace(sizeStr[:lastChar-1])
 178  				default:
 179  					multiplier = 1
 180  					sizeStr = strings.TrimSpace(sizeStr[:lastChar])
 181  				}
 182  			}
 183  		}
 184  	}
 185  
 186  	size := cast.ToInt(sizeStr)
 187  	if size < 0 {
 188  		size = 0
 189  	}
 190  
 191  	return safeMul(uint(size), multiplier)
 192  }
 193  
 194  // deepSearch scans deep maps, following the key indexes listed in the
 195  // sequence "path".
 196  // The last value is expected to be another map, and is returned.
 197  //
 198  // In case intermediate keys do not exist, or map to a non-map value,
 199  // a new map is created and inserted, and the search continues from there:
 200  // the initial map "m" may be modified!
 201  func deepSearch(m map[string]any, path []string) map[string]any {
 202  	for _, k := range path {
 203  		m2, ok := m[k]
 204  		if !ok {
 205  			// intermediate key does not exist
 206  			// => create it and continue from there
 207  			m3 := make(map[string]any)
 208  			m[k] = m3
 209  			m = m3
 210  			continue
 211  		}
 212  		m3, ok := m2.(map[string]any)
 213  		if !ok {
 214  			// intermediate key is a value
 215  			// => replace with a new map
 216  			m3 = make(map[string]any)
 217  			m[k] = m3
 218  		}
 219  		// continue search from here
 220  		m = m3
 221  	}
 222  	return m
 223  }
 224