1package config
2
3import (
4 "fmt"
5 "io/fs"
6 "os"
7 "strings"
8
9 "github.com/pelletier/go-toml/v2"
10)
11
12// configFilePermissions is the file permission used when writing migrated config files.
13const configFilePermissions fs.FileMode = 0o644
14
15// legacyEngineAlias defines the single metadata source for old engine config keys.
16// Dropped aliases are detected as legacy input but are not written to a new target.
17type legacyEngineAlias struct {
18 legacySection string
19 legacyField string
20 targetSection string // "engine" or "engine.webkit"; empty when dropped
21 targetField string // empty when dropped
22 dropped bool
23}
24
25func (a legacyEngineAlias) legacyKey() string {
26 return a.legacySection + "." + a.legacyField
27}
28
29const (
30 engineTargetSection = "engine"
31 engineWebKitTargetSection = "engine.webkit"
32)
33
34func legacyEngineTarget(legacySection, legacyField, targetSection, targetField string) legacyEngineAlias {
35 return legacyEngineAlias{
36 legacySection: legacySection,
37 legacyField: legacyField,
38 targetSection: targetSection,
39 targetField: targetField,
40 }
41}
42
43func legacyEngineSameTarget(legacySection, legacyField, targetSection string) legacyEngineAlias {
44 return legacyEngineTarget(legacySection, legacyField, targetSection, legacyField)
45}
46
47func droppedLegacyEngineAlias(legacySection, legacyField string) legacyEngineAlias {
48 return legacyEngineAlias{legacySection: legacySection, legacyField: legacyField, dropped: true}
49}
50
51// legacyEngineAliases is the canonical list of old-to-new engine config aliases.
52var legacyEngineAliases = []legacyEngineAlias{
53 // [performance] -> [engine]
54 legacyEngineSameTarget("performance", "profile", engineTargetSection),
55 legacyEngineSameTarget("performance", "zoom_cache_size", engineTargetSection),
56 legacyEngineTarget("performance", "webview_pool_prewarm_count", engineTargetSection, "pool_prewarm_count"),
57 // [privacy] -> [engine]
58 legacyEngineSameTarget("privacy", "cookie_policy", engineTargetSection),
59
60 // [privacy] -> [engine.webkit]
61 legacyEngineSameTarget("privacy", "itp_enabled", engineWebKitTargetSection),
62
63 // [rendering] -> [engine.webkit] (rendering.mode is dropped)
64 droppedLegacyEngineAlias("rendering", "mode"),
65 legacyEngineSameTarget("rendering", "disable_dmabuf_renderer", engineWebKitTargetSection),
66 legacyEngineSameTarget("rendering", "force_compositing_mode", engineWebKitTargetSection),
67 legacyEngineSameTarget("rendering", "disable_compositing_mode", engineWebKitTargetSection),
68 legacyEngineSameTarget("rendering", "gsk_renderer", engineWebKitTargetSection),
69 legacyEngineSameTarget("rendering", "disable_mipmaps", engineWebKitTargetSection),
70 legacyEngineSameTarget("rendering", "prefer_gl", engineWebKitTargetSection),
71 legacyEngineSameTarget("rendering", "draw_compositing_indicators", engineWebKitTargetSection),
72 legacyEngineSameTarget("rendering", "show_fps", engineWebKitTargetSection),
73 legacyEngineSameTarget("rendering", "sample_memory", engineWebKitTargetSection),
74 legacyEngineSameTarget("rendering", "debug_frames", engineWebKitTargetSection),
75
76 // [performance] -> [engine.webkit] (webkit-specific) — field names are identical
77 legacyEngineSameTarget("performance", "skia_cpu_painting_threads", engineWebKitTargetSection),
78 legacyEngineSameTarget("performance", "skia_gpu_painting_threads", engineWebKitTargetSection),
79 legacyEngineSameTarget("performance", "skia_enable_cpu_rendering", engineWebKitTargetSection),
80 legacyEngineSameTarget("performance", "web_process_memory_limit_mb", engineWebKitTargetSection),
81 legacyEngineSameTarget("performance", "web_process_memory_poll_interval_sec", engineWebKitTargetSection),
82 legacyEngineSameTarget("performance", "web_process_memory_conservative_threshold", engineWebKitTargetSection),
83 legacyEngineSameTarget("performance", "web_process_memory_strict_threshold", engineWebKitTargetSection),
84 legacyEngineSameTarget("performance", "network_process_memory_limit_mb", engineWebKitTargetSection),
85 legacyEngineSameTarget("performance", "network_process_memory_poll_interval_sec", engineWebKitTargetSection),
86 legacyEngineSameTarget("performance", "network_process_memory_conservative_threshold", engineWebKitTargetSection),
87 legacyEngineSameTarget("performance", "network_process_memory_strict_threshold", engineWebKitTargetSection),
88
89 // [media] GStreamer fields -> [engine.webkit]
90 legacyEngineSameTarget("media", "force_vsync", engineWebKitTargetSection),
91 legacyEngineSameTarget("media", "gl_rendering_mode", engineWebKitTargetSection),
92 legacyEngineSameTarget("media", "gstreamer_debug_level", engineWebKitTargetSection),
93
94 // [runtime] -> [engine.webkit]
95 legacyEngineSameTarget("runtime", "prefix", engineWebKitTargetSection),
96}
97
98func legacyEngineAliasesForSection(section string) []legacyEngineAlias {
99 aliases := make([]legacyEngineAlias, 0)
100 for _, alias := range legacyEngineAliases {
101 if alias.legacySection == section {
102 aliases = append(aliases, alias)
103 }
104 }
105 return aliases
106}
107
108func legacyEngineSectionsToRemove() []string {
109 sections := make([]string, 0)
110 seen := make(map[string]struct{})
111 for _, alias := range legacyEngineAliases {
112 if alias.legacySection == "media" {
113 continue
114 }
115 if _, ok := seen[alias.legacySection]; ok {
116 continue
117 }
118 seen[alias.legacySection] = struct{}{}
119 sections = append(sections, alias.legacySection)
120 }
121 return sections
122}
123
124func legacyEngineInputSections() []string {
125 sections := make([]string, 0)
126 seen := make(map[string]struct{})
127 for _, alias := range legacyEngineAliases {
128 if _, ok := seen[alias.legacySection]; ok {
129 continue
130 }
131 seen[alias.legacySection] = struct{}{}
132 sections = append(sections, alias.legacySection)
133 }
134 return sections
135}
136
137func legacyEngineInputSectionsMessage() string {
138 sections := legacyEngineInputSections()
139 formatted := make([]string, 0, len(sections))
140 for _, section := range sections {
141 formatted = append(formatted, "["+section+"]")
142 }
143 return strings.Join(formatted, ", ")
144}
145
146func hasLegacyEngineAlias(raw map[string]any, alias legacyEngineAlias) bool {
147 sectionAny, exists := raw[alias.legacySection]
148 if !exists {
149 return false
150 }
151 sectionMap, ok := sectionAny.(map[string]any)
152 if !ok {
153 return false
154 }
155 _, exists = sectionMap[alias.legacyField]
156 return exists
157}
158
159func canonicalLegacyEngineValue(alias legacyEngineAlias, val any) any {
160 if alias.targetSection == engineTargetSection && alias.targetField == "cookie_policy" {
161 if cookiePolicy, ok := val.(string); ok && cookiePolicy == "accept_all" {
162 return string(CookiePolicyAlways)
163 }
164 }
165 return val
166}
167
168// MigrateToEngineConfig restructures old top-level config sections into [engine]/[engine.webkit].
169// Returns true if migration was performed, false if already migrated or no old sections found.
170func MigrateToEngineConfig(configFile string) (bool, error) {
171 raw, err := readRawTOML(configFile)
172 if err != nil {
173 return false, err
174 }
175
176 if _, exists := raw["engine"]; exists {
177 for _, section := range legacyEngineSectionsToRemove() {
178 if _, hasLegacy := raw[section]; hasLegacy {
179 return false, fmt.Errorf(
180 "mixed old/new config: [engine] coexists with legacy [%s]; "+
181 "remove [rendering], [performance], [privacy], [runtime]",
182 section,
183 )
184 }
185 }
186 for _, alias := range legacyEngineAliasesForSection("media") {
187 if hasLegacyEngineAlias(raw, alias) {
188 return false, fmt.Errorf(
189 "mixed old/new config: [engine] coexists with deprecated "+
190 "media.%s; remove it from [media] (now in [engine.webkit])",
191 alias.legacyField,
192 )
193 }
194 }
195 return false, nil // already migrated
196 }
197
198 if !hasOldEngineSections(raw) {
199 return false, nil
200 }
201
202 applyEngineMappings(raw)
203
204 out, marshalErr := toml.Marshal(raw)
205 if marshalErr != nil {
206 return false, marshalErr
207 }
208
209 if err := atomicWriteFile(configFile, string(out), configFilePermissions); err != nil {
210 return false, err
211 }
212
213 return true, nil
214}
215
216// readRawTOML reads a TOML file into a generic map.
217func readRawTOML(path string) (map[string]any, error) {
218 data, err := os.ReadFile(path)
219 if err != nil {
220 return nil, err
221 }
222 var raw map[string]any
223 if err := toml.Unmarshal(data, &raw); err != nil {
224 return nil, err
225 }
226 return raw, nil
227}
228
229// hasOldEngineSections checks if the raw config has old sections that need migration.
230func hasOldEngineSections(raw map[string]any) bool {
231 for _, section := range legacyEngineSectionsToRemove() {
232 if _, exists := raw[section]; exists {
233 return true
234 }
235 }
236 for _, alias := range legacyEngineAliasesForSection("media") {
237 if hasLegacyEngineAlias(raw, alias) {
238 return true
239 }
240 }
241 return false
242}
243
244// applyEngineMappings builds [engine]/[engine.webkit] from old sections and cleans up.
245func applyEngineMappings(raw map[string]any) {
246 engineMap := make(map[string]any)
247 webkitMap := make(map[string]any)
248
249 for _, alias := range legacyEngineAliases {
250 if alias.dropped {
251 continue
252 }
253 val := getSectionField(raw, alias.legacySection, alias.legacyField)
254 if val == nil {
255 continue
256 }
257 val = canonicalLegacyEngineValue(alias, val)
258 switch alias.targetSection {
259 case engineTargetSection:
260 engineMap[alias.targetField] = val
261 case engineWebKitTargetSection:
262 webkitMap[alias.targetField] = val
263 }
264 }
265
266 engineMap["type"] = "webkit"
267 if len(webkitMap) > 0 {
268 engineMap["webkit"] = webkitMap
269 }
270 raw["engine"] = engineMap
271
272 for _, section := range legacyEngineSectionsToRemove() {
273 delete(raw, section)
274 }
275
276 stripMigratedMediaFields(raw)
277}
278
279// stripMigratedMediaFields removes migrated fields from [media] while keeping the rest.
280func stripMigratedMediaFields(raw map[string]any) {
281 mediaAny, exists := raw["media"]
282 if !exists {
283 return
284 }
285 media, ok := mediaAny.(map[string]any)
286 if !ok {
287 return
288 }
289 for _, alias := range legacyEngineAliasesForSection("media") {
290 delete(media, alias.legacyField)
291 }
292 if len(media) == 0 {
293 delete(raw, "media")
294 }
295}
296
297// getSectionField retrieves a field value from a named section in the raw config map.
298func getSectionField(raw map[string]any, section, field string) any {
299 sectionAny, exists := raw[section]
300 if !exists {
301 return nil
302 }
303 sectionMap, ok := sectionAny.(map[string]any)
304 if !ok {
305 return nil
306 }
307 val, exists := sectionMap[field]
308 if !exists {
309 return nil
310 }
311 return val
312}