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