// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package wine import ( "bytes" "os" "path/filepath" "reflect" "strings" "testing" "time" ) func write_helper_file(t *testing.T, path, data string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(data), 0700); err != nil { t.Fatal(err) } } func TestProtonLoaderBypassesProxy(t *testing.T) { root := t.TempDir() loader := filepath.Join(root, "Proton", "files", "bin", "wine") preloader := filepath.Join(root, "Proton", "files", "lib", "wine", "i386-unix", "wine64-preloader") proxy := filepath.Join(root, "proxy", "wine") write_helper_file(t, loader, "loader") write_helper_file(t, preloader, "preloader") write_helper_file(t, proxy, "#!/bin/sh\n# PROTONTRICKS_STEAM_RUNTIME\n") for _, configured := range []string{"", proxy, loader} { if got := wine_loader(preloader, configured); got != loader { t.Fatalf("loader = %q, want %q", got, loader) } } } func TestContainerHelperPublishesImmutableCompleteSets(t *testing.T) { root := t.TempDir() t.Setenv("XDG_STATE_HOME", filepath.Join(root, "state")) helper := filepath.Join(root, "installed", "wine-access.exe") names := []string{"wine-access.exe", "wine-access.exe.so", "wine-access-menu.dll", "wine-access-menu32.dll", "wine-access-menu-host32.exe"} for _, name := range names { write_helper_file(t, filepath.Join(filepath.Dir(helper), name), name) } environment := map[string]string{"PRESSURE_VESSEL_RUNTIME": "runtime"} original, err := stage_helper(helper, environment) if err != nil { t.Fatal(err) } if original == helper { t.Fatal("helper was not staged") } for _, name := range names { data, err := os.ReadFile(filepath.Join(filepath.Dir(original), name)) if err != nil || string(data) != name { t.Fatalf("incomplete set: %s: %v", name, err) } } write_helper_file(t, helper+".so", "new sidecar") updated, err := stage_helper(helper, environment) if err != nil || updated == original { t.Fatalf("updated set = %q, %v", updated, err) } data, _ := os.ReadFile(original + ".so") if string(data) != "wine-access.exe.so" { t.Fatal("old sidecar overwritten") } write_helper_file(t, updated+".so", "tampered") if _, err := stage_helper(helper, environment); err == nil { t.Fatal("accepted modified staging") } if err := os.Remove(helper + ".so"); err != nil { t.Fatal(err) } if _, err := stage_helper(helper, environment); err == nil { t.Fatal("accepted incomplete source set") } if got, err := stage_helper(helper, nil); err != nil || got != helper { t.Fatalf("ordinary Wine = %q, %v", got, err) } } func TestListedProgramDoesNotForceWholesaleMode(t *testing.T) { t.Setenv("WINE_ACCESS_NO_DIALOG_READER", "") // A listed program is gated natively by the helper. Its presence must // never add the prefix-wide --no-dialog-reader, or ordinary programs in // the same prefix lose dialog reading. listed := Settings{ExcludedPrograms: []string{"Game.EXE", "game.exe"}} if wholesale_controller_only(listed) { t.Fatal("listed programs selected wholesale mode") } arguments := helper_arguments(wholesale_controller_only(listed), listed) if len(arguments) != 2 || arguments[0] != "--exclude-hook-program" || arguments[1] != "game.exe" { t.Fatalf("mixed-prefix arguments = %v", arguments) } global := Settings{ControllerOnly: true, ExcludedPrograms: []string{"Game.exe"}} if !wholesale_controller_only(global) { t.Fatal("global controller-only switch ignored") } arguments = helper_arguments(true, global) if arguments[len(arguments)-1] != "--no-dialog-reader" { t.Fatalf("global arguments = %v", arguments) } t.Setenv("WINE_ACCESS_NO_DIALOG_READER", "1") if !wholesale_controller_only(Settings{}) { t.Fatal("environment override ignored") } } func TestHelperArgumentsValidateProgramNames(t *testing.T) { settings := Settings{ExcludedPrograms: []string{ "Game.exe", "Another Game.exe", "Café.exe", "game.EXE", "Game\t.exe", "Game\x01.exe", "Game\x1f.exe", `C:\Game.exe`, strings.Repeat("é", 126) + ".exe", }} want := []string{ "--exclude-hook-program", "another game.exe", "--exclude-hook-program", "café.exe", "--exclude-hook-program", "game.exe", } if got := helper_arguments(false, settings); !reflect.DeepEqual(got, want) { t.Fatalf("validated helper arguments = %q, want %q", got, want) } } func TestProtonInfrastructureDoesNotKeepHelperAlive(t *testing.T) { for _, program := range []string{`C:\windows\system32\steam.exe`, `C:\windows\syswow64\steam.exe`, "xalia.exe", "conhost.exe"} { if applicationProcess(program) { t.Fatalf("infrastructure counted as app: %s", program) } } for _, program := range []string{`C:\Program Files\Steam\steam.exe`, "Game.exe", "explorer.exe"} { if !applicationProcess(program) { t.Fatalf("app ignored: %s", program) } } } func TestLaunchForwardsProtonSyncAndKeepsReaderBus(t *testing.T) { root := t.TempDir() output := filepath.Join(root, "environment") helper := filepath.Join(root, "helper") write_helper_file(t, helper, "#!/bin/sh\nenv > \"$TEST_WINE_ENV\"\n") t.Setenv("TEST_WINE_ENV", output) t.Setenv("DBUS_SESSION_BUS_ADDRESS", "unix:path=/reader/bus") manager := New(nil) manager.helper = helper child := &process{} source := prefix{path: root, environment: map[string]string{"WINEESYNC": "1", "WINEFSYNC": "1", "XAUTHORITY": "/fixture/auth", "DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/pressure-vessel/bus"}} if err := manager.launch(child, source, time.Now(), Settings{}); err != nil { t.Fatal(err) } select { case <-child.done: case <-time.After(3 * time.Second): stop(child) t.Fatal("helper did not exit") } data, err := os.ReadFile(output) if err != nil { t.Fatal(err) } for _, value := range []string{"WINEESYNC=1\n", "WINEFSYNC=1\n", "XAUTHORITY=/fixture/auth\n", "DBUS_SESSION_BUS_ADDRESS=unix:path=/reader/bus\n"} { if !bytes.Contains(data, []byte(value)) { t.Fatalf("missing %q", value) } } } func TestDiscoverMixedPrefixKeepsOrdinaryProgramsReadable(t *testing.T) { t.Setenv("WINE_ACCESS_NO_DIALOG_READER", "") root := t.TempDir() procRoot := filepath.Join(root, "proc") loader := filepath.Join(root, "wine") write_helper_file(t, loader, "loader") makeProcess := func(pid, program string) { t.Helper() base := filepath.Join(procRoot, pid) write_helper_file(t, filepath.Join(base, "cmdline"), program+"\x00") write_helper_file(t, filepath.Join(base, "environ"), "WINEPREFIX="+root+"\x00") if err := os.Symlink(loader, filepath.Join(base, "exe")); err != nil { t.Fatal(err) } } makeProcess("101", "launcher.exe") helper := filepath.Join(root, "helper") write_helper_file(t, helper, "#!/bin/sh\nexec sleep 30\n") current := Settings{ExcludedPrograms: []string{"GAME.exe"}} manager := New(nil) manager.helper, manager.procRoot = helper, procRoot manager.SettingsSource = func() Settings { return current } active := map[string]*process{} defer func() { for _, child := range active { stop(child) } }() // Launcher only: the exclusion flag is already armed, in normal mode. manager.poll(active) child := active[root] if child == nil || child.cmd == nil || child.controllerOnly { t.Fatal("launcher helper did not start normally") } if !child.exclusions["game.exe"] { t.Fatal("launcher helper lacks the armed exclusion flag") } originalPID := child.cmd.Process.Pid // The listed game and an ordinary program appear. The helper already // carries the flag, so nothing restarts and nothing goes wholesale: // the ordinary program keeps dialog reading by construction, since // --no-dialog-reader was never passed. makeProcess("102", "Game.exe") makeProcess("103", "notepad.exe") discovered := discover(procRoot, uint32(os.Getuid())) if len(discovered[root].programs) != 3 { t.Fatalf("programs = %v", discovered[root].programs) } manager.poll(active) if child.controllerOnly || child.cmd == nil || child.cmd.Process.Pid != originalPID { t.Fatal("listed game restarted or flipped the helper wholesale") } // The listed game disappears: still no restart and no mode change. if err := os.RemoveAll(filepath.Join(procRoot, "102")); err != nil { t.Fatal(err) } manager.poll(active) if child.controllerOnly || child.cmd == nil || child.cmd.Process.Pid != originalPID { t.Fatal("helper restarted when the listed game disappeared") } // A listing for a program that is not running waits for the next // natural spawn: no restart for prospective protection. current.ExcludedPrograms = []string{"GAME.exe", "Absent.exe"} manager.poll(active) if child.cmd.Process.Pid != originalPID { t.Fatal("helper restarted for a listed program that is not running") } // A newly listed program that is currently discovered but lacks its // flag restarts the helper once, in the same mode, carrying the flag. current.ExcludedPrograms = []string{"GAME.exe", "Notepad.exe"} manager.poll(active) if child.controllerOnly || child.cmd == nil || child.cmd.Process.Pid == originalPID { t.Fatal("newly listed running program did not restart the helper in place") } if !child.exclusions["notepad.exe"] { t.Fatal("restarted helper lacks the new exclusion flag") } pid := child.cmd.Process.Pid manager.poll(active) if child.cmd.Process.Pid != pid { t.Fatal("helper restarted more than once for one listing") } // Removing a listing never downgrades or restarts a running helper. current.ExcludedPrograms = []string{"GAME.exe"} manager.poll(active) if child.cmd.Process.Pid != pid { t.Fatal("helper restarted when a listing was removed") } // The global switch still upgrades a running helper to wholesale, and // clearing it never downgrades the running helper. current.ControllerOnly = true manager.poll(active) if !child.controllerOnly || child.cmd == nil || child.cmd.Process.Pid == pid { t.Fatal("global switch did not upgrade the helper to wholesale") } pid = child.cmd.Process.Pid current.ControllerOnly = false manager.poll(active) if !child.controllerOnly || child.cmd.Process.Pid != pid { t.Fatal("wholesale helper downgraded after the global switch cleared") } } func TestUnconfirmedHelperRestartsWholesaleOnceAndIsRemembered(t *testing.T) { t.Setenv("WINE_ACCESS_NO_DIALOG_READER", "") root := t.TempDir() procRoot := filepath.Join(root, "proc") loader := filepath.Join(root, "wine") write_helper_file(t, loader, "loader") base := filepath.Join(procRoot, "101") write_helper_file(t, filepath.Join(base, "cmdline"), "launcher.exe\x00") write_helper_file(t, filepath.Join(base, "environ"), "WINEPREFIX="+root+"\x00") if err := os.Symlink(loader, filepath.Join(base, "exe")); err != nil { t.Fatal(err) } helper := filepath.Join(root, "helper") // An old helper that ignores the exclusion flags reports zero. script := "#!/bin/sh\n" + "echo 'wine-access: ready {\"reader\":\"org.example.Reader\",\"controls_enabled\":true,\"hook_exclusions\":0}' >&2\n" + "exec sleep 30\n" write_helper_file(t, helper, script) manager := New(nil) manager.helper, manager.procRoot = helper, procRoot manager.SettingsSource = func() Settings { return Settings{ExcludedPrograms: []string{"Game.exe"}} } active := map[string]*process{} defer func() { for _, child := range active { stop(child) } }() manager.poll(active) child := active[root] if child == nil || child.cmd == nil || child.controllerOnly { t.Fatal("first helper did not start normally") } if got := strings.Join(child.cmd.Args[1:], " "); got != "--exclude-hook-program game.exe" { t.Fatalf("first launch arguments = %q", got) } firstPID := child.cmd.Process.Pid deadline := time.Now().Add(2 * time.Second) for !child.readySeen.Load() && time.Now().Before(deadline) { time.Sleep(10 * time.Millisecond) } if !child.readySeen.Load() { t.Fatal("manager did not consume the ready record") } manager.poll(active) if !child.controllerOnly || child.cmd == nil || child.cmd.Process.Pid == firstPID { t.Fatal("unconfirmed helper did not restart wholesale") } if got := strings.Join(child.cmd.Args[1:], " "); got != "--exclude-hook-program game.exe --no-dialog-reader" { t.Fatalf("wholesale launch arguments = %q", got) } wholesalePID := child.cmd.Process.Pid manager.poll(active) if child.cmd.Process.Pid != wholesalePID { t.Fatal("unconfirmed helper restarted wholesale more than once") } // The binary is remembered: a later spawn goes wholesale directly. stop(child) fresh := map[string]*process{} defer func() { for _, child := range fresh { stop(child) } }() manager.poll(fresh) replacement := fresh[root] if replacement == nil || replacement.cmd == nil || !replacement.controllerOnly { t.Fatal("remembered helper did not start wholesale directly") } if got := strings.Join(replacement.cmd.Args[1:], " "); got != "--exclude-hook-program game.exe --no-dialog-reader" { t.Fatalf("remembered launch arguments = %q", got) } }