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