package config import ( "encoding/base64" "os" "path/filepath" "runtime" "testing" ) // TestLoadSanitizesInvalidUTF8 ensures a mangled config file (valid base64 + // XOR but with invalid UTF-8 inside a string field) loads with replacement // characters instead of propagating raw bytes into the UI/login request. func TestLoadSanitizesInvalidUTF8(t *testing.T) { if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" { t.Skip("XDG_CONFIG_HOME only honored on unix") } dir := t.TempDir() t.Setenv("XDG_CONFIG_HOME", dir) // "wine" is a key from pre-Wine-removal configs; unknown fields must be ignored. raw := []byte(`{"game_path":"","wine":"wine","user":"u","pass":"abc` + "\xff\xfe" + `de"}`) for i := range raw { raw[i] ^= key[i%len(key)] } p := filepath.Join(dir, "ffxiv-tiny-launcher", "config") if err := os.MkdirAll(filepath.Dir(p), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(p, []byte(base64.StdEncoding.EncodeToString(raw)), 0o600); err != nil { t.Fatal(err) } cfg, err := Load() if err != nil { t.Fatal(err) } // ToValidUTF8 replaces a whole run of invalid bytes with one rune. want := "abc\ufffdde" if cfg.Pass != want { t.Fatalf("pass = %q, want %q", cfg.Pass, want) } if cfg.User != "u" { t.Fatalf("user = %q, want %q", cfg.User, "u") } } func TestSaveAtomicAndRoundTrip(t *testing.T) { dir := t.TempDir() if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" { t.Setenv("XDG_CONFIG_HOME", dir) } else if runtime.GOOS == "windows" { t.Setenv("APPDATA", dir) } in := Config{GamePath: `C:\ffxiv`, User: "u", Pass: "p"} if err := Save(in); err != nil { t.Fatal(err) } out, err := Load() if err != nil { t.Fatal(err) } if out != in { t.Fatalf("round trip = %+v, want %+v", out, in) } p, _ := Path() if _, err := os.Stat(p + ".tmp"); !os.IsNotExist(err) { t.Fatalf("leftover temp file %q (err=%v)", p+".tmp", err) } }