From 286f9dddec344ece0236f7a65b52ce631d1be2b6 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sun, 12 Oct 2025 12:38:37 +1000 Subject: [PATCH] Qt: Validate cheats before saving edits --- src/core/cheats.cpp | 66 ++++++++++++------- src/core/cheats.h | 5 ++ .../gamecheatsettingswidget.cpp | 22 ++++++- 3 files changed, 66 insertions(+), 27 deletions(-) diff --git a/src/core/cheats.cpp b/src/core/cheats.cpp index 6e07c39e9..0ed3fdb65 100644 --- a/src/core/cheats.cpp +++ b/src/core/cheats.cpp @@ -231,7 +231,8 @@ static void EnumerateChtFiles(const std::string_view serial, std::optional ParseOption(const std::string_view value); static bool ParseOptionRange(const std::string_view value, u16* out_range_start, u16* out_range_end); -extern void ParseFile(CheatCodeList* dst_list, const std::string_view file_contents); +static void ParseFile(CheatCodeList* dst_list, const std::string_view file_contents); +static std::unique_ptr ParseCode(CheatCode::Metadata metadata, const std::string_view data, Error* error); static Cheats::FileFormat DetectFileFormat(const std::string_view file_contents); static bool ImportPCSXFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, Error* error); @@ -240,9 +241,6 @@ static bool ImportLibretroFile(CodeInfoList* dst, const std::string_view file_co static bool ImportEPSXeFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, Error* error); static bool ImportOldChtFile(const std::string_view serial); -static std::unique_ptr ParseGamesharkCode(CheatCode::Metadata metadata, const std::string_view data, - Error* error); - const char* PATCHES_CONFIG_SECTION = "Patches"; const char* CHEATS_CONFIG_SECTION = "Cheats"; const char* PATCH_ENABLE_CONFIG_KEY = "Enable"; @@ -792,6 +790,24 @@ void Cheats::RemoveAllCodes(const std::string_view serial, const std::string_vie } } +bool Cheats::ValidateCodeBody(std::string_view name, CodeType type, CodeActivation activation, std::string_view body, + Error* error) +{ + // don't need the full metadata, only enough to get through + CheatCode::Metadata metadata = {}; + metadata.name = name; + metadata.type = type; + metadata.activation = activation; + + std::unique_ptr code = ParseCode(std::move(metadata), body, error); + return static_cast(code); +} + +bool Cheats::ValidateCodeBody(const CodeInfo& code, Error* error) +{ + return ValidateCodeBody(code.name, code.type, code.activation, code.body, error); +} + std::string Cheats::GetChtFilename(const std::string_view serial, std::optional hash, bool cheats) { return Path::Combine(cheats ? EmuFolders::Cheats : EmuFolders::Patches, GetChtTemplate(serial, hash, false)); @@ -1457,28 +1473,18 @@ void Cheats::ParseFile(CheatCodeList* dst_list, const std::string_view file_cont const std::string_view code_body = file_contents.substr(code_body_start.value(), reader.GetCurrentLineOffset() - code_body_start.value()); - std::unique_ptr code; - if (next_code_metadata.type == CodeType::Gameshark) - { - Error error; - code = ParseGamesharkCode(std::move(next_code_metadata), code_body, &error); - if (!code) - { - WARNING_LOG("Failed to parse gameshark code ending on line {}: {}", reader.GetCurrentLineNumber(), - error.GetDescription()); - return; - } - } - else + Error error; + std::unique_ptr code = ParseCode(std::move(next_code_metadata), code_body, &error); + if (!code) { - WARNING_LOG("Unknown code type ending at line {}", reader.GetCurrentLineNumber()); - return; + WARNING_LOG("Failed to parse gameshark code ending on line {}: {}", reader.GetCurrentLineNumber(), + error.GetDescription()); } next_code_group = {}; next_code_metadata = {}; code_body_start.reset(); - if (std::exchange(next_code_ignored, false)) + if (std::exchange(next_code_ignored, false) || !code) return; // overwrite existing codes with the same name. @@ -4446,14 +4452,26 @@ void Cheats::GamesharkCheatCode::SetOptionValue(u32 value) { Instruction& inst = instructions[index]; const u32 value_mask = ((1u << bit_count) - 1); - ; const u32 fixed_mask = ~(value_mask << bitpos_start); inst.second = (inst.second & fixed_mask) | ((value & value_mask) << bitpos_start); } } -std::unique_ptr Cheats::ParseGamesharkCode(CheatCode::Metadata metadata, const std::string_view data, - Error* error) +std::unique_ptr Cheats::ParseCode(CheatCode::Metadata metadata, const std::string_view data, + Error* error) { - return GamesharkCheatCode::Parse(std::move(metadata), data, error); + std::unique_ptr ret; + + switch (metadata.type) + { + case CodeType::Gameshark: + ret = GamesharkCheatCode::Parse(std::move(metadata), data, error); + break; + + default: + Error::SetStringView(error, "Unknown code type"); + break; + } + + return ret; } diff --git a/src/core/cheats.h b/src/core/cheats.h index fdf568cbf..fbb234872 100644 --- a/src/core/cheats.h +++ b/src/core/cheats.h @@ -124,6 +124,11 @@ extern bool SaveCodesToFile(const char* path, const CodeInfoList& codes, Error* /// Removes any .cht files for the specified game. extern void RemoveAllCodes(const std::string_view serial, const std::string_view title, std::optional hash); +/// Validates whether a cheat code is properly formatted. +extern bool ValidateCodeBody(std::string_view name, CodeType type, CodeActivation activation, std::string_view body, + Error* error); +extern bool ValidateCodeBody(const CodeInfo& code, Error* error); + /// Returns the path to a new cheat/patch cht for the specified serial and hash. extern std::string GetChtFilename(const std::string_view serial, std::optional hash, bool cheats); diff --git a/src/duckstation-qt/gamecheatsettingswidget.cpp b/src/duckstation-qt/gamecheatsettingswidget.cpp index e70dff037..e49368b3c 100644 --- a/src/duckstation-qt/gamecheatsettingswidget.cpp +++ b/src/duckstation-qt/gamecheatsettingswidget.cpp @@ -864,6 +864,23 @@ void CheatCodeEditorDialog::saveClicked() return; } + const Cheats::CodeType new_type = static_cast(m_ui.type->currentIndex()); + const Cheats::CodeActivation new_activation = static_cast(m_ui.activation->currentIndex()); + + // Validate it before trying to save it. + Error error; + if (!Cheats::ValidateCodeBody(new_name, new_type, new_activation, new_body, &error)) + { + if (QMessageBox::question(QtUtils::GetRootWidget(this), tr("Error"), + tr("The entered cheat code is not valid:\n\n%1\n\nTrying to use this cheat will not work " + "as expected. Do you want to continue?") + .arg(QString::fromStdString(error.GetDescription())), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) + { + return; + } + } + // name actually includes the prefix if (const int index = m_ui.group->currentIndex(); index != 0) { @@ -894,8 +911,8 @@ void CheatCodeEditorDialog::saveClicked() .replace(QChar('\n'), QChar(' ')) .trimmed() .toStdString(); - m_code->type = static_cast(m_ui.type->currentIndex()); - m_code->activation = static_cast(m_ui.activation->currentIndex()); + m_code->type = new_type; + m_code->activation = new_activation; m_code->body = std::move(new_body); m_code->option_range_start = 0; @@ -914,7 +931,6 @@ void CheatCodeEditorDialog::saveClicked() } std::string path = m_parent->getPathForSavingCheats(); - Error error; if (!Cheats::UpdateCodeInFile(path.c_str(), old_name, m_code, &error)) { QMessageBox::critical(this, tr("Error"),