1package config
2
3import (
4 "fmt"
5 "github.com/pelletier/go-toml/v2"
6 "io/fs"
7 "os"
8)
9
10// configFilePermissions is the file permission used when writing migrated config files.
11const configFilePermissions fs.FileMode = 0o644
12
13// engineMigrationMapping defines old key -> new key mappings for the engine migration.
14// Keys with empty new key ("") are dropped (not migrated).
15// The source section is the first part of the old key.
16type engineFieldMapping struct {
17 oldSection string
18 oldField string
19 newSection string // "engine" or "engine.webkit"
20 newField string // empty means drop
21}
22
23// engineUniversalMappings are fields that go into [engine] (not [engine.webkit]).
24var engineUniversalMappings = []engineFieldMapping{
25 // [performance] -> [engine]
26 {oldSection: "performance", oldField: "profile", newSection: "engine", newField: "profile"},
27 {oldSection: "performance", oldField: "zoom_cache_size", newSection: "engine", newField: "zoom_cache_size"},
28 {oldSection: "performance", oldField: "webview_pool_prewarm_count", newSection: "engine", newField: "pool_prewarm_count"},
29 // [privacy] -> [engine]
30 {oldSection: "privacy", oldField: "cookie_policy", newSection: "engine", newField: "cookie_policy"},
31}
32
33// engineWebkitMappings are fields that go into [engine.webkit].
34var engineWebkitMappings = []engineFieldMapping{
35 // [privacy] -> [engine.webkit]
36 {oldSection: "privacy", oldField: "itp_enabled", newSection: "engine.webkit", newField: "itp_enabled"},
37
38 // [rendering] -> [engine.webkit] (rendering.mode is dropped)
39 {oldSection: "rendering", oldField: "disable_dmabuf_renderer", newSection: "engine.webkit", newField: "disable_dmabuf_renderer"},
40 {oldSection: "rendering", oldField: "force_compositing_mode", newSection: "engine.webkit", newField: "force_compositing_mode"},
41 {oldSection: "rendering", oldField: "disable_compositing_mode", newSection: "engine.webkit", newField: "disable_compositing_mode"},
42 {oldSection: "rendering", oldField: "gsk_renderer", newSection: "engine.webkit", newField: "gsk_renderer"},
43 {oldSection: "rendering", oldField: "disable_mipmaps", newSection: "engine.webkit", newField: "disable_mipmaps"},
44 {oldSection: "rendering", oldField: "prefer_gl", newSection: "engine.webkit", newField: "prefer_gl"},
45 {oldSection: "rendering", oldField: "draw_compositing_indicators", newSection: "engine.webkit", newField: "draw_compositing_indicators"},
46 {oldSection: "rendering", oldField: "show_fps", newSection: "engine.webkit", newField: "show_fps"},
47 {oldSection: "rendering", oldField: "sample_memory", newSection: "engine.webkit", newField: "sample_memory"},
48 {oldSection: "rendering", oldField: "debug_frames", newSection: "engine.webkit", newField: "debug_frames"},
49
50 // [performance] -> [engine.webkit] (webkit-specific) — field names are identical
51 {oldSection: "performance", oldField: "skia_cpu_painting_threads",
52 newSection: "engine.webkit", newField: "skia_cpu_painting_threads"},
53 {oldSection: "performance", oldField: "skia_gpu_painting_threads",
54 newSection: "engine.webkit", newField: "skia_gpu_painting_threads"},
55 {oldSection: "performance", oldField: "skia_enable_cpu_rendering",
56 newSection: "engine.webkit", newField: "skia_enable_cpu_rendering"},
57 {oldSection: "performance", oldField: "web_process_memory_limit_mb",
58 newSection: "engine.webkit", newField: "web_process_memory_limit_mb"},
59 {oldSection: "performance", oldField: "web_process_memory_poll_interval_sec",
60 newSection: "engine.webkit", newField: "web_process_memory_poll_interval_sec"},
61 {oldSection: "performance", oldField: "web_process_memory_conservative_threshold",
62 newSection: "engine.webkit", newField: "web_process_memory_conservative_threshold"},
63 {oldSection: "performance", oldField: "web_process_memory_strict_threshold",
64 newSection: "engine.webkit", newField: "web_process_memory_strict_threshold"},
65 {oldSection: "performance", oldField: "network_process_memory_limit_mb",
66 newSection: "engine.webkit", newField: "network_process_memory_limit_mb"},
67 {oldSection: "performance", oldField: "network_process_memory_poll_interval_sec",
68 newSection: "engine.webkit", newField: "network_process_memory_poll_interval_sec"},
69 {oldSection: "performance", oldField: "network_process_memory_conservative_threshold",
70 newSection: "engine.webkit", newField: "network_process_memory_conservative_threshold"},
71 {oldSection: "performance", oldField: "network_process_memory_strict_threshold",
72 newSection: "engine.webkit", newField: "network_process_memory_strict_threshold"},
73
74 // [media] GStreamer fields -> [engine.webkit]
75 {oldSection: "media", oldField: "force_vsync", newSection: "engine.webkit", newField: "force_vsync"},
76 {oldSection: "media", oldField: "gl_rendering_mode", newSection: "engine.webkit", newField: "gl_rendering_mode"},
77 {oldSection: "media", oldField: "gstreamer_debug_level", newSection: "engine.webkit", newField: "gstreamer_debug_level"},
78
79 // [runtime] -> [engine.webkit]
80 {oldSection: "runtime", oldField: "prefix", newSection: "engine.webkit", newField: "prefix"},
81}
82
83// oldSectionsToRemove are the sections that are fully removed after migration
84// (media is not here because it has fields that stay).
85var oldSectionsToRemove = []string{"rendering", "performance", "privacy", "runtime"}
86
87// MigrateToEngineConfig restructures old top-level config sections into [engine]/[engine.webkit].
88// Returns true if migration was performed, false if already migrated or no old sections found.
89func MigrateToEngineConfig(configFile string) (bool, error) {
90 raw, err := readRawTOML(configFile)
91 if err != nil {
92 return false, err
93 }
94
95 if _, exists := raw["engine"]; exists {
96 for _, section := range []string{"rendering", "performance", "privacy", "runtime"} {
97 if _, hasLegacy := raw[section]; hasLegacy {
98 return false, fmt.Errorf(
99 "mixed old/new config: [engine] coexists with legacy [%s]; "+
100 "remove [rendering], [performance], [privacy], [runtime]",
101 section,
102 )
103 }
104 }
105 // Check for deprecated media keys that moved to [engine.webkit].
106 if mediaRaw, ok := raw["media"].(map[string]any); ok {
107 for _, key := range []string{"force_vsync", "gl_rendering_mode", "gstreamer_debug_level"} {
108 if _, has := mediaRaw[key]; has {
109 return false, fmt.Errorf(
110 "mixed old/new config: [engine] coexists with deprecated "+
111 "media.%s; remove it from [media] (now in [engine.webkit])",
112 key,
113 )
114 }
115 }
116 }
117 return false, nil // already migrated
118 }
119
120 if !hasOldEngineSections(raw) {
121 return false, nil
122 }
123
124 applyEngineMappings(raw)
125
126 out, marshalErr := toml.Marshal(raw)
127 if marshalErr != nil {
128 return false, marshalErr
129 }
130
131 if err := atomicWriteFile(configFile, string(out), configFilePermissions); err != nil {
132 return false, err
133 }
134
135 return true, nil
136}
137
138// readRawTOML reads a TOML file into a generic map.
139func readRawTOML(path string) (map[string]any, error) {
140 data, err := os.ReadFile(path)
141 if err != nil {
142 return nil, err
143 }
144 var raw map[string]any
145 if err := toml.Unmarshal(data, &raw); err != nil {
146 return nil, err
147 }
148 return raw, nil
149}
150
151// hasOldEngineSections checks if the raw config has old sections that need migration.
152func hasOldEngineSections(raw map[string]any) bool {
153 for _, section := range []string{"rendering", "performance", "privacy", "runtime"} {
154 if _, exists := raw[section]; exists {
155 return true
156 }
157 }
158 if mediaAny, exists := raw["media"]; exists {
159 if media, ok := mediaAny.(map[string]any); ok {
160 for _, f := range []string{"force_vsync", "gl_rendering_mode", "gstreamer_debug_level"} {
161 if _, has := media[f]; has {
162 return true
163 }
164 }
165 }
166 }
167 return false
168}
169
170// applyEngineMappings builds [engine]/[engine.webkit] from old sections and cleans up.
171func applyEngineMappings(raw map[string]any) {
172 engineMap := make(map[string]any)
173 webkitMap := make(map[string]any)
174
175 for _, m := range engineUniversalMappings {
176 if val := getSectionField(raw, m.oldSection, m.oldField); val != nil {
177 engineMap[m.newField] = val
178 }
179 }
180 for _, m := range engineWebkitMappings {
181 if val := getSectionField(raw, m.oldSection, m.oldField); val != nil {
182 webkitMap[m.newField] = val
183 }
184 }
185
186 engineMap["type"] = "webkit"
187 if len(webkitMap) > 0 {
188 engineMap["webkit"] = webkitMap
189 }
190 raw["engine"] = engineMap
191
192 for _, section := range oldSectionsToRemove {
193 delete(raw, section)
194 }
195
196 stripMigratedMediaFields(raw)
197}
198
199// stripMigratedMediaFields removes GStreamer fields from [media] while keeping the rest.
200func stripMigratedMediaFields(raw map[string]any) {
201 mediaAny, exists := raw["media"]
202 if !exists {
203 return
204 }
205 media, ok := mediaAny.(map[string]any)
206 if !ok {
207 return
208 }
209 delete(media, "force_vsync")
210 delete(media, "gl_rendering_mode")
211 delete(media, "gstreamer_debug_level")
212 if len(media) == 0 {
213 delete(raw, "media")
214 }
215}
216
217// getSectionField retrieves a field value from a named section in the raw config map.
218func getSectionField(raw map[string]any, section, field string) any {
219 sectionAny, exists := raw[section]
220 if !exists {
221 return nil
222 }
223 sectionMap, ok := sectionAny.(map[string]any)
224 if !ok {
225 return nil
226 }
227 val, exists := sectionMap[field]
228 if !exists {
229 return nil
230 }
231 return val
232}