diff --git a/rpcs3/Emu/Cell/Modules/cellGame.cpp b/rpcs3/Emu/Cell/Modules/cellGame.cpp index b67d23a486..8b5ffb8765 100644 --- a/rpcs3/Emu/Cell/Modules/cellGame.cpp +++ b/rpcs3/Emu/Cell/Modules/cellGame.cpp @@ -466,7 +466,7 @@ error_code cellHddGameCheck(ppu_thread& ppu, u32 version, vm::cptr dirName // psf::assign(sfo, "CATEGORY", psf::string(3, "HG")); // } - // psf::assign(sfo, "TITLE_ID", psf::string(CELL_GAME_SYSP_TITLEID_SIZE, setParam->titleId)); + // psf::assign(sfo, "TITLE_ID", psf::string(TITLEID_SFO_ENTRY_SIZE, setParam->titleId)); // psf::assign(sfo, "TITLE", psf::string(CELL_GAME_SYSP_TITLE_SIZE, setParam->title)); // psf::assign(sfo, "VERSION", psf::string(CELL_GAME_SYSP_VERSION_SIZE, setParam->dataVersion)); // psf::assign(sfo, "PARENTAL_LEVEL", +setParam->parentalLevel); @@ -990,7 +990,7 @@ error_code cellGameDataCheckCreate2(ppu_thread& ppu, u32 version, vm::cptr psf::assign(sfo, "CATEGORY", psf::string(3, "GD")); } - psf::assign(sfo, "TITLE_ID", psf::string(CELL_GAME_SYSP_TITLEID_SIZE, setParam->titleId)); + psf::assign(sfo, "TITLE_ID", psf::string(TITLEID_SFO_ENTRY_SIZE, setParam->titleId, true)); psf::assign(sfo, "TITLE", psf::string(CELL_GAME_SYSP_TITLE_SIZE, setParam->title)); psf::assign(sfo, "VERSION", psf::string(CELL_GAME_SYSP_VERSION_SIZE, setParam->dataVersion)); psf::assign(sfo, "PARENTAL_LEVEL", +setParam->parentalLevel); @@ -1005,6 +1005,14 @@ error_code cellGameDataCheckCreate2(ppu_thread& ppu, u32 version, vm::cptr psf::assign(sfo, fmt::format("TITLE_%02d", i), psf::string(CELL_GAME_SYSP_TITLE_SIZE, setParam->titleLang[i])); } + if (!psf::check_registry(sfo)) + { + // This results in CELL_OK, broken SFO and CELL_GAMEDATA_ERROR_BROKEN on the next load + // Avoid creation for now + cellGame.error("Broken SFO paramters: %s", sfo); + return CELL_OK; + } + fs::pending_file temp(vfs::get(dir + "/PARAM.SFO")); temp.file.write(psf::save_object(sfo)); ensure(temp.commit()); @@ -1125,7 +1133,7 @@ error_code cellGameCreateGameData(vm::ptr init, vm::ptrtitleId) }, + { "TITLE_ID", psf::string(TITLEID_SFO_ENTRY_SIZE, init->titleId) }, { "TITLE", psf::string(CELL_GAME_SYSP_TITLE_SIZE, init->title) }, { "VERSION", psf::string(CELL_GAME_SYSP_VERSION_SIZE, init->version) }, }; diff --git a/rpcs3/Emu/Cell/Modules/cellGame.h b/rpcs3/Emu/Cell/Modules/cellGame.h index 5be2085c58..a9bbc0666d 100644 --- a/rpcs3/Emu/Cell/Modules/cellGame.h +++ b/rpcs3/Emu/Cell/Modules/cellGame.h @@ -216,6 +216,11 @@ enum // old consts CELL_DISCGAME_SYSP_TITLEID_SIZE=10, }; +enum +{ + TITLEID_SFO_ENTRY_SIZE = 16, // This is the true length on PS3 (TODO: Fix in more places) +}; + struct CellGameDataSystemFileParam { char title[CELL_GAMEDATA_SYSP_TITLE_SIZE]; diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 2fa9d70844..c8b8f2fee8 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -1036,6 +1036,14 @@ game_boot_result Emulator::Load(const std::string& title_id, bool add_only, bool // Try to boot a game through game ID only m_title_id = m_path.substr(("%RPCS3_GAMEID%:"sv).size()); m_title_id = m_title_id.substr(0, m_title_id.find_first_of(fs::delim)); + + if (m_title_id.size() < 3 && m_title_id.find_first_not_of('.') == umax) + { + // Do not allow if TITLE_ID result in path redirection + sys_log.fatal("Game directory not found using GAMEID token. ('%s')", m_title_id); + return game_boot_result::invalid_file_or_folder; + } + std::string tail = m_path.substr(("%RPCS3_GAMEID%:"sv).size() + m_title_id.size()); if (tail.find_first_not_of(fs::delim) == umax) diff --git a/rpcs3/Loader/PSF.cpp b/rpcs3/Loader/PSF.cpp index e3c3f71f92..60751cf478 100644 --- a/rpcs3/Loader/PSF.cpp +++ b/rpcs3/Loader/PSF.cpp @@ -100,16 +100,21 @@ namespace psf }; - entry::entry(format type, u32 max_size, std::string_view value) + entry::entry(format type, u32 max_size, std::string_view value, bool allow_truncate) noexcept : m_type(type) , m_max_size(max_size) , m_value_string(value) { ensure(type == format::string || type == format::array); - ensure(max_size); + ensure(max_size > (type == format::string ? 1 : 0)); + + if (allow_truncate && value.size() > max(false)) + { + m_value_string.resize(max(false)); + } } - entry::entry(u32 value) + entry::entry(u32 value) noexcept : m_type(format::integer) , m_max_size(sizeof(u32)) , m_value_integer(value) @@ -148,7 +153,7 @@ namespace psf { case format::string: case format::array: - return std::min(m_max_size, ::narrow(m_value_string.size() + (m_type == format::string))); + return std::min(m_max_size, ::narrow(m_value_string.size() + (m_type == format::string ? 1 : 0))); case format::integer: return sizeof(u32); @@ -157,6 +162,22 @@ namespace psf fmt::throw_exception("Invalid format (0x%x)", m_type); } + bool entry::is_valid() const + { + switch (m_type) + { + case format::string: + case format::array: + return m_value_string.size() <= this->max(false); + + case format::integer: + return true; + default: break; + } + + fmt::throw_exception("Invalid format (0x%x)", m_type); + } + load_result_t load(const fs::file& stream, std::string_view filename) { #define PSF_CHECK(cond, err) if (!static_cast(cond)) { if (error::err != error::stream) psf_log.error("Error loading PSF '%s': %s%s", filename, error::err, \ @@ -251,14 +272,6 @@ namespace psf PSF_CHECK(false, corrupt); } - const auto tid = get_string(pair.sfo, "TITLE_ID", ""); - - if (std::find_if(tid.begin(), tid.end(), [](char ch){ return !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')); }) != tid.end()) - { - psf_log.error("Invalid title ID ('%s')", tid); - PSF_CHECK(false, corrupt); - } - #undef PSF_CHECK return pair; } @@ -283,7 +296,7 @@ namespace psf index.key_off = ::narrow(key_offset); index.param_fmt = entry.second.type(); index.param_len = entry.second.size(); - index.param_max = entry.second.max(); + index.param_max = entry.second.max(true); index.data_off = ::narrow(data_offset); // Update offsets: @@ -322,7 +335,7 @@ namespace psf for (const auto& entry : psf) { const auto fmt = entry.second.type(); - const u32 max = entry.second.max(); + const u32 max = entry.second.max(true); if (fmt == format::integer && max == sizeof(u32)) { @@ -331,17 +344,17 @@ namespace psf } else if (fmt == format::string || fmt == format::array) { - const std::string& value = entry.second.as_string(); - const usz size = std::min(max, value.size()); + std::string_view value = entry.second.as_string(); - if (value.size() + (fmt == format::string) > max) + if (!entry.second.is_valid()) { // TODO: check real limitations of PSF format - psf_log.error("Entry value shrinkage (key='%s', value='%s', size=0x%zx, max=0x%x)", entry.first, value, size, max); + psf_log.error("Entry value shrinkage (key='%s', value='%s', size=0x%zx, max=0x%x)", entry.first, value, value.size(), max); + value = value.substr(0, entry.second.max(false)); } - stream.write(value); - stream.trunc(stream.seek(max - size, fs::seek_cur)); // Skip up to max_size + stream.write(value.data(), value.size()); + stream.trunc(stream.seek(max - value.size(), fs::seek_cur)); // Skip up to max_size } else { @@ -375,4 +388,44 @@ namespace psf return found->second.as_integer(); } + + bool check_registry(const registry& psf, std::function validate, u32 line, u32 col, const char* file, const char* func) + { + bool psf_ok = true; + + for (const auto& [key, value] : psf) + { + bool entry_ok = value.is_valid(); + + if (validate) + { + // Validate against a custom condition as well (forward error) + if (!validate(entry_ok, key, value)) + { + entry_ok = false; + } + } + + if (!entry_ok) + { + if (value.type() == format::string) + { + psf_log.error("Entry '%s' is invalid: string='%s'.%s", key, value.as_string(), src_loc{line , col, file, func}); + } + else + { + // TODO: Better logging of other types + psf_log.error("Entry %s is invalid.%s", key, value.as_string(), src_loc{line , col, file, func}); + } + } + + if (!entry_ok) + { + // Do not break, run over all entries in order to report all errors + psf_ok = false; + } + } + + return psf_ok; + } } diff --git a/rpcs3/Loader/PSF.h b/rpcs3/Loader/PSF.h index c21ba415a0..eb5c79b1ea 100644 --- a/rpcs3/Loader/PSF.h +++ b/rpcs3/Loader/PSF.h @@ -55,10 +55,10 @@ namespace psf public: // Construct string entry, assign the value - entry(format type, u32 max_size, std::string_view value); + entry(format type, u32 max_size, std::string_view value, bool allow_truncate = false) noexcept; // Construct integer entry, assign the value - entry(u32 value); + entry(u32 value) noexcept; ~entry() = default; @@ -69,8 +69,9 @@ namespace psf entry& operator =(u32 value); format type() const { return m_type; } - u32 max() const { return m_max_size; } + u32 max(bool with_nts) const { return m_max_size - (!with_nts && m_type == format::string ? 1 : 0); } u32 size() const; + bool is_valid() const; }; // Define PSF registry as a sorted map of entries: @@ -102,6 +103,12 @@ namespace psf // Get integer value or default value u32 get_integer(const registry& psf, std::string_view key, u32 def = 0); + bool check_registry(const registry& psf, std::function validate = {}, + u32 line = __builtin_LINE(), + u32 col = __builtin_COLUMN(), + const char* file = __builtin_FILE(), + const char* func = __builtin_FUNCTION()); + // Assign new entry inline void assign(registry& psf, std::string_view key, entry&& _entry) { @@ -118,9 +125,18 @@ namespace psf } // Make string entry - inline entry string(u32 max_size, std::string_view value) + inline entry string(u32 max_size, std::string_view value, bool allow_truncate = false) { - return {format::string, max_size, value}; + return {format::string, max_size, value, allow_truncate}; + } + + // Make string entry (from char[N]) + template + inline entry string(u32 max_size, char (&value_array)[CharN], bool allow_truncate = false) + { + std::string_view value{value_array, CharN}; + value = value.substr(0, std::min(value.find_first_of('\0'), value.size())); + return string(CharN, value, allow_truncate); } // Make array entry