diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index 3645c476d..4f515a105 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -95,6 +95,27 @@ std::uint32_t parseHex4(const std::string& text, std::size_t& position) { return value; } +void appendRawUtf8(const std::string& text, std::size_t& position, unsigned char first, std::string& result) { + unsigned count; + std::uint32_t codepoint; + std::uint32_t minimum; + if (first >= 0xC2 && first <= 0xDF) { count = 1; codepoint = first & 0x1F; minimum = 0x80; } + else if (first >= 0xE0 && first <= 0xEF) { count = 2; codepoint = first & 0x0F; minimum = 0x800; } + else if (first >= 0xF0 && first <= 0xF4) { count = 3; codepoint = first & 0x07; minimum = 0x10000; } + else throw std::runtime_error("invalid UTF-8 in param.json"); + if (count > text.size() - position) throw std::runtime_error("truncated UTF-8 in param.json"); + for (unsigned index = 0; index < count; ++index) { + const auto byte = static_cast(text[position + index]); + if ((byte & 0xC0) != 0x80) throw std::runtime_error("invalid UTF-8 continuation in param.json"); + codepoint = (codepoint << 6) | (byte & 0x3F); + } + if (codepoint < minimum || codepoint > 0x10FFFF || (codepoint >= 0xD800 && codepoint <= 0xDFFF)) + throw std::runtime_error("invalid UTF-8 codepoint in param.json"); + result.push_back(static_cast(first)); + result.append(text, position, count); + position += count; +} + std::string parseJsonString(const std::string& text, std::size_t& position) { expectChar(text, position, '"'); std::string result; @@ -103,7 +124,11 @@ std::string parseJsonString(const std::string& text, std::size_t& position) { const char c = text[position++]; if (c == '"') break; if (static_cast(c) < 0x20) throw std::runtime_error("control character in param.json string"); - if (c != '\\') { result.push_back(c); continue; } + if (c != '\\') { + if (static_cast(c) < 0x80) result.push_back(c); + else appendRawUtf8(text, position, static_cast(c), result); + continue; + } if (position >= text.size()) throw std::runtime_error("unterminated escape in param.json"); const char escape = text[position++]; switch (escape) { diff --git a/core/libs/tests/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index 653fb6745..67d3e54ab 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -90,11 +90,34 @@ static void CheckNumberSyntax(const std::filesystem::path& path) { } } +static void CheckUtf8(const std::filesystem::path& path) { + const std::string prefix = R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":")"; + const std::string suffix = R"("}}})"; + for (const auto& title : {std::string("ASCII"), std::string("\xC2\x80"), std::string("\xE3\x82\xB2\xE3\x83\xBC\xE3\x83\xA0"), + std::string("\xF0\x9F\x8E\xAE"), std::string("\xF4\x8F\xBF\xBF")}) { + Require(ParseText(path, prefix + title + suffix).title == title, "Valid UTF-8 title changed"); + } + Require(ParseText(path, prefix + R"(\uD83C\uDFAE)" + suffix).title == "\xF0\x9F\x8E\xAE", "Escaped title changed"); + for (const auto& title : {std::string("\x80"), std::string("\xC0\xAF"), std::string("\xC1\xBF"), std::string("\xC2"), + std::string("\xE0\x80\xAF"), std::string("\xED\xA0\x80"), std::string("\xE3\x82"), + std::string("\xF0\x80\x80\xAF"), std::string("\xF4\x90\x80\x80"), std::string("\xF5\x80\x80\x80"), + std::string("\xFE"), std::string("\xFF"), std::string("\xC2x"), std::string("\xE3\x82x")}) { + bool rejected = false; + try { ParseText(path, prefix + title + suffix); } catch (const std::exception&) { rejected = true; } + Require(rejected, "Accepted malformed UTF-8 title"); + } + bool rejected = false; + try { ParseText(path, std::string("{\"") + "\x80" + R"(":0,)" + prefix.substr(1) + "Valid" + suffix); } + catch (const std::exception&) { rejected = true; } + Require(rejected, "Accepted malformed UTF-8 object key"); +} + int main() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-param-json-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".json"); int result = 0; try { + CheckUtf8(path); CheckNumberSyntax(path); const std::pair sizes[] = { {"", 0}, {"0", 0}, {"-0", 0}, {"0.25", 0}, {"0e3", 0}, {"0E+3", 0}, {"-0e-3", 0}, {"4096", 4096}, diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 83a29bfbb..7886acc40 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -202,6 +202,13 @@ if(BUILD_TESTING) target_include_directories(buffer_bounds_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/io/include) add_test(NAME buffer_bounds COMMAND buffer_bounds_tests) + add_test_executable(param_json_utf8_tests + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/tests/ParamJsonParser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp) + target_include_directories(param_json_utf8_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../libs) + add_test(NAME param_json_utf8 COMMAND param_json_utf8_tests) + set_tests_properties(param_json_utf8 PROPERTIES TIMEOUT 20) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") add_test_executable(file_writer_tests ${CMAKE_CURRENT_SOURCE_DIR}/io/tests/FileWriterTests.cpp