diff --git a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxDesktopEntryWriter.hpp b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxDesktopEntryWriter.hpp index fbefc0d6d..b4b95f3ee 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxDesktopEntryWriter.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxDesktopEntryWriter.hpp @@ -1,13 +1,25 @@ #ifndef ELFPATCHER_LINUX_LINUXDESKTOPENTRYWRITER_HPP #define ELFPATCHER_LINUX_LINUXDESKTOPENTRYWRITER_HPP +#include #include +#include +#include +#include namespace Elfpatcher::Linux { +struct LinuxDesktopEntry { + std::filesystem::path IconPath; + std::vector Icon; + std::filesystem::path EntryPath; + std::string Text; +}; + class LinuxDesktopEntryWriter { public: - std::filesystem::path Write(const std::filesystem::path& sceSysDirectory, const std::filesystem::path& executablePath) const; + std::optional Prepare(const std::filesystem::path& sceSysDirectory, const std::filesystem::path& executablePath) const; + std::filesystem::path Write(const LinuxDesktopEntry& entry) const; }; } diff --git a/core/relinker/elfpatcher/src/linux/LinuxDesktopEntryWriter.cpp b/core/relinker/elfpatcher/src/linux/LinuxDesktopEntryWriter.cpp index 97f8beda3..8dbe77091 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxDesktopEntryWriter.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxDesktopEntryWriter.cpp @@ -68,6 +68,15 @@ std::string escapeValue(const std::string& value) { return result; } +void requirePrintable(const std::filesystem::path& path) { + const auto text = path.string(); + const bool control = std::any_of(text.begin(), text.end(), [](const char c) { + const auto byte = static_cast(c); + return byte < 0x20 || byte == 0x7f; + }); + if (control) throw Domain::RelinkerException("Cannot write a desktop entry for '" + text + "': the path contains a control character"); +} + std::string quoteExec(const std::string& path) { std::string result = "\""; for (const char c : path) { @@ -80,35 +89,41 @@ std::string quoteExec(const std::string& path) { } -std::filesystem::path LinuxDesktopEntryWriter::Write(const std::filesystem::path& sceSysDirectory, const std::filesystem::path& executablePath) const { +std::optional LinuxDesktopEntryWriter::Prepare(const std::filesystem::path& sceSysDirectory, const std::filesystem::path& executablePath) const { const auto iconSource = sceSysDirectory / "icon0.png"; - if (!fileExists(iconSource)) return {}; - const auto iconBytes = readPng(iconSource); + if (!fileExists(iconSource)) return std::nullopt; + requirePrintable(executablePath); + LinuxDesktopEntry entry; + entry.Icon = readPng(iconSource); const auto name = title(sceSysDirectory / "param.json", executablePath); + entry.IconPath = std::filesystem::path(executablePath.string() + ".png"); + entry.EntryPath = std::filesystem::path(executablePath.string() + ".desktop"); + entry.Text = "[Desktop Entry]\n" + "Type=Application\n" + "Name=" + escapeValue(name) + "\n" + "Exec=" + quoteExec(executablePath.string()) + "\n" + "Path=" + escapeValue(executablePath.parent_path().string()) + "\n" + "Icon=" + escapeValue(entry.IconPath.string()) + "\n" + "Terminal=false\n" + "Categories=Game;\n"; + return entry; +} - const auto iconPath = std::filesystem::path(executablePath.string() + ".png"); - std::ofstream icon(iconPath, std::ios::binary); - icon.write(reinterpret_cast(iconBytes.data()), static_cast(iconBytes.size())); +std::filesystem::path LinuxDesktopEntryWriter::Write(const LinuxDesktopEntry& entry) const { + std::ofstream icon(entry.IconPath, std::ios::binary); + icon.write(reinterpret_cast(entry.Icon.data()), static_cast(entry.Icon.size())); icon.close(); - if (!icon) throw Domain::RelinkerException("Failed to write icon '" + iconPath.string() + "'"); + if (!icon) throw Domain::RelinkerException("Failed to write icon '" + entry.IconPath.string() + "'"); - const auto entryPath = std::filesystem::path(executablePath.string() + ".desktop"); - std::ofstream entry(entryPath, std::ios::binary); - entry << "[Desktop Entry]\n" - << "Type=Application\n" - << "Name=" << escapeValue(name) << '\n' - << "Exec=" << quoteExec(executablePath.string()) << '\n' - << "Path=" << escapeValue(executablePath.parent_path().string()) << '\n' - << "Icon=" << escapeValue(iconPath.string()) << '\n' - << "Terminal=false\n" - << "Categories=Game;\n"; - entry.close(); - if (!entry) throw Domain::RelinkerException("Failed to write desktop entry '" + entryPath.string() + "'"); + std::ofstream text(entry.EntryPath, std::ios::binary); + text << entry.Text; + text.close(); + if (!text) throw Domain::RelinkerException("Failed to write desktop entry '" + entry.EntryPath.string() + "'"); std::error_code error; - std::filesystem::permissions(entryPath, std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec, std::filesystem::perm_options::add, error); - if (error) throw Domain::RelinkerException("Cannot mark desktop entry '" + entryPath.string() + "' executable: " + error.message()); - return entryPath; + std::filesystem::permissions(entry.EntryPath, std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec, std::filesystem::perm_options::add, error); + if (error) throw Domain::RelinkerException("Cannot mark desktop entry '" + entry.EntryPath.string() + "' executable: " + error.message()); + return entry.EntryPath; } } diff --git a/core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp b/core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp index 37185e5ec..bfbaf19eb 100644 --- a/core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp +++ b/core/relinker/elfpatcher/tests/LinuxDesktopEntryTests.cpp @@ -61,21 +61,25 @@ void requireField(const std::map& fields, const std::s require(found != fields.end() && found->second == expected, key + " is '" + (found == fields.end() ? "" : found->second) + "', expected '" + expected + "'"); } -void requireFailure(const std::filesystem::path& sceSys, const std::filesystem::path& executable, const std::string& path) { +std::filesystem::path write(const std::filesystem::path& sceSys, const std::filesystem::path& executable) { + const Elfpatcher::Linux::LinuxDesktopEntryWriter writer; + const auto entry = writer.Prepare(sceSys, executable); + return entry ? writer.Write(*entry) : std::filesystem::path{}; +} + +void requireFailure(const std::filesystem::path& sceSys, const std::filesystem::path& executable, const std::string& reason) { try { - Elfpatcher::Linux::LinuxDesktopEntryWriter().Write(sceSys, executable); + Elfpatcher::Linux::LinuxDesktopEntryWriter().Prepare(sceSys, executable); } catch (const Domain::RelinkerException& e) { - require(std::string(e.what()).find(path) != std::string::npos, "Failure does not name " + path + ": " + e.what()); - require(!std::filesystem::exists(executable.string() + ".desktop"), "Desktop entry written despite the failure"); + require(std::string(e.what()).find(reason) != std::string::npos, "Failure does not name " + reason + ": " + e.what()); + require(!std::filesystem::exists(executable.string() + ".desktop") && !std::filesystem::exists(executable.string() + ".png"), "Files written despite the failure"); return; } - throw std::runtime_error("Write succeeded for a malformed " + path); + throw std::runtime_error("Prepare succeeded despite " + reason); } void run(const std::filesystem::path& root) { - const Elfpatcher::Linux::LinuxDesktopEntryWriter writer; - - require(writer.Write(root / "none" / "sce_sys", root / "none" / "out" / "game").empty(), "Desktop entry written without icon0.png"); + require(write(root / "none" / "sce_sys", root / "none" / "out" / "game").empty(), "Desktop entry written without icon0.png"); require(!std::filesystem::exists(root / "none" / "out"), "Files written without icon0.png"); const auto titledSceSys = root / "titled" / "game" / "sce_sys"; @@ -83,7 +87,7 @@ void run(const std::filesystem::path& root) { writeBytes(titledSceSys / "icon0.png", Png); writeText(titledSceSys / "param.json", param("Some™ Game®:\\n Edition© ")); std::filesystem::create_directories(titled.parent_path()); - require(writer.Write(titledSceSys, titled) == titled.string() + ".desktop", "Unexpected desktop entry path"); + require(write(titledSceSys, titled) == titled.string() + ".desktop", "Unexpected desktop entry path"); const auto fields = readEntry(titled.string() + ".desktop"); requireField(fields, "Type", "Application"); requireField(fields, "Name", "Some Game: Edition"); @@ -99,7 +103,7 @@ void run(const std::filesystem::path& root) { const auto untitled = root / "untitled" / "out" / "100% $HOME"; writeBytes(untitledSceSys / "icon0.png", Png); std::filesystem::create_directories(untitled.parent_path()); - writer.Write(untitledSceSys, untitled); + write(untitledSceSys, untitled); const auto untitledFields = readEntry(untitled.string() + ".desktop"); requireField(untitledFields, "Name", "100% $HOME"); requireField(untitledFields, "Exec", "\"" + (root / "untitled" / "out").string() + "/100%% \\\\$HOME\""); @@ -114,6 +118,14 @@ void run(const std::filesystem::path& root) { writeText(badParamSceSys / "param.json", "{"); std::filesystem::create_directories(root / "badparam" / "out"); requireFailure(badParamSceSys, root / "badparam" / "out" / "game", "param.json"); + + const auto controlSceSys = root / "control" / "game" / "sce_sys"; + writeBytes(controlSceSys / "icon0.png", Png); + for (const char control : {'\n', '\t', '\r', '\x7f'}) { + const auto directory = root / "control" / (std::string("out") + control + "dir"); + std::filesystem::create_directories(directory); + requireFailure(controlSceSys, directory / "game", "control character"); + } } } diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index b1b0d0581..0cdb22105 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -112,6 +112,9 @@ int Run(const int argc, char* argv[]) { guestArtifacts = Relinker::GuestModuleBuilder().Build(Io::NativePath(args.inputPath), Io::NativePath(absPath), result.DynamicSection, args.toWindows, args.toMacos, args.toIntel, args.toRosetta, *syscallScanner, args.lazyBinding, args.runPath, args.excludedSceModules, Io::NativePath(args.sceModulePath), args.moduleDirectories); } + const Elfpatcher::Linux::LinuxDesktopEntryWriter desktopEntryWriter; + const auto desktopEntry = args.toWindows ? std::nullopt : desktopEntryWriter.Prepare(std::filesystem::path(args.inputPath).parent_path() / "sce_sys", absPath); + if (args.writeRegistry) { const std::filesystem::path outFsPath = Io::NativePath(absPath); const std::string registryPath = Io::Utf8Path(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + ".registry.json")); @@ -155,10 +158,7 @@ int Run(const int argc, char* argv[]) { std::cout << "Guest module: " << Io::Utf8Path(artifact.Path) << '\n'; } fileWriter.Write(absPath, executableBytes); - if (!args.toWindows) { - const auto desktopEntry = Elfpatcher::Linux::LinuxDesktopEntryWriter().Write(std::filesystem::path(args.inputPath).parent_path() / "sce_sys", absPath); - if (!desktopEntry.empty()) std::cout << "Desktop entry: " << desktopEntry.string() << '\n'; - } + if (desktopEntry) std::cout << "Desktop entry: " << desktopEntryWriter.Write(*desktopEntry).string() << '\n'; std::cout << "External prx references: " << result.RegistryEntries.size() << "\nOutput file: " << absPath << '\n'; std::cout << "Expected runtime layout (relative to the output executable):\n" << Io::Utf8Path(Io::NativePath(absPath).filename()) << "\n"