diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index f1b4a5b64..76afdbb42 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -115,6 +115,19 @@ int Run(const int argc, char* argv[]) { const Elfpatcher::Linux::LinuxDesktopEntryWriter desktopEntryWriter; const auto desktopEntry = args.toWindows || args.toMacos ? std::nullopt : desktopEntryWriter.Prepare(Io::NativePath(args.inputPath).parent_path() / "sce_sys", Io::NativePath(absPath)); + const auto checkOutputInputs = [&](const std::filesystem::path& path, const char* kind) { + if (!std::filesystem::exists(path)) return; + if (std::filesystem::equivalent(Io::NativePath(args.inputPath), path)) + throw Domain::RelinkerException(std::string(kind) + " output would overwrite the input executable: " + Io::Utf8Path(path)); + for (const auto& artifact : guestArtifacts) + if (std::filesystem::equivalent(artifact.SourcePath, path)) + throw Domain::RelinkerException(std::string(kind) + " output would overwrite an input module: " + Io::Utf8Path(path)); + }; + if (desktopEntry) { + checkOutputInputs(desktopEntry->IconPath, "Icon"); + checkOutputInputs(desktopEntry->EntryPath, "Desktop entry"); + } + if (args.writeRegistry) { const std::filesystem::path outFsPath = Io::NativePath(absPath); std::vector registryPaths{ @@ -122,14 +135,7 @@ int Run(const int argc, char* argv[]) { }; for (const auto& artifact : guestArtifacts) registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + "." + Io::Utf8Path(artifact.Path.filename()) + ".registry.json")); - for (const auto& path : registryPaths) { - if (!std::filesystem::exists(path)) continue; - if (std::filesystem::equivalent(Io::NativePath(args.inputPath), path)) - throw Domain::RelinkerException("Registry output would overwrite the input executable: " + Io::Utf8Path(path)); - for (const auto& artifact : guestArtifacts) - if (std::filesystem::equivalent(artifact.SourcePath, path)) - throw Domain::RelinkerException("Registry output would overwrite an input module: " + Io::Utf8Path(path)); - } + for (const auto& path : registryPaths) checkOutputInputs(path, "Registry"); const auto registryWriter = std::make_shared(); fileWriter.Write(Io::Utf8Path(registryPaths.front()), registryWriter->WriteCallRegistry(result.RegistryEntries)); for (std::size_t index = 0; index < guestArtifacts.size(); ++index) diff --git a/core/relinker/relinker/tests/test_input_preservation.py b/core/relinker/relinker/tests/test_input_preservation.py index 851e35fcf..2114abbe3 100644 --- a/core/relinker/relinker/tests/test_input_preservation.py +++ b/core/relinker/relinker/tests/test_input_preservation.py @@ -5,6 +5,7 @@ import sys import tempfile from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture, main_fixture +from test_linux_desktop_entry_output import png def check_registry_inputs(relinker, work, windows): @@ -52,10 +53,66 @@ def check_registry_inputs(relinker, work, windows): assert not (case / "app0").exists() +def check_desktop_inputs(relinker, work): + for desktop in (False, True): + for bundled in (False, True): + for alias in ("direct", "hardlink", "symlink"): + for registry in (False, True): + case = work / f"desktop-{desktop}-{bundled}-{alias}-{registry}" + modules = case / "sce_module" + modules.mkdir(parents=True) + sce_sys = case / "sce_sys" + sce_sys.mkdir() + image = png(1, 1) + (sce_sys / "icon0.png").write_bytes(image) + output = (modules if bundled and alias == "direct" else case) / "output.elf" + paths = [Path(f"{output}.png"), Path(f"{output}.desktop")] + target = paths[int(desktop)] + source = target if not bundled and alias == "direct" else case / "input.elf" + original = bytes(main_fixture()) + source.write_bytes(original) + module = target 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, target) + elif alias == "symlink": + try: + target.symlink_to(protected) + except OSError as error: + if os.name != "nt" or error.winerror != 1314: + raise + print("Desktop symbolic-link case skipped: Windows symlink privilege unavailable") + continue + previous_output = b"previous executable" + output.write_bytes(previous_output) + untouched = paths[int(not desktop)] + previous_auxiliary = b"previous auxiliary output" + untouched.write_bytes(previous_auxiliary) + registry_path = output.with_suffix(".registry.json") + previous_registry = b"previous registry" + registry_path.write_bytes(previous_registry) + options = ["--registry"] if registry else [] + result = subprocess.run([str(relinker), *options, str(source), str(output)], + capture_output=True, text=True, timeout=20) + kind = "Desktop entry" if desktop else "Icon" + owner = "an input module" if bundled else "the input executable" + assert result.returncode == 2 and f"{kind} output would overwrite {owner}" in result.stderr, (result.stdout, result.stderr) + assert str(target) in result.stderr, result.stderr + assert source.read_bytes() == original and module.read_bytes() == module_original + assert output.read_bytes() == previous_output and untouched.read_bytes() == previous_auxiliary + assert registry_path.read_bytes() == previous_registry + assert target.read_bytes() == (module_original if bundled else original) + assert (sce_sys / "icon0.png").read_bytes() == image + assert not (output.parent / "app0").exists() + + def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-input-preservation-") as directory: work = Path(directory) + check_desktop_inputs(relinker, work) for windows in (False, True): options = ["--windows"] if windows else [] check_registry_inputs(relinker, work, windows) @@ -115,6 +172,13 @@ def main(): module_original = guest_fixture(PLAIN_SITE) module.write_bytes(module_original) output = case / ("output.exe" if windows else "output.elf") + if not windows: + (case / "sce_sys").mkdir() + image = png(1, 1) + (case / "sce_sys" / "icon0.png").write_bytes(image) + if existing: + Path(f"{output}.png").write_bytes(b"previous icon") + Path(f"{output}.desktop").write_bytes(b"previous desktop entry") if existing: output.write_bytes(b"previous output") result = subprocess.run([str(relinker), *options, "--registry", str(source), str(output)], @@ -126,6 +190,9 @@ def main(): or not output.with_suffix(".registry.json").exists() or source.read_bytes() != original or module.read_bytes() != module_original): raise AssertionError((windows, existing, result.returncode, result.stdout, result.stderr)) + if not windows: + assert Path(f"{output}.png").read_bytes() == image + assert Path(f"{output}.desktop").read_text().startswith("[Desktop Entry]\n") print("Input preservation tests passed") diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 2aa4daa4a..ca2718f95 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 output must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. An existing output file can be replaced if it is separate from those inputs. +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. ## Options