diff --git a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp index e5f172fcea6e..c2c9b2d2c6a9 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp @@ -6,12 +6,14 @@ #include #include #include +#include #include #include #include namespace { +std::mutex g_metadataMutex; bool g_titleLoaded = false; bool g_iconAttempted = false; bool g_iconLoaded = false; @@ -32,6 +34,7 @@ void copyToFixedBuffer(char* destination, std::size_t destinationSize, const std } void ensureTitleLoaded() { + std::lock_guard lock(g_metadataMutex); if (g_titleLoaded) return; const auto resolvedPath = ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath); if (!std::filesystem::exists(resolvedPath)) throw std::runtime_error("param.json not found"); @@ -68,6 +71,7 @@ const bool g_dataMounted = [] { }(); void ensureIconLoaded() { + std::lock_guard lock(g_metadataMutex); if (g_iconAttempted) return; g_iconAttempted = true; const auto resolvedPath = ResolvePath_nid_no_patch(AppMetadataIconGuestPath); @@ -76,10 +80,11 @@ void ensureIconLoaded() { if (!file.is_open()) throw std::runtime_error("failed to open icon0.png"); file.seekg(0, std::ios::end); const auto fileSize = file.tellg(); + if (file.fail() || fileSize <= 0) throw std::runtime_error("icon0.png is empty or unreadable"); file.seekg(0, std::ios::beg); std::vector bytes(static_cast(fileSize)); - if (!bytes.empty()) file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); - if (bytes.empty()) throw std::runtime_error("icon0.png is empty"); + file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + if (!file || file.gcount() != static_cast(bytes.size())) throw std::runtime_error("failed to read icon0.png"); constexpr std::uint8_t PngSignature[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; if (bytes.size() < 8 || std::memcmp(bytes.data(), PngSignature, sizeof(PngSignature)) != 0) throw std::runtime_error("icon0.png is not a valid PNG file"); g_iconBytes = std::move(bytes); diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index fc45573cab4c..129c763ea73c 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -356,6 +356,16 @@ if(BUILD_TESTING) set_tests_properties(guest_kernel_is_stack PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_app_icon_tests ${LIBS_INCLUDE_DIR}/tests/GuestAppIcon.cpp) + target_include_directories(guest_app_icon_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_app_icon_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_app_icon_tests) + add_test(NAME guest_app_icon COMMAND guest_app_icon_tests) + set_tests_properties(guest_app_icon PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_app_icon PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_app_info_tests ${LIBS_INCLUDE_DIR}/tests/GuestAppInfo.cpp) target_include_directories(guest_app_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_app_info_tests PRIVATE libkernel libc) diff --git a/core/libs/tests/GuestAppIcon.cpp b/core/libs/tests/GuestAppIcon.cpp new file mode 100644 index 000000000000..0657ac60f242 --- /dev/null +++ b/core/libs/tests/GuestAppIcon.cpp @@ -0,0 +1,126 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool value) { + if (!value) std::abort(); +} + +int CheckNoIcon(const char* root) { + std::filesystem::current_path(root); + Require(!HasAppIcon_nid_postfix()); + bool threw = false; + try { + GetAppIconData_nid_postfix(); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find("icon not available") != std::string_view::npos; + } + Require(threw); + return 0; +} + +int CheckValidIcon(const char* root) { + std::filesystem::current_path(root); + Require(HasAppIcon_nid_postfix()); + const auto icon = GetAppIconData_nid_postfix(); + Require(icon.bytes != nullptr); + Require(icon.size == 16); + Require(icon.bytes[0] == 0x89 && icon.bytes[1] == 'P' && icon.bytes[2] == 'N' && icon.bytes[3] == 'G'); + return 0; +} + +int CheckEmptyIcon(const char* root) { + std::filesystem::current_path(root); + bool threw = false; + try { + HasAppIcon_nid_postfix(); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find("icon0.png is empty or unreadable") != std::string_view::npos; + } + Require(threw); + return 0; +} + +int CheckCorruptPng(const char* root) { + std::filesystem::current_path(root); + bool threw = false; + try { + HasAppIcon_nid_postfix(); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find("icon0.png is not a valid PNG file") != std::string_view::npos; + } + Require(threw); + return 0; +} + +int CheckConcurrentAccess(const char* root) { + std::filesystem::current_path(root); + std::vector threads; + for (int i = 0; i < 8; ++i) { + threads.emplace_back([] { + Require(HasAppIcon_nid_postfix()); + const auto icon = GetAppIconData_nid_postfix(); + Require(icon.size == 16); + }); + } + for (auto& t : threads) t.join(); + return 0; +} + +void RunCase(const char* self, const char* mode, std::initializer_list> files) { + const auto root = std::filesystem::temp_directory_path() / ("guest_app_icon_" + std::to_string(std::random_device{}())); + std::filesystem::remove_all(root); + std::filesystem::create_directories(root / "app0" / "sce_sys"); + for (const auto& [name, content] : files) { + std::ofstream stream(root / "app0" / "sce_sys" / name, std::ios::binary); + stream.write(content.data(), static_cast(content.size())); + } + const std::string arguments = "\"" + std::string(self) + "\" " + mode + " \"" + root.string() + "\""; +#ifdef _WIN32 + const std::string command = "\"" + arguments + "\""; +#else + const std::string command = arguments; +#endif + const int status = std::system(command.c_str()); + std::filesystem::remove_all(root); + Require(status == 0); +} + +} + +int main(int argc, char** argv) { + if (argc == 3) { + const std::string_view mode = argv[1]; + if (mode == "--no-icon") return CheckNoIcon(argv[2]); + if (mode == "--valid-icon") return CheckValidIcon(argv[2]); + if (mode == "--empty-icon") return CheckEmptyIcon(argv[2]); + if (mode == "--corrupt-png") return CheckCorruptPng(argv[2]); + if (mode == "--concurrent") return CheckConcurrentAccess(argv[2]); + return 1; + } + + const std::string validPng("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00", 16); + const std::string corruptPng("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", 16); + const std::string emptyPng(""); + + RunCase(argv[0], "--no-icon", {}); + RunCase(argv[0], "--valid-icon", {{"icon0.png", validPng}}); + RunCase(argv[0], "--empty-icon", {{"icon0.png", emptyPng}}); + RunCase(argv[0], "--corrupt-png", {{"icon0.png", corruptPng}}); + RunCase(argv[0], "--concurrent", {{"icon0.png", validPng}}); + + return 0; +}