fix(relinker): preserve inputs when writing desktop assets

This commit is contained in:
Ondra97 2026-10-11 17:11:26 +02:00
parent a11c8f7f70
commit 1f34f6e8d4
3 changed files with 82 additions and 9 deletions

View File

@ -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<std::filesystem::path> 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<Relinker::CallRegistryWriter>();
fileWriter.Write(Io::Utf8Path(registryPaths.front()), registryWriter->WriteCallRegistry(result.RegistryEntries));
for (std::size_t index = 0; index < guestArtifacts.size(); ++index)

View File

@ -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")

View File

@ -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