From 4b6bb2f05928d8026d89459b22d3cba0a532ac6d Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Mon, 12 Oct 2026 00:01:25 -0700 Subject: [PATCH] fix(relinker): preserve inputs when writing Windows diagnostics --- core/relinker/main.cpp | 2 + .../relinker/tests/test_input_preservation.py | 67 +++++++++++++++++++ docs/user/USAGE.md | 2 +- 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 0e747853d..4a7b7a7da 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -127,6 +127,8 @@ int Run(const int argc, char* argv[]) { checkOutputInputs(desktopEntry->IconPath, "Icon"); checkOutputInputs(desktopEntry->EntryPath, "Desktop entry"); } + if (args.windowsDiagnostics) + checkOutputInputs(std::filesystem::absolute("windows-diagnostics-imports.txt"), "Windows import diagnostics"); if (args.writeRegistry) { const std::filesystem::path outFsPath = Io::NativePath(absPath); diff --git a/core/relinker/relinker/tests/test_input_preservation.py b/core/relinker/relinker/tests/test_input_preservation.py index 2114abbe3..7863f55a7 100644 --- a/core/relinker/relinker/tests/test_input_preservation.py +++ b/core/relinker/relinker/tests/test_input_preservation.py @@ -8,6 +8,72 @@ from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture, main_fixture from test_linux_desktop_entry_output import png +def check_diagnostic_inputs(relinker, work): + filename = "windows-diagnostics-imports.txt" + for bundled in (False, True): + for alias in ("direct", "hardlink", "symlink"): + for registry in (False, True): + case = work / f"diagnostics-{bundled}-{alias}-{registry}" + modules = case / "sce_module" + modules.mkdir(parents=True) + diagnostics = (modules if bundled and alias == "direct" else case) / filename + source = diagnostics if not bundled and alias == "direct" else case / "input.elf" + original = bytes(main_fixture()) + source.write_bytes(original) + module = diagnostics if bundled and alias == "direct" else modules / "sample.prx" + module_original = bytes(guest_fixture(PLAIN_SITE)) + module.write_bytes(module_original) + protected = module if bundled else source + if alias == "hardlink": + os.link(protected, diagnostics) + elif alias == "symlink": + try: + diagnostics.symlink_to(protected) + except OSError as error: + if os.name != "nt" or error.winerror != 1314: + raise + print("Diagnostic symbolic-link case skipped: Windows symlink privilege unavailable") + continue + output = case / "output.exe" + previous_output = b"previous executable" + output.write_bytes(previous_output) + registry_path = case / "output.registry.json" + previous_registry = b"previous registry" + registry_path.write_bytes(previous_registry) + options = [] if bundled else ["--skip-sce-module"] + if registry: + options.append("--registry") + result = subprocess.run([str(relinker), "--windows", "--windows-diagnostics", *options, + str(source), str(output)], cwd=diagnostics.parent, + capture_output=True, text=True, timeout=20) + owner = "an input module" if bundled else "the input executable" + assert result.returncode == 2 and f"Windows import diagnostics output would overwrite {owner}" in result.stderr, (result.stdout, result.stderr) + assert str(diagnostics) in result.stderr, result.stderr + assert source.read_bytes() == original and module.read_bytes() == module_original + assert diagnostics.read_bytes() == (module_original if bundled else original) + assert output.read_bytes() == previous_output and registry_path.read_bytes() == previous_registry + assert not (case / "app0").exists() + + for enabled in (False, True): + for existing in (False, True): + case = work / f"diagnostics-valid-{enabled}-{existing}" + case.mkdir() + diagnostics = case / filename + source = case / "input.elf" if enabled else diagnostics + original = bytes(main_fixture()) + source.write_bytes(original) + if enabled and existing: + diagnostics.write_bytes(b"previous diagnostics") + output = case / "output.exe" + options = ["--windows-diagnostics"] if enabled else [] + result = subprocess.run([str(relinker), "--windows", "--skip-sce-module", *options, + str(source), str(output)], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 0 and output.read_bytes().startswith(b"MZ"), (result.stdout, result.stderr) + assert source.read_bytes() == original + assert diagnostics.read_bytes() == (b"" if enabled else original) + + def check_registry_inputs(relinker, work, windows): options = ["--windows"] if windows else [] for module_registry in (False, True): @@ -112,6 +178,7 @@ def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-input-preservation-") as directory: work = Path(directory) + check_diagnostic_inputs(relinker, work) check_desktop_inputs(relinker, work) for windows in (False, True): options = ["--windows"] if windows else [] diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 12accb979..4f2ba2837 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -35,7 +35,7 @@ relinker --macos source/input.elf eboot Add `--to-intel` for Intel hosts, or `--to-rosetta` for Rosetta; a title that runs under Rosetta on Apple silicon is relinked with `--macos --to-rosetta`. The output format defaults to Linux ELF regardless of the filename; `.exe` alone does not select Windows. -The executable, registry, icon and desktop entry output files must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. These collisions fail before any output is written. An existing output file can be replaced if it is separate from those inputs. +The executable, registry, Windows import diagnostics, icon and desktop entry output files must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. These collisions fail before any output is written. An existing output file can be replaced if it is separate from those inputs. ## Options