diff options
| author | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
| commit | 153acb7f644d755c45533b7c5311e3ffa47e8973 (patch) | |
| tree | 7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/admin/api_colors.go | |
| parent | 87d08f0151e643951769e323995f1c5ed89ee200 (diff) | |
| download | thehouseoficarus-153acb7f644d755c45533b7c5311e3ffa47e8973.tar.gz | |
fix: harden engine across casino, economy, concurrency, security, and content
Diffstat (limited to 'internal/admin/api_colors.go')
| -rw-r--r-- | internal/admin/api_colors.go | 143 |
1 files changed, 138 insertions, 5 deletions
diff --git a/internal/admin/api_colors.go b/internal/admin/api_colors.go index b57ad82..7989e35 100644 --- a/internal/admin/api_colors.go +++ b/internal/admin/api_colors.go @@ -1,6 +1,8 @@ package admin import ( + "bytes" + "fmt" "net/http" "os" "strings" @@ -575,14 +577,145 @@ func (s *AdminServer) handleColorsReset(w http.ResponseWriter, r *http.Request) writeJSON(w, map[string]any{"ok": true, "colors": live, "constant_colors": liveConst}) } -// writeConfigFile marshals the entire live *config.Config back to config.yaml, -// overwriting whatever was on disk. A full re-marshal drops the file's manual -// comments but produces a valid, reloadable file with the edited colors. +// writeConfigFile persists the two edited color maps back to config.yaml +// without overwriting unrelated operator edits made after startup. The file +// is re-read and only the default_colors / constant_colors mappings are +// patched (comments and every other key are preserved), after writing a +// backup. A missing/unreadable config falls back to a full marshal so a fresh +// install still gets a valid file. func (s *AdminServer) writeConfigFile() error { - _ = backupFile(s.configPath) // best-effort snapshot for safety - out, err := yaml.Marshal(s.cfg) + if err := s.backupConfigFile(); err != nil { + return err + } + data, err := os.ReadFile(s.configPath) + if err != nil { + // No config on disk yet (fresh install or removed): write the live + // config whole so the file exists and reloads. + out, merr := yaml.Marshal(s.cfg) + if merr != nil { + return merr + } + return os.WriteFile(s.configPath, out, 0644) + } + out, err := patchColorMaps(data, s.cfg.DefaultColors, s.cfg.ConstantColors) if err != nil { return err } return os.WriteFile(s.configPath, out, 0644) } + +// backupConfigFile copies the current config.yaml next to itself before an +// edit, so an operator can recover from a bad color save. A fresh install +// (no file yet) has nothing to back up. +func (s *AdminServer) backupConfigFile() error { + data, err := os.ReadFile(s.configPath) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + return os.WriteFile(s.configPath+".bak", data, 0600) +} + +// patchColorMaps updates the default_colors and constant_colors mappings in a +// config.yaml document, preserving every other key and all comments. Only the +// keys present in the live maps are written: keys an operator added inside +// those sections, and every comment around them, survive the save. +func patchColorMaps(data []byte, defaults, constants map[string]string) ([]byte, error) { + var doc yaml.Node + if err := yaml.Unmarshal(data, &doc); err != nil { + return nil, fmt.Errorf("parse config: %w", err) + } + if len(doc.Content) == 0 || doc.Content[0].Kind != yaml.MappingNode { + return nil, fmt.Errorf("config is not a mapping") + } + root := doc.Content[0] + for _, patch := range []struct { + key string + m map[string]string + }{ + {"default_colors", defaults}, + {"constant_colors", constants}, + } { + if err := patchMapping(root, patch.key, patch.m); err != nil { + return nil, err + } + } + var buf bytes.Buffer + enc := yaml.NewEncoder(&buf) + enc.SetIndent(2) + if err := enc.Encode(&doc); err != nil { + return nil, err + } + if err := enc.Close(); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// patchMapping writes the entries of m into the mapping at root[key], creating +// the mapping (and the key) when absent. Entries already on disk are updated +// in place so their comments survive; entries on disk that m does not name are +// left exactly as the operator wrote them. +func patchMapping(root *yaml.Node, key string, m map[string]string) error { + fresh, err := colorMapNode(m) + if err != nil { + return err + } + for i := 0; i+1 < len(root.Content); i += 2 { + if root.Content[i].Value != key { + continue + } + if existing := root.Content[i+1]; existing.Kind == yaml.MappingNode { + mergeMapNode(existing, fresh) + return nil + } + root.Content[i+1] = fresh + return nil + } + root.Content = append(root.Content, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key}, fresh) + return nil +} + +// mergeMapNode copies src's entries into dst: a key dst already has is updated +// in place (keeping the operator's comments on it), a key it lacks is appended +// with the node from src. +func mergeMapNode(dst, src *yaml.Node) { + for i := 0; i+1 < len(src.Content); i += 2 { + key, value := src.Content[i], src.Content[i+1] + merged := false + for j := 0; j+1 < len(dst.Content); j += 2 { + if dst.Content[j].Value != key.Value { + continue + } + if dst.Content[j+1].Kind == yaml.ScalarNode && value.Kind == yaml.ScalarNode { + dst.Content[j+1].Value = value.Value + dst.Content[j+1].Tag = value.Tag + } else { + dst.Content[j+1] = value + } + merged = true + break + } + if !merged { + dst.Content = append(dst.Content, key, value) + } + } +} + +func colorMapNode(m map[string]string) (*yaml.Node, error) { + b, err := yaml.Marshal(m) + if err != nil { + return nil, err + } + var n yaml.Node + if err := yaml.Unmarshal(b, &n); err != nil { + return nil, err + } + if len(n.Content) == 0 { + return &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}, nil + } + return n.Content[0], nil +} |
