Repository navigation
Expand file tree
/
Copy pathutil.go
More file actions
192 lines (179 loc) · 5.47 KB
/
Copy pathutil.go
File metadata and controls
192 lines (179 loc) · 5.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
package config
import (
"encoding"
"net/url"
"reflect"
"strconv"
"strings"
"time"
)
var (
// urlPtrType is the only pointer type supported as a field.
urlPtrType = reflect.TypeFor[*url.URL]()
durationType = reflect.TypeFor[time.Duration]()
textUnmarshalerType = reflect.TypeFor[encoding.TextUnmarshaler]()
)
// isTextUnmarshaler reports whether a pointer to t implements encoding.TextUnmarshaler.
// Such types are always set as a single value, even if their kind is struct or slice.
func isTextUnmarshaler(t reflect.Type) bool {
return reflect.PointerTo(t).Implements(textUnmarshalerType)
}
func mapKeysToFields(ptr reflect.Value, m map[string]reflect.Value, prefix string) {
for fieldType, fieldValue := range ptr.Elem().Fields() {
if !fieldType.IsExported() {
continue // unexported fields cannot be set
}
fieldPtr := fieldValue.Addr()
key := getKey(fieldType, prefix)
switch {
case fieldType.Type.Kind() == reflect.Struct && !isTextUnmarshaler(fieldType.Type):
mapKeysToFields(fieldPtr, m, key+structDelim)
default:
m[key] = fieldPtr
}
}
}
// stringsToMap builds a map from a string slice.
// The input strings are assumed to be environment variable in style e.g. KEY=VALUE
// Keys with no value are not added to the map.
func stringsToMap(ss []string) map[string]string {
m := make(map[string]string)
for _, s := range ss {
key, value, ok := strings.Cut(s, "=")
if ok && key != "" && value != "" {
m[strings.ToLower(key)] = value
}
}
return m
}
// getKey returns the string that represents this structField in the config map.
// If the structField has the appropriate structTag set, it is used.
// Otherwise, field's name is used.
func getKey(t reflect.StructField, prefix string) string {
name := t.Name
if tag, exists := t.Tag.Lookup(structTagKey); exists {
if tag = strings.TrimSpace(tag); tag != "" {
name = tag
}
}
return strings.ToLower(prefix + name)
}
// stringToSlice converts a string to a slice of string, using delim.
// It strips surrounding whitespace of all entries.
// If the input string is empty or all whitespace, nil is returned.
func stringToSlice(s, delim string) []string {
if delim == "" {
panic("empty delimiter") // impossible or programmer error
}
s = strings.TrimSpace(s)
if s == "" {
return nil
}
split := strings.Split(s, delim)
filtered := split[:0] // https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating
for _, v := range split {
v = strings.TrimSpace(v)
if v != "" {
filtered = append(filtered, v)
}
}
return filtered
}
// convertAndSetSlice builds a slice of a dynamic type.
// It converts each entry in "values" to the elemType of the passed in slice,
// and replaces the slice's existing contents with the converted values.
// The slice is left untouched if "values" is empty.
// All values are attempted.
// Returns the errors of failed values, keyed by index.
func convertAndSetSlice(slicePtr reflect.Value, values []string) map[int]error {
if len(values) == 0 {
return nil
}
sliceVal := slicePtr.Elem()
elemType := sliceVal.Type().Elem()
newSlice := reflect.MakeSlice(sliceVal.Type(), 0, len(values))
var failed map[int]error
for i, s := range values {
valuePtr := reflect.New(elemType)
if err := convertAndSetValue(valuePtr, s); err != nil {
if failed == nil {
failed = make(map[int]error)
}
failed[i] = err
} else {
newSlice = reflect.Append(newSlice, valuePtr.Elem())
}
}
sliceVal.Set(newSlice)
return failed
}
// convertAndSetValue receives a settable of an arbitrary kind, and sets its value to s.
// If the settable implements encoding.TextUnmarshaler, its UnmarshalText method is used.
// Otherwise it calls the matching strconv function on s, based on the settable's kind.
// All basic types (bool, int, float, string) are handled by this function.
// Slice and struct are handled elsewhere.
//
// An unhandled kind returns ErrUnsupportedType.
// A failed parse returns a sanitized error that does not contain s,
// to prevent accidental logging of secrets.
func convertAndSetValue(settable reflect.Value, s string) error {
if tu, ok := settable.Interface().(encoding.TextUnmarshaler); ok {
if err := tu.UnmarshalText([]byte(s)); err != nil {
return ErrInvalidValue // UnmarshalText errors may contain s
}
return nil
}
settableValue := settable.Elem()
var (
err error
i int64
u uint64
b bool
f float64
p *url.URL
)
switch settableValue.Kind() {
case reflect.Pointer:
if settableValue.Type() != urlPtrType {
return ErrUnsupportedType
}
p, err = url.Parse(s)
if err == nil {
settableValue.Set(reflect.ValueOf(p))
}
case reflect.String:
settableValue.SetString(s)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if settableValue.Type() == durationType {
var d time.Duration
d, err = time.ParseDuration(s)
i = int64(d)
} else {
i, err = strconv.ParseInt(s, 10, settableValue.Type().Bits())
}
if err == nil {
settableValue.SetInt(i)
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
u, err = strconv.ParseUint(s, 10, settableValue.Type().Bits())
if err == nil {
settableValue.SetUint(u)
}
case reflect.Bool:
b, err = strconv.ParseBool(s)
if err == nil {
settableValue.SetBool(b)
}
case reflect.Float32, reflect.Float64:
f, err = strconv.ParseFloat(s, settableValue.Type().Bits())
if err == nil {
settableValue.SetFloat(f)
}
default:
return ErrUnsupportedType
}
if err != nil {
return sanitize(err)
}
return nil
}