package config import ( "encoding/base64" "encoding/json" "os" "path/filepath" "strings" "sync" ) // saveMu serializes Saves: the UI thread (path browse, Dalamud toggle) and the // login worker (credentials after a successful login) can both Save, and they // would otherwise interleave on the same .tmp file. var saveMu sync.Mutex type Config struct { GamePath string `json:"game_path"` User string `json:"user"` Pass string `json:"pass"` Dalamud bool `json:"dalamud"` // launch with the Dalamud plugin framework } func Path() (string, error) { dir, err := os.UserConfigDir() if err != nil { return "", err } return filepath.Join(dir, "ffxiv-tiny-launcher", "config"), nil } // Load reads the config file if present. Missing file yields an empty config. func Load() (Config, error) { var cfg Config p, err := Path() if err != nil { return cfg, err } data, err := os.ReadFile(p) if err != nil { if os.IsNotExist(err) { return cfg, nil } return cfg, err } raw, err := base64.StdEncoding.DecodeString(string(data)) if err != nil { return Config{}, nil // corrupted; start fresh rather than crash } for i := range raw { raw[i] ^= key[i%len(key)] } // A mangled file (e.g. a torn write on older builds) can contain invalid // UTF-8, which json.Unmarshal rejects outright — replace such bytes before // parsing so the rest of the config still loads and nothing raw ever // reaches the UI or the login request. (SE passwords are ASCII in // practice, so this only ever triggers on corruption.) raw = []byte(strings.ToValidUTF8(string(raw), "\uFFFD")) if err := json.Unmarshal(raw, &cfg); err != nil { return Config{}, nil } return cfg, nil } // Save writes the config obfuscated (XOR+base64). This is hiding from // casual eyes only, not real protection. Callers must pass a snapshot of the // config, never a struct they may still be mutating on another goroutine. func Save(cfg Config) error { saveMu.Lock() defer saveMu.Unlock() p, err := Path() if err != nil { return err } if err := os.MkdirAll(filepath.Dir(p), 0o700); err != nil { return err } raw, err := json.Marshal(cfg) if err != nil { return err } for i := range raw { raw[i] ^= key[i%len(key)] } // Write atomically (temp file + rename) so a crash mid-write can never // leave a truncated config behind. tmp := p + ".tmp" if err := os.WriteFile(tmp, []byte(base64.StdEncoding.EncodeToString(raw)), 0o600); err != nil { return err } return os.Rename(tmp, p) } var key = []byte("ffxiv-tiny-launcher-key")