1package config
2
3import (
4	"fmt"
5	"os"
6	"path/filepath"
7	"sort"
8
9	"github.com/gopasspw/gopass/pkg/debug"
10	"github.com/gopasspw/gopass/pkg/fsutil"
11
12	"gopkg.in/yaml.v3"
13)
14
15// LoadWithFallbackRelaxed will try to load the config from one of the default
16// locations but also accept a more recent config.
17func LoadWithFallbackRelaxed() *Config {
18	return loadWithFallback(true)
19}
20
21// LoadWithFallback will try to load the config from one of the default locations
22func LoadWithFallback() *Config {
23	return loadWithFallback(false)
24}
25
26func loadWithFallback(relaxed bool) *Config {
27	for _, l := range configLocations() {
28		if cfg := loadConfig(l, relaxed); cfg != nil {
29			return cfg
30		}
31	}
32	return loadDefault()
33}
34
35// Load will load the config from the default location or return a default config
36func Load() *Config {
37	if cfg := loadConfig(configLocation(), false); cfg != nil {
38		return cfg
39	}
40	return loadDefault()
41}
42
43func loadConfig(l string, relaxed bool) *Config {
44	debug.Log("Trying to load config from %s", l)
45	cfg, err := load(l, relaxed)
46	if err == ErrConfigNotFound {
47		return nil
48	}
49	if err != nil {
50		return nil
51	}
52	debug.Log("Loaded config from %s: %+v", l, cfg)
53	return cfg
54}
55
56func loadDefault() *Config {
57	cfg := New()
58	cfg.Path = PwStoreDir("")
59	debug.Log("Loaded default config: %+v", cfg)
60	return cfg
61}
62
63func load(cf string, relaxed bool) (*Config, error) {
64	// deliberately using os.Stat here, a symlinked
65	// config is OK
66	if _, err := os.Stat(cf); err != nil {
67		return nil, ErrConfigNotFound
68	}
69	buf, err := os.ReadFile(cf)
70	if err != nil {
71		fmt.Fprintf(os.Stderr, "Error reading config from %s: %s\n", cf, err)
72		return nil, ErrConfigNotFound
73	}
74
75	cfg, err := decode(buf, relaxed)
76	if err != nil {
77		fmt.Fprintf(os.Stderr, "Error reading config from %s: %s\n", cf, err)
78		return nil, ErrConfigNotParsed
79	}
80	if cfg.Mounts == nil {
81		cfg.Mounts = make(map[string]string)
82	}
83	cfg.ConfigPath = cf
84	return cfg, nil
85}
86
87func checkOverflow(m map[string]interface{}) error {
88	if len(m) < 1 {
89		return nil
90	}
91
92	var keys []string
93	for k := range m {
94		keys = append(keys, k)
95	}
96	sort.Strings(keys)
97	return fmt.Errorf("unknown fields: %+v", keys)
98}
99
100type configer interface {
101	Config() *Config
102	CheckOverflow() error
103}
104
105func decode(buf []byte, relaxed bool) (*Config, error) {
106	mostRecent := &Config{
107		AutoImport:    true,
108		ClipTimeout:   45,
109		ExportKeys:    true,
110		Notifications: true,
111		Parsing:       true,
112		Path:          PwStoreDir(""),
113	}
114	cfgs := []configer{
115		// most recent config must come first
116		mostRecent,
117		&Pre1127{},
118		&Pre1102{},
119		&Pre193{
120			Root: &Pre193StoreConfig{},
121		},
122		&Pre182{
123			Root: &Pre182StoreConfig{},
124		},
125		&Pre140{},
126		&Pre130{},
127	}
128	if relaxed {
129		// most recent config must come last as well, will be tried w/o
130		// overflow checks
131		cfgs = append(cfgs, mostRecent)
132	}
133	var warn string
134	for i, cfg := range cfgs {
135		debug.Log("Trying to unmarshal config into %T", cfg)
136		if err := yaml.Unmarshal(buf, cfg); err != nil {
137			debug.Log("Loading config %T failed: %s", cfg, err)
138			continue
139		}
140		if err := cfg.CheckOverflow(); err != nil {
141			debug.Log("Extra elements in config: %s", err)
142			if i == 0 {
143				warn = fmt.Sprintf("Failed to load config %T. Do you need to remove deprecated fields? %s\n", cfg, err)
144			}
145			// usually we are strict about extra fields, i.e. any field left
146			// unparsed means this config failed and we try the next one.
147			if i < len(cfgs)-1 {
148				continue
149			}
150			// in relaxed mode we append an extra copy of the most recent
151			// config to the end of the slice and might just ignore these
152			// extra fields.
153			if !relaxed {
154				continue
155			}
156			debug.Log("Ignoring extra config fields for fallback config (only)")
157		}
158		debug.Log("Loaded config: %T: %+v", cfg, cfg)
159		conf := cfg.Config()
160		if i > 0 {
161			debug.Log("Loaded legacy config. Should rewrite config.")
162		}
163		return conf, nil
164	}
165	// We try to provide a seamless config upgrade path for users of our
166	// released versions. But some users build gopass from the master branch
167	// and these might run into issues when we remove config options.
168	// Since our config parser is pedantic (it has to) we fail parsing on
169	// unknown config options. If we remove one and the user rebuilds it's
170	// gopass binary without changing the config, it will fail to parse the
171	// current config and the legacy configs will likely fail as well.
172	// But if we always display the warning users with configs from previous
173	// releases will always see the warning. So instead we only display the
174	// warning if parsing of the most up to date config struct fails AND
175	// not other succeeds.
176	if warn != "" {
177		fmt.Fprint(os.Stderr, warn)
178	}
179	return nil, ErrConfigNotParsed
180}
181
182// Save saves the config
183func (c *Config) Save() error {
184	buf, err := yaml.Marshal(c)
185	if err != nil {
186		return fmt.Errorf("failed to marshal YAML: %w", err)
187	}
188
189	cfgLoc := configLocation()
190	cfgDir := filepath.Dir(cfgLoc)
191	if !fsutil.IsDir(cfgDir) {
192		if err := os.MkdirAll(cfgDir, 0700); err != nil {
193			return fmt.Errorf("failed to create dir %q: %w", cfgDir, err)
194		}
195	}
196	if err := os.WriteFile(cfgLoc, buf, 0600); err != nil {
197		return fmt.Errorf("failed to write config file to %q: %w", cfgLoc, err)
198	}
199	debug.Log("Saved config to %s: %+v\n", cfgLoc, c)
200	return nil
201}
202