#include "prx/libkernel/AppMetadata/include/ParamSfoParser.hpp" #include #include #include #include #include #include #include #include #include namespace { struct Entry { std::string key; std::uint16_t format; std::vector data; }; void Require(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); } Entry Text(const std::string& key, const std::string& value) { std::vector data(value.begin(), value.end()); data.push_back(0); return {key, 0x0204, data}; } Entry Integer(const std::string& key, std::int32_t value) { std::vector data(4); std::memcpy(data.data(), &value, 4); return {key, 0x0404, data}; } void Put32(std::vector& bytes, std::size_t offset, std::uint32_t value) { std::memcpy(bytes.data() + offset, &value, 4); } std::vector Build(const std::vector& entries) { std::vector keys; std::vector data; std::vector bytes(20 + entries.size() * 16); for (std::size_t index = 0; index < entries.size(); ++index) { const auto entry = 20 + index * 16; const auto keyOffset = static_cast(keys.size()); std::memcpy(bytes.data() + entry, &keyOffset, 2); std::memcpy(bytes.data() + entry + 2, &entries[index].format, 2); Put32(bytes, entry + 4, static_cast(entries[index].data.size())); Put32(bytes, entry + 8, static_cast(entries[index].data.size())); Put32(bytes, entry + 12, static_cast(data.size())); keys.insert(keys.end(), entries[index].key.begin(), entries[index].key.end()); keys.push_back(0); data.insert(data.end(), entries[index].data.begin(), entries[index].data.end()); } while (keys.size() % 4 != 0) keys.push_back(0); std::memcpy(bytes.data(), "\0PSF", 4); Put32(bytes, 4, 0x0101); Put32(bytes, 8, static_cast(bytes.size())); Put32(bytes, 12, static_cast(bytes.size() + keys.size())); Put32(bytes, 16, static_cast(entries.size())); bytes.insert(bytes.end(), keys.begin(), keys.end()); bytes.insert(bytes.end(), data.begin(), data.end()); return bytes; } ParsedParamJson Parse(const std::filesystem::path& path, const std::vector& bytes) { { std::ofstream file(path, std::ios::binary); file.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); Require(static_cast(file), "Cannot write param.sfo fixture"); } return parseParamSfo(path); } void Rejects(const std::filesystem::path& path, const std::vector& bytes, const std::string& expected) { try { Parse(path, bytes); } catch (const std::exception& error) { Require(std::string(error.what()).find(expected) != std::string::npos, "Unexpected error for " + expected + ": " + error.what()); return; } throw std::runtime_error("Accepted invalid param.sfo: " + expected); } std::vector Valid() { return {Text("CATEGORY", "gd"), Integer("DOWNLOAD_DATA_SIZE", 64), Text("TITLE", "Example"), Text("TITLE_ID", "CUSA00000"), Integer("USER_DEFINED_PARAM_1", 23566), Integer("USER_DEFINED_PARAM_3", -7), {"PUBTOOLINFO", 0x0004, {1, 2, 3}}}; } } int main() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-param-sfo-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".sfo"); int result = 0; try { const auto parsed = Parse(path, Build(Valid())); Require(parsed.title == "Example" && parsed.titleId == "CUSA00000", "Incorrect title metadata"); Require(parsed.downloadDataSizeMiB == 64, "Incorrect download data size"); Require(parsed.userDefinedParams[0] == 23566 && parsed.userDefinedParams[1] == 0 && parsed.userDefinedParams[2] == -7 && parsed.userDefinedParams[3] == 0, "Incorrect user defined params"); auto entries = Valid(); entries.erase(entries.begin() + 2); Rejects(path, Build(entries), "missing TITLE"); entries = Valid(); entries.erase(entries.begin() + 3); Rejects(path, Build(entries), "missing TITLE_ID"); entries = Valid(); entries[1] = Integer("DOWNLOAD_DATA_SIZE", -1); Rejects(path, Build(entries), "DOWNLOAD_DATA_SIZE is negative"); entries = Valid(); entries[2].data.back() = 'x'; Rejects(path, Build(entries), "TITLE is not a terminated string"); entries = Valid(); entries[4].data.pop_back(); Rejects(path, Build(entries), "USER_DEFINED_PARAM_1 is not a 32-bit integer"); entries = Valid(); entries.push_back(Text("TITLE", "Again")); Rejects(path, Build(entries), "repeats TITLE"); auto bytes = Build(Valid()); bytes[1] = 'X'; Rejects(path, bytes, "no PSF header"); bytes = Build(Valid()); Put32(bytes, 16, 1000); Rejects(path, bytes, "tables are out of bounds"); bytes = Build(Valid()); Put32(bytes, 20 + 2 * 16 + 12, 0x10000); Rejects(path, bytes, "entry is out of bounds"); bytes = Build(Valid()); Put32(bytes, 20 + 4, 0x10000); Rejects(path, bytes, "entry is out of bounds"); bytes = Build(Valid()); bytes.resize(bytes.size() - 64); Rejects(path, bytes, "out of bounds"); Rejects(path, std::vector(8), "no PSF header"); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; result = 1; } std::filesystem::remove(path); if (result == 0) std::cout << "param.sfo parser tests passed\n"; return result; }