From 286cf28ec7e397432bcd9fa04a0e7d25d071a78f Mon Sep 17 00:00:00 2001 From: thuanquang <60096168+thuanquang@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:17:12 +0700 Subject: [PATCH] fix(relinker): reject a directory as the executable output path An existing directory, or a path ending in a separator, was only rejected when the executable was written, after the converted modules had already been written to app0/ beside or inside it. Co-Authored-By: Claude Opus 5.5 --- core/relinker/main.cpp | 2 ++ .../relinker/tests/test_input_preservation.py | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 95652550c..cf63c3296 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -91,6 +91,8 @@ int Run(const int argc, char* argv[]) { if (std::filesystem::exists(Io::NativePath(args.inputPath)) && std::filesystem::exists(Io::NativePath(args.outputPath)) && std::filesystem::equivalent(Io::NativePath(args.inputPath), Io::NativePath(args.outputPath))) throw Domain::RelinkerException("Executable output would overwrite the input executable"); + if (Io::NativePath(args.outputPath).filename().empty() || std::filesystem::is_directory(Io::NativePath(args.outputPath))) + throw Domain::RelinkerException("Executable output is a directory: " + args.outputPath); const std::string absPath = Io::Utf8Path(std::filesystem::absolute(Io::NativePath(args.outputPath))); struct ExtraExecutable { diff --git a/core/relinker/relinker/tests/test_input_preservation.py b/core/relinker/relinker/tests/test_input_preservation.py index 3ec4114b3..10e7c5117 100644 --- a/core/relinker/relinker/tests/test_input_preservation.py +++ b/core/relinker/relinker/tests/test_input_preservation.py @@ -142,6 +142,26 @@ def check_registry_after_failed_patch(relinker, work): assert not (case / "app0").exists() +def check_output_directory(relinker, work): + for windows in (False, True): + for existing in (False, True): + case = work / f"output-directory-{windows}-{existing}" + (case / "sce_module").mkdir(parents=True) + (case / "sce_module" / "sample.prx").write_bytes(guest_fixture(PLAIN_SITE)) + source = case / "input.elf" + source.write_bytes(main_fixture()) + target = case / "output" + if existing: + target.mkdir() + output = str(target) if existing else str(target) + os.sep + options = ["--windows"] if windows else [] + result = subprocess.run([str(relinker), *options, str(source), output], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Executable output is a directory" in result.stderr, (result.stdout, result.stderr) + assert not (case / "app0").exists() and not (target / "app0").exists() + assert target.exists() == existing + + def check_desktop_inputs(relinker, work): for desktop in (False, True): for bundled in (False, True): @@ -204,6 +224,7 @@ def main(): check_diagnostic_inputs(relinker, work) check_desktop_inputs(relinker, work) check_registry_after_failed_patch(relinker, work) + check_output_directory(relinker, work) for windows in (False, True): options = ["--windows"] if windows else [] check_registry_inputs(relinker, work, windows)