Merge pull request #353 from adriadam10/feat/relinker-linux-desktop-entry
feat(relinker): write a desktop entry with the game icon for Linux executables
This commit is contained in:
commit
0f863c1022
|
|
@ -4,6 +4,8 @@ add_executable(relinker
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxElfPatcher.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxDesktopEntryWriter.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsGuestStartup.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/guest/GuestImageReader.cpp
|
||||
|
|
@ -76,6 +78,7 @@ target_include_directories(relinker PRIVATE
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/io/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/codegen/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../libs
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
|
|
@ -172,6 +175,20 @@ if(BUILD_TESTING)
|
|||
)
|
||||
add_test(NAME file_writer_binary COMMAND file_writer_tests binary)
|
||||
add_test(NAME file_writer_text COMMAND file_writer_tests text)
|
||||
|
||||
add_test_executable(linux_desktop_entry_tests
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/LinuxDesktopEntryTests.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxDesktopEntryWriter.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/io/src/NativePath.cpp
|
||||
)
|
||||
target_include_directories(linux_desktop_entry_tests PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/domain/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/io/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../libs
|
||||
)
|
||||
add_test(NAME linux_desktop_entry COMMAND linux_desktop_entry_tests)
|
||||
endif()
|
||||
|
||||
add_test_executable(amd64_only_converter_tests
|
||||
|
|
@ -276,6 +293,8 @@ if(BUILD_TESTING)
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_bss.py $<TARGET_FILE:relinker>)
|
||||
add_test(NAME windows_icon COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_icon.py $<TARGET_FILE:relinker>)
|
||||
add_test(NAME linux_desktop_entry_output COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_desktop_entry_output.py $<TARGET_FILE:relinker>)
|
||||
add_test(NAME guest_intel_trampolines COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_intel_trampolines.py $<TARGET_FILE:relinker>)
|
||||
add_test(NAME guest_tls COMMAND ${Python3_EXECUTABLE}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
#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::optional<LinuxDesktopEntry> Prepare(const std::filesystem::path& sceSysDirectory, const std::filesystem::path& executablePath) const;
|
||||
std::filesystem::path Write(const LinuxDesktopEntry& entry) const;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
#include <elfpatcher/linux/LinuxDesktopEntryWriter.hpp>
|
||||
#include <domain/Types.hpp>
|
||||
#include <io/NativePath.hpp>
|
||||
#include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace Elfpatcher::Linux {
|
||||
|
||||
namespace {
|
||||
|
||||
bool fileExists(const std::filesystem::path& path) {
|
||||
std::error_code error;
|
||||
const bool result = std::filesystem::exists(path, error);
|
||||
if (error) throw Domain::RelinkerException("Cannot inspect '" + Io::Utf8Path(path) + "': " + error.message());
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> readPng(const std::filesystem::path& path) {
|
||||
std::ifstream stream(path, std::ios::binary);
|
||||
if (!stream) throw Domain::RelinkerException("Cannot open icon '" + Io::Utf8Path(path) + "'");
|
||||
std::vector<std::uint8_t> bytes{std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>()};
|
||||
constexpr std::array<std::uint8_t, 16> header{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52};
|
||||
if (bytes.size() < 33 || !std::equal(header.begin(), header.end(), bytes.begin())) throw Domain::RelinkerException("Invalid icon PNG '" + Io::Utf8Path(path) + "': missing PNG signature or IHDR");
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::string cleanTitle(const std::string& title) {
|
||||
std::string withoutMarks;
|
||||
for (std::size_t i = 0; i < title.size(); ++i) {
|
||||
const std::string_view rest(title.data() + i, title.size() - i);
|
||||
if (rest.starts_with("\xe2\x84\xa2")) { i += 2; continue; }
|
||||
if (rest.starts_with("\xc2\xae") || rest.starts_with("\xc2\xa9")) { i += 1; continue; }
|
||||
const auto c = static_cast<unsigned char>(title[i]);
|
||||
withoutMarks += c < 0x20 || c == 0x7f ? ' ' : title[i];
|
||||
}
|
||||
std::string result;
|
||||
for (const char c : withoutMarks) {
|
||||
if (c == ' ' && (result.empty() || result.back() == ' ')) continue;
|
||||
result += c;
|
||||
}
|
||||
if (!result.empty() && result.back() == ' ') result.pop_back();
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string title(const std::filesystem::path& paramJsonPath, const std::filesystem::path& executablePath) {
|
||||
if (!fileExists(paramJsonPath)) return Io::Utf8Path(executablePath.stem());
|
||||
std::string parsed;
|
||||
try {
|
||||
parsed = cleanTitle(parseParamJson(paramJsonPath).title);
|
||||
} catch (const std::exception& e) {
|
||||
throw Domain::RelinkerException("Invalid '" + Io::Utf8Path(paramJsonPath) + "': " + e.what());
|
||||
}
|
||||
return parsed.empty() ? Io::Utf8Path(executablePath.stem()) : parsed;
|
||||
}
|
||||
|
||||
std::string escapeValue(const std::string& value) {
|
||||
std::string result;
|
||||
for (const char c : value) {
|
||||
if (c == '\\') result += "\\\\";
|
||||
else result += c;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void requirePrintable(const std::filesystem::path& path) {
|
||||
const auto text = Io::Utf8Path(path);
|
||||
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) {
|
||||
if (c == '"' || c == '`' || c == '$' || c == '\\') result += '\\';
|
||||
if (c == '%') result += '%';
|
||||
result += c;
|
||||
}
|
||||
return escapeValue(result + "\"");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 std::nullopt;
|
||||
requirePrintable(executablePath);
|
||||
LinuxDesktopEntry entry;
|
||||
entry.Icon = readPng(iconSource);
|
||||
const auto name = title(sceSysDirectory / "param.json", executablePath);
|
||||
entry.IconPath = executablePath;
|
||||
entry.IconPath += ".png";
|
||||
entry.EntryPath = executablePath;
|
||||
entry.EntryPath += ".desktop";
|
||||
entry.Text = "[Desktop Entry]\n"
|
||||
"Type=Application\n"
|
||||
"Name=" + escapeValue(name) + "\n"
|
||||
"Exec=" + quoteExec(Io::Utf8Path(executablePath)) + "\n"
|
||||
"Path=" + escapeValue(Io::Utf8Path(executablePath.parent_path())) + "\n"
|
||||
"Icon=" + escapeValue(Io::Utf8Path(entry.IconPath)) + "\n"
|
||||
"Terminal=false\n"
|
||||
"Categories=Game;\n";
|
||||
return entry;
|
||||
}
|
||||
|
||||
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 '" + Io::Utf8Path(entry.IconPath) + "'");
|
||||
|
||||
std::ofstream text(entry.EntryPath, std::ios::binary);
|
||||
text << entry.Text;
|
||||
text.close();
|
||||
if (!text) throw Domain::RelinkerException("Failed to write desktop entry '" + Io::Utf8Path(entry.EntryPath) + "'");
|
||||
|
||||
std::error_code error;
|
||||
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 '" + Io::Utf8Path(entry.EntryPath) + "' executable: " + error.message());
|
||||
return entry.EntryPath;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
#include <elfpatcher/linux/LinuxDesktopEntryWriter.hpp>
|
||||
#include <domain/Types.hpp>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
const std::vector<std::uint8_t> Png{
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01,
|
||||
0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x44, 0x41,
|
||||
0x54, 0x78, 0x9c, 0x63, 0xf8, 0xcf, 0xc0, 0xf0, 0x1f, 0x00, 0x05, 0x00, 0x01, 0xff, 0x89, 0x99, 0x3d, 0x1d, 0x00, 0x00,
|
||||
0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82};
|
||||
|
||||
void require(const bool condition, const std::string& message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
void writeBytes(const std::filesystem::path& path, const std::vector<std::uint8_t>& bytes) {
|
||||
std::filesystem::create_directories(path.parent_path());
|
||||
std::ofstream stream(path, std::ios::binary);
|
||||
stream.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
|
||||
}
|
||||
|
||||
void writeText(const std::filesystem::path& path, const std::string& text) {
|
||||
writeBytes(path, std::vector<std::uint8_t>(text.begin(), text.end()));
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> readBytes(const std::filesystem::path& path) {
|
||||
std::ifstream stream(path, std::ios::binary);
|
||||
return {std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>()};
|
||||
}
|
||||
|
||||
std::string param(const std::string& title) {
|
||||
return R"({"titleId": "PPSA00000", "defaultLanguage": "en-US", "localizedParameters": {"en-US": {"titleName": ")" + title + R"("}}})";
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> readEntry(const std::filesystem::path& path) {
|
||||
std::ifstream stream(path);
|
||||
std::string line;
|
||||
std::getline(stream, line);
|
||||
require(line == "[Desktop Entry]", "Missing [Desktop Entry] group in " + path.string());
|
||||
std::map<std::string, std::string> fields;
|
||||
while (std::getline(stream, line)) {
|
||||
const auto separator = line.find('=');
|
||||
require(separator != std::string::npos, "Malformed desktop entry line: " + line);
|
||||
fields[line.substr(0, separator)] = line.substr(separator + 1);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
void requireField(const std::map<std::string, std::string>& fields, const std::string& key, const std::string& expected) {
|
||||
const auto found = fields.find(key);
|
||||
require(found != fields.end() && found->second == expected, key + " is '" + (found == fields.end() ? "<missing>" : found->second) + "', expected '" + expected + "'");
|
||||
}
|
||||
|
||||
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().Prepare(sceSys, executable);
|
||||
} catch (const Domain::RelinkerException& e) {
|
||||
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("Prepare succeeded despite " + reason);
|
||||
}
|
||||
|
||||
void run(const std::filesystem::path& root) {
|
||||
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";
|
||||
const auto titled = root / "titled" / "out dir" / "game";
|
||||
writeBytes(titledSceSys / "icon0.png", Png);
|
||||
writeText(titledSceSys / "param.json", param("Some™ Game®:\\n Edition© "));
|
||||
std::filesystem::create_directories(titled.parent_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");
|
||||
requireField(fields, "Exec", "\"" + titled.string() + "\"");
|
||||
requireField(fields, "Path", titled.parent_path().string());
|
||||
requireField(fields, "Icon", titled.string() + ".png");
|
||||
requireField(fields, "Terminal", "false");
|
||||
require(readBytes(titled.string() + ".png") == Png, "Icon bytes changed");
|
||||
const auto permissions = std::filesystem::status(titled.string() + ".desktop").permissions();
|
||||
require((permissions & std::filesystem::perms::owner_exec) != std::filesystem::perms::none, "Desktop entry is not executable");
|
||||
|
||||
const auto untitledSceSys = root / "untitled" / "game" / "sce_sys";
|
||||
const auto untitled = root / "untitled" / "out" / "100% $HOME";
|
||||
writeBytes(untitledSceSys / "icon0.png", Png);
|
||||
std::filesystem::create_directories(untitled.parent_path());
|
||||
write(untitledSceSys, untitled);
|
||||
const auto untitledFields = readEntry(untitled.string() + ".desktop");
|
||||
requireField(untitledFields, "Name", "100% $HOME");
|
||||
requireField(untitledFields, "Exec", "\"" + (root / "untitled" / "out").string() + "/100%% \\\\$HOME\"");
|
||||
|
||||
const auto badPngSceSys = root / "badpng" / "game" / "sce_sys";
|
||||
writeText(badPngSceSys / "icon0.png", "GIF89a" + std::string(40, '\0'));
|
||||
std::filesystem::create_directories(root / "badpng" / "out");
|
||||
requireFailure(badPngSceSys, root / "badpng" / "out" / "game", "icon0.png");
|
||||
|
||||
const auto badParamSceSys = root / "badparam" / "game" / "sce_sys";
|
||||
writeBytes(badParamSceSys / "icon0.png", Png);
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
const auto root = std::filesystem::temp_directory_path() / "anyps5-desktop-entry-tests";
|
||||
std::filesystem::remove_all(root);
|
||||
try {
|
||||
run(root);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << e.what() << '\n';
|
||||
std::filesystem::remove_all(root);
|
||||
return 1;
|
||||
}
|
||||
std::filesystem::remove_all(root);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
#include <io/FileReader.hpp>
|
||||
#include <io/NativePath.hpp>
|
||||
#include <elfpatcher/linux/LinuxElfPatcher.hpp>
|
||||
#include <elfpatcher/linux/LinuxDesktopEntryWriter.hpp>
|
||||
#include <elfpatcher/general/SegmentFilter.hpp>
|
||||
#include <elfpatcher/general/EntryStubBuilder.hpp>
|
||||
#include <elfpatcher/general/ProgramHeaderLayoutBuilder.hpp>
|
||||
|
|
@ -111,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 || args.toMacos ? std::nullopt : desktopEntryWriter.Prepare(Io::NativePath(args.inputPath).parent_path() / "sce_sys", Io::NativePath(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"));
|
||||
|
|
@ -154,6 +158,7 @@ int Run(const int argc, char* argv[]) {
|
|||
std::cout << "Guest module: " << Io::Utf8Path(artifact.Path) << '\n';
|
||||
}
|
||||
fileWriter.Write(absPath, executableBytes);
|
||||
if (desktopEntry) std::cout << "Desktop entry: " << Io::Utf8Path(desktopEntryWriter.Write(*desktopEntry)) << '\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"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
from pathlib import Path
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import zlib
|
||||
|
||||
from test_guest_intel_trampolines import main_fixture as fixture
|
||||
|
||||
|
||||
def chunk(kind, data):
|
||||
return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data))
|
||||
|
||||
|
||||
def png(width, height):
|
||||
rows = b"".join(b"\0" + bytes((15, 65, 200, 255)) * width for _ in range(height))
|
||||
return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)) +
|
||||
chunk(b"IDAT", zlib.compress(rows)) + chunk(b"IEND", b""))
|
||||
|
||||
|
||||
def unescape(value):
|
||||
result, index = "", 0
|
||||
while index < len(value):
|
||||
if value[index] == "\\" and index + 1 < len(value):
|
||||
result += {"s": " ", "n": "\n", "t": "\t", "r": "\r", "\\": "\\"}[value[index + 1]]
|
||||
index += 2
|
||||
else:
|
||||
result += value[index]
|
||||
index += 1
|
||||
return result
|
||||
|
||||
|
||||
def unquote(value):
|
||||
assert value.startswith('"') and value.endswith('"'), value
|
||||
result, index = "", 1
|
||||
while index < len(value) - 1:
|
||||
if value[index] == "\\":
|
||||
assert value[index + 1] in '"`$\\', value
|
||||
index += 1
|
||||
elif value[index] == "%":
|
||||
assert value[index + 1] == "%", value
|
||||
index += 1
|
||||
result += value[index]
|
||||
index += 1
|
||||
return result
|
||||
|
||||
|
||||
def entry(path):
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
assert lines[0] == "[Desktop Entry]", lines
|
||||
return dict(line.split("=", 1) for line in lines[1:])
|
||||
|
||||
|
||||
def main():
|
||||
relinker = Path(sys.argv[1]).resolve()
|
||||
with tempfile.TemporaryDirectory(prefix="anyps5-desktop-") as directory:
|
||||
work = Path(directory)
|
||||
app = work / "app with spaces"
|
||||
(app / "sce_sys").mkdir(parents=True)
|
||||
source = app / "eboot.bin"
|
||||
source.write_bytes(fixture())
|
||||
|
||||
def relink(name, *options):
|
||||
output = work / name
|
||||
result = subprocess.run([str(relinker), "--skip-sce-module", *options, str(source), str(output)],
|
||||
cwd=work, capture_output=True, text=True, timeout=20)
|
||||
desktop, icon = Path(f"{output}.desktop"), Path(f"{output}.png")
|
||||
return result, output, desktop, icon
|
||||
|
||||
result, output, desktop, icon = relink("no-icon")
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)
|
||||
assert output.exists() and not desktop.exists() and not icon.exists()
|
||||
|
||||
image = png(64, 64)
|
||||
(app / "sce_sys" / "icon0.png").write_bytes(image)
|
||||
(app / "sce_sys" / "param.json").write_text(json.dumps(
|
||||
{"titleId": "PPSA00000", "defaultLanguage": "en-US",
|
||||
"localizedParameters": {"en-US": {"titleName": "Sample Title"}}}))
|
||||
|
||||
result, output, desktop, icon = relink("linux")
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)
|
||||
assert icon.read_bytes() == image
|
||||
fields = entry(desktop)
|
||||
assert fields["Name"] == "Sample Title", fields
|
||||
assert unquote(unescape(fields["Exec"])) == str(output), fields
|
||||
assert unescape(fields["Path"]) == str(work) and unescape(fields["Icon"]) == str(icon), fields
|
||||
assert os.name == "nt" or desktop.stat().st_mode & 0o111
|
||||
validator = shutil.which("desktop-file-validate")
|
||||
if validator:
|
||||
checked = subprocess.run([validator, str(desktop)], capture_output=True, text=True)
|
||||
assert checked.returncode == 0 and not checked.stdout.strip(), checked.stdout
|
||||
|
||||
for name, option in (("macos", "--macos"), ("windows.exe", "--windows")):
|
||||
result, output, desktop, icon = relink(name, option)
|
||||
assert result.returncode == 0, (name, result.stdout, result.stderr)
|
||||
assert output.exists() and not desktop.exists() and not icon.exists(), name
|
||||
|
||||
(app / "sce_sys" / "icon0.png").write_bytes(b"not a PNG")
|
||||
result, output, desktop, icon = relink("bad-icon")
|
||||
assert result.returncode != 0, (result.stdout, result.stderr)
|
||||
assert not output.exists() and not desktop.exists() and not icon.exists()
|
||||
assert "Invalid icon PNG" in result.stderr, result.stderr
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue