fix(relinker): validate the desktop entry before writing the executable
The icon, title and output path are checked before the ELF is written, so a malformed icon0.png or param.json leaves no output behind. Paths with control characters are refused instead of producing an invalid entry. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1ff7b534fe
commit
ca330dfefb
|
|
@ -1,13 +1,25 @@
|
|||
#ifndef ELFPATCHER_LINUX_LINUXDESKTOPENTRYWRITER_HPP
|
||||
#define ELFPATCHER_LINUX_LINUXDESKTOPENTRYWRITER_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Elfpatcher::Linux {
|
||||
|
||||
struct LinuxDesktopEntry {
|
||||
std::filesystem::path IconPath;
|
||||
std::vector<std::uint8_t> 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<LinuxDesktopEntry> Prepare(const std::filesystem::path& sceSysDirectory, const std::filesystem::path& executablePath) const;
|
||||
std::filesystem::path Write(const LinuxDesktopEntry& entry) const;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<unsigned char>(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<LinuxDesktopEntry> 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<const char*>(iconBytes.data()), static_cast<std::streamsize>(iconBytes.size()));
|
||||
std::filesystem::path LinuxDesktopEntryWriter::Write(const LinuxDesktopEntry& entry) const {
|
||||
std::ofstream icon(entry.IconPath, std::ios::binary);
|
||||
icon.write(reinterpret_cast<const char*>(entry.Icon.data()), static_cast<std::streamsize>(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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,21 +61,25 @@ void requireField(const std::map<std::string, std::string>& fields, const std::s
|
|||
require(found != fields.end() && found->second == expected, key + " is '" + (found == fields.end() ? "<missing>" : 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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue