1package driver
2
3import (
4	"encoding/json"
5	"fmt"
6	"io/ioutil"
7	"net/url"
8	"os"
9	"path/filepath"
10)
11
12// settings holds pprof settings.
13type settings struct {
14	// Configs holds a list of named UI configurations.
15	Configs []namedConfig `json:"configs"`
16}
17
18// namedConfig associates a name with a config.
19type namedConfig struct {
20	Name string `json:"name"`
21	config
22}
23
24// settingsFileName returns the name of the file where settings should be saved.
25func settingsFileName() (string, error) {
26	// Return "pprof/settings.json" under os.UserConfigDir().
27	dir, err := os.UserConfigDir()
28	if err != nil {
29		return "", err
30	}
31	return filepath.Join(dir, "pprof", "settings.json"), nil
32}
33
34// readSettings reads settings from fname.
35func readSettings(fname string) (*settings, error) {
36	data, err := ioutil.ReadFile(fname)
37	if err != nil {
38		if os.IsNotExist(err) {
39			return &settings{}, nil
40		}
41		return nil, fmt.Errorf("could not read settings: %w", err)
42	}
43	settings := &settings{}
44	if err := json.Unmarshal(data, settings); err != nil {
45		return nil, fmt.Errorf("could not parse settings: %w", err)
46	}
47	for i := range settings.Configs {
48		settings.Configs[i].resetTransient()
49	}
50	return settings, nil
51}
52
53// writeSettings saves settings to fname.
54func writeSettings(fname string, settings *settings) error {
55	data, err := json.MarshalIndent(settings, "", "  ")
56	if err != nil {
57		return fmt.Errorf("could not encode settings: %w", err)
58	}
59
60	// create the settings directory if it does not exist
61	// XDG specifies permissions 0700 when creating settings dirs:
62	// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
63	if err := os.MkdirAll(filepath.Dir(fname), 0700); err != nil {
64		return fmt.Errorf("failed to create settings directory: %w", err)
65	}
66
67	if err := ioutil.WriteFile(fname, data, 0644); err != nil {
68		return fmt.Errorf("failed to write settings: %w", err)
69	}
70	return nil
71}
72
73// configMenuEntry holds information for a single config menu entry.
74type configMenuEntry struct {
75	Name       string
76	URL        string
77	Current    bool // Is this the currently selected config?
78	UserConfig bool // Is this a user-provided config?
79}
80
81// configMenu returns a list of items to add to a menu in the web UI.
82func configMenu(fname string, url url.URL) []configMenuEntry {
83	// Start with system configs.
84	configs := []namedConfig{{Name: "Default", config: defaultConfig()}}
85	if settings, err := readSettings(fname); err == nil {
86		// Add user configs.
87		configs = append(configs, settings.Configs...)
88	}
89
90	// Convert to menu entries.
91	result := make([]configMenuEntry, len(configs))
92	lastMatch := -1
93	for i, cfg := range configs {
94		dst, changed := cfg.config.makeURL(url)
95		if !changed {
96			lastMatch = i
97		}
98		result[i] = configMenuEntry{
99			Name:       cfg.Name,
100			URL:        dst.String(),
101			UserConfig: (i != 0),
102		}
103	}
104	// Mark the last matching config as currennt
105	if lastMatch >= 0 {
106		result[lastMatch].Current = true
107	}
108	return result
109}
110
111// editSettings edits settings by applying fn to them.
112func editSettings(fname string, fn func(s *settings) error) error {
113	settings, err := readSettings(fname)
114	if err != nil {
115		return err
116	}
117	if err := fn(settings); err != nil {
118		return err
119	}
120	return writeSettings(fname, settings)
121}
122
123// setConfig saves the config specified in request to fname.
124func setConfig(fname string, request url.URL) error {
125	q := request.Query()
126	name := q.Get("config")
127	if name == "" {
128		return fmt.Errorf("invalid config name")
129	}
130	cfg := currentConfig()
131	if err := cfg.applyURL(q); err != nil {
132		return err
133	}
134	return editSettings(fname, func(s *settings) error {
135		for i, c := range s.Configs {
136			if c.Name == name {
137				s.Configs[i].config = cfg
138				return nil
139			}
140		}
141		s.Configs = append(s.Configs, namedConfig{Name: name, config: cfg})
142		return nil
143	})
144}
145
146// removeConfig removes config from fname.
147func removeConfig(fname, config string) error {
148	return editSettings(fname, func(s *settings) error {
149		for i, c := range s.Configs {
150			if c.Name == config {
151				s.Configs = append(s.Configs[:i], s.Configs[i+1:]...)
152				return nil
153			}
154		}
155		return fmt.Errorf("config %s not found", config)
156	})
157}
158