Repository navigation
Expand file tree
/
Copy pathmain_config.go
More file actions
255 lines (234 loc) · 7.77 KB
/
Copy pathmain_config.go
File metadata and controls
255 lines (234 loc) · 7.77 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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
package main
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
)
// Defaults beyond the built-in ones come from a JSON config file and from
// SUBENUM_* environment variables (#88). Precedence, highest first:
// command-line flag > environment > config file > built-in default.
// configEnv overrides the config file location (also used by tests).
const configEnv = "SUBENUM_CONFIG"
// errConfig marks an invalid config file or environment value; run reports
// it as a usage error.
var errConfig = errors.New("invalid configuration")
// Setting sources, as -print-config reports them.
const (
sourceDefault = "default"
sourceConfig = "config"
sourceEnv = "env"
sourceFlag = "flag"
)
// noDefaultFlags are never taken from the config file or environment: they
// select a mode or an action rather than a setting.
var noDefaultFlags = map[string]bool{"tui": true, "version": true, "print-config": true, "dry-run": true, "resume": true, "h": true, "help": true}
// listFlags take comma-separated values, so the config file may also give
// them as a JSON array of strings.
var listFlags = map[string]bool{"exclude": true, "type": true}
// sourceRank orders sources by precedence, highest last.
var sourceRank = map[string]int{sourceDefault: 0, sourceConfig: 1, sourceEnv: 2, sourceFlag: 3}
// configValue turns one config-file value into a flag value. Only strings,
// numbers and booleans are settings; fmt.Sprint of null or an object would
// silently become a bogus value such as "<nil>".
func configValue(name string, v any) (string, error) {
switch v := v.(type) {
case string:
return v, nil
case json.Number, bool:
return fmt.Sprint(v), nil
case []any:
if listFlags[name] {
parts := make([]string, len(v))
for i, p := range v {
s, ok := p.(string)
if !ok {
return "", fmt.Errorf("list entries must be strings")
}
parts[i] = s
}
return strings.Join(parts, ","), nil
}
}
if listFlags[name] {
return "", fmt.Errorf("must be a string or a list of strings")
}
return "", fmt.Errorf("must be a string, number or boolean")
}
// defaultsLoader applies config-file and environment defaults to a FlagSet
// and remembers where each value came from.
type defaultsLoader struct {
path string // config file; "" means none
getenv func(string) string // os.Getenv, or a fake in tests
sources map[string]string // flag name -> source
values map[string]string // flag name -> raw value applied
}
// newDefaultsLoader uses $SUBENUM_CONFIG, else <user config dir>/subenum/config.json.
func newDefaultsLoader() *defaultsLoader {
path := os.Getenv(configEnv)
if path == "" {
if dir, err := os.UserConfigDir(); err == nil {
path = filepath.Join(dir, "subenum", "config.json")
}
}
return &defaultsLoader{path: path, getenv: os.Getenv}
}
// envName maps a flag to its environment variable: dns-server -> SUBENUM_DNS_SERVER.
func envName(flagName string) string {
return "SUBENUM_" + strings.ToUpper(strings.ReplaceAll(flagName, "-", "_"))
}
// apply sets config-file, then environment values on fs without marking
// them as given on the command line, so explicit-only checks keep working.
// A bad value does not stop the others from being applied: every problem is
// returned, joined, so one run reports them all.
func (d *defaultsLoader) apply(fs *flag.FlagSet) error {
d.sources = map[string]string{}
d.values = map[string]string{}
var errs []error
set := func(name, value, source string) bool {
if err := fs.Lookup(name).Value.Set(value); err != nil {
errs = append(errs, fmt.Errorf("%w: %s value %q for %s: %w", errConfig, source, value, name, err))
return false
}
if v, ok := fs.Lookup(name).Value.(interface{ markDefault() }); ok {
v.markDefault()
}
d.values[name] = value
return true
}
if d.path != "" {
data, err := os.ReadFile(d.path)
switch {
case errors.Is(err, os.ErrNotExist):
case err != nil:
errs = append(errs, fmt.Errorf("%w: reading %s: %w", errConfig, d.path, err))
default:
var cfg map[string]any
// UseNumber keeps 1000000 as "1000000", not float64's "1e+06".
dec := json.NewDecoder(bytes.NewReader(data))
dec.UseNumber()
if err := dec.Decode(&cfg); err != nil {
errs = append(errs, fmt.Errorf("%w: %s: %w", errConfig, d.path, err))
break
}
names := make([]string, 0, len(cfg))
for name := range cfg {
names = append(names, name)
}
sort.Strings(names)
source := sourceConfig + " " + d.path
for _, name := range names {
switch {
case noDefaultFlags[name]:
errs = append(errs, fmt.Errorf("%w: %s sets %q, which is not allowed in a config file (give it on the command line)", errConfig, source, name))
continue
case fs.Lookup(name) == nil:
errs = append(errs, fmt.Errorf("%w: %s sets unknown setting %q", errConfig, source, name))
continue
}
value, err := configValue(name, cfg[name])
if err != nil {
errs = append(errs, fmt.Errorf("%w: %s value for %s %w", errConfig, source, name, err))
continue
}
if set(name, value, source) {
d.sources[name] = sourceConfig
}
}
}
}
fs.VisitAll(func(fl *flag.Flag) {
if noDefaultFlags[fl.Name] {
return
}
if v := d.getenv(envName(fl.Name)); v != "" && set(fl.Name, v, envName(fl.Name)) {
d.sources[fl.Name] = sourceEnv
}
})
return errors.Join(errs...)
}
// source reports where a flag's effective value came from: flag, env,
// config or default.
func (d *defaultsLoader) source(fs *flag.FlagSet, name string) string {
if flagSet(fs, name) {
return sourceFlag
}
if d != nil {
if s, ok := d.sources[name]; ok {
return s
}
}
return sourceDefault
}
// describe names where a non-default setting came from, for error messages:
// "-r", "SUBENUM_R" or "r in the config file".
func (d *defaultsLoader) describe(fs *flag.FlagSet, name string) string {
switch d.source(fs, name) {
case sourceEnv:
return envName(name)
case sourceConfig:
return fmt.Sprintf("%q in the config file", name)
default:
return "-" + name
}
}
// printConfig writes every setting's effective value and its source.
func (d *defaultsLoader) printConfig(w io.Writer, fset *flag.FlagSet) {
given := map[string]bool{}
fset.Visit(func(fl *flag.Flag) { given[fl.Name] = true })
if d.path != "" {
_, _ = fmt.Fprintf(w, "# config file: %s\n", d.path)
}
fset.VisitAll(func(fl *flag.Flag) {
if noDefaultFlags[fl.Name] {
return
}
source := sourceDefault
if s, ok := d.sources[fl.Name]; ok {
source = s
}
if given[fl.Name] {
source = sourceFlag
}
value := fl.Value.String()
if fl.Name == "attempts" {
value, source = d.effectiveAttempts(fset, value, source)
}
_, _ = fmt.Fprintf(w, "%-14s = %-24q (%s)\n", fl.Name, value, source)
})
}
// effectiveAttempts resolves -attempts, whose flag value 0 means "unset", to
// the value a scan uses, merging the deprecated -retries (#153).
func (d *defaultsLoader) effectiveAttempts(fset *flag.FlagSet, value, source string) (string, string) {
attempts, aerr := strconv.Atoi(value)
retries, rerr := strconv.Atoi(fset.Lookup("retries").Value.String())
if aerr != nil || rerr != nil {
return value, source
}
n, fromRetries, err := effectiveAttempts(attempts, retries, d.source(fset, "attempts"), d.source(fset, "retries"))
if err != nil {
return value, source
}
if fromRetries {
source = d.source(fset, "retries") + ", from retries"
}
return strconv.Itoa(n), source
}
// settings returns the effective value of every setting that came from the
// environment or the config file, keyed by flag name, for the TUI's form.
func (d *defaultsLoader) settings(fs *flag.FlagSet) map[string]string {
out := map[string]string{}
for name, src := range d.sources {
if src == sourceEnv || src == sourceConfig {
out[name] = fs.Lookup(name).Value.String()
}
}
return out
}