mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-21 18:29:05 +02:00
- Fixes #14955 Cheats copied from websites can carry invisible Unicode characters, like a pop directional formatting mark after each address. That broke the hex parse of every line, and a cheat with no valid lines didn't show up at all. The parser now drops such characters (a new SanitizeString mode), and the cheat screen shows the first few parse errors instead of silently leaving cheats out. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
261 lines
8.7 KiB
C++
261 lines
8.7 KiB
C++
// Copyright (c) 2012- PPSSPP Project.
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, version 2.0 or later versions.
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License 2.0 for more details.
|
|
|
|
// A copy of the GPL 2.0 should have been included with the program.
|
|
// If not, see http://www.gnu.org/licenses/
|
|
|
|
// Official git repository and contact information can be found at
|
|
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
|
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "Common/File/FileUtil.h"
|
|
#include "Common/File/Path.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Core/CwCheat.h"
|
|
#include "Core/MemMap.h"
|
|
#include "Core/MemMapHelpers.h"
|
|
#include "Core/HLE/sceCtrl.h"
|
|
#include "Core/MIPS/MIPS.h"
|
|
#include "UnitTest.h"
|
|
|
|
// Most of these are the PSPAR tests from TempAR's docs/tests/tests.db, with their expected results.
|
|
static const char *const g_cheatFile = R"(_S TEST00000
|
|
_G Cheat tests
|
|
_C1 Loop
|
|
_M 0xD5000000 0x11223344
|
|
_M 0xC0000000 0x00000002
|
|
_M 0xD6000000 0x08802000
|
|
_M 0xD4000000 0x44444444
|
|
_M 0xD2000000 0x00000000
|
|
_C1 Loop without terminator
|
|
_M 0xD5000000 0x11223344
|
|
_M 0xC0000000 0x00000002
|
|
_M 0xD6000000 0x08802000
|
|
_M 0xD4000000 0x44444444
|
|
_C1 Patch
|
|
_M 0xE8802000 0x0000000C
|
|
_M 0x11223344 0x55667788
|
|
_M 0x99AABBCC 0x00000000
|
|
_C1 Patch with extra lines
|
|
_M 0xE8802000 0x0000000C # Expected Result:
|
|
_M 0x11223344 0x55667788 # 0x08802000 = 0x11223344; 0x08802004 = 0x55667788
|
|
_M 0x99AABBCC 0x00000000 # 0x08802008 = 0x99AABBCC; 0x0880200C = 0xDDEEFF00
|
|
_M 0x0880200C 0xDDEEFF00 #
|
|
_C1 Memory copy
|
|
_M 0xD3000000 0x08802000
|
|
_M 0x00000000 0x11223344
|
|
_M 0xF8802008 0x00000004
|
|
_C1 Write offset
|
|
_M 0x08802000 0x08802004
|
|
_M 0x08802004 0x08802020
|
|
_M 0x68802000 0x00000000
|
|
_M 0xB8802000 0x00000000
|
|
_M 0xB0000000 0x00000000
|
|
_M 0xC6000000 0x08802008
|
|
_M 0xD2000000 0x00000000
|
|
_C1 Multi-level pointer
|
|
_M 0x08802000 0x08802004
|
|
_M 0x08802004 0x08802020
|
|
_M 0x68802000 0x00000000
|
|
_M 0xB8802000 0x00000000
|
|
_M 0xB0000000 0x00000000
|
|
_M 0x00000000 0x12345678
|
|
_M 0xD2000000 0x00000000
|
|
_C1 Data register add
|
|
_M 0x08802000 0x11111111
|
|
_M 0xD9000000 0x08802000
|
|
_M 0xD4000000 0x11111111
|
|
_M 0xD6000000 0x08802004
|
|
_M 0xD2000000 0x00000000
|
|
_C1 Data register sub
|
|
_M 0x08802000 0x22222222
|
|
_M 0xD9000000 0x08802000
|
|
_M 0xD4000000 0xEEEEEEEF
|
|
_M 0xD6000000 0x08802004
|
|
_M 0xD2000000 0x00000000
|
|
_C1 Safe data store
|
|
_M 0xC4000000 0x00000000
|
|
_M 0x00000020 0x08802000
|
|
_M 0x00000024 0x12345678
|
|
_M 0xD2000000 0x00000000
|
|
_M 0x00000000 0x00000000
|
|
_C1 Conditions and terminator
|
|
_M 0x08802000 0x00001234
|
|
_M 0x58802000 0x00001234
|
|
_M 0x08802004 0x11111111
|
|
_M 0x58802000 0x00005678
|
|
_M 0x08802008 0x22222222
|
|
_M 0xD0000000 0x00000000
|
|
_M 0x0880200C 0x33333333
|
|
_M 0xD0000000 0x00000000
|
|
_M 0x88802000 0xFF0F0005
|
|
_M 0x08802010 0x44444444
|
|
_M 0xD2000000 0x00000000
|
|
_M 0x28802014 0x00000055
|
|
_C1 Pad (PSPAR address)
|
|
_M 0x99F00000 0x00000200
|
|
_M 0x08802000 0x11111111
|
|
_M 0xD0000000 0x00000000
|
|
_M 0xA9F00000 0x00000200
|
|
_M 0x08802004 0x22222222
|
|
_C1 Pad (TempAR address)
|
|
_M 0x9A000000 0xFDFF0200
|
|
_M 0x08802000 0x33333333
|
|
_C1 CwCheat with a comment
|
|
_L 0x20002000 0x11111111 # Comment
|
|
)";
|
|
|
|
struct ExpectedWord {
|
|
u32 addr;
|
|
u32 value;
|
|
};
|
|
|
|
static bool RunCheatTest(CWCheatEngine &engine, const CheatCode &cheat, const std::vector<ExpectedWord> &expected) {
|
|
Memory::Memset(0x08801F00, 0, 0x400, "CheatTest");
|
|
engine.SetCheats({ cheat });
|
|
engine.Run();
|
|
for (const ExpectedWord &e : expected) {
|
|
u32 actual = Memory::ReadUnchecked_U32(e.addr);
|
|
if (actual != e.value) {
|
|
printf("Cheat '%s': %08x = %08x, expected %08x\n", cheat.name.c_str(), e.addr, actual, e.value);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool RunCheatTests() {
|
|
const Path filename = UnitTestTempPath("unittest_cheats.ini");
|
|
if (!File::WriteStringToFile(true, g_cheatFile, filename)) {
|
|
printf("Can't write %s\n", filename.c_str());
|
|
return false;
|
|
}
|
|
bool parsed;
|
|
std::vector<CheatCode> cheats;
|
|
{
|
|
// The parser holds the file open, which keeps it from being deleted on Windows.
|
|
CheatFileParser parser(filename, "TEST00000");
|
|
parsed = parser.Parse();
|
|
for (const std::string &error : parser.GetErrors()) {
|
|
printf("%s\n", error.c_str());
|
|
}
|
|
cheats = parser.GetCheats();
|
|
}
|
|
File::Delete(filename);
|
|
EXPECT_TRUE(parsed);
|
|
EXPECT_EQ_INT((int)cheats.size(), 14);
|
|
|
|
const std::vector<std::vector<ExpectedWord>> expected = {
|
|
{ { 0x08802000, 0x11223344 }, { 0x08802004, 0x55667788 }, { 0x08802008, 0x99AABBCC } },
|
|
{ { 0x08802000, 0x11223344 }, { 0x08802004, 0x55667788 }, { 0x08802008, 0x99AABBCC } },
|
|
{ { 0x08802000, 0x11223344 }, { 0x08802004, 0x55667788 }, { 0x08802008, 0x99AABBCC }, { 0x0880200C, 0 } },
|
|
{ { 0x08802000, 0x11223344 }, { 0x08802004, 0x55667788 }, { 0x08802008, 0x99AABBCC }, { 0x0880200C, 0xDDEEFF00 } },
|
|
{ { 0x08802000, 0x11223344 }, { 0x08802008, 0x11223344 } },
|
|
{ { 0x08802000, 0x08802004 }, { 0x08802004, 0x08802020 }, { 0x08802008, 0x08802020 } },
|
|
{ { 0x08802000, 0x08802004 }, { 0x08802004, 0x08802020 }, { 0x08802020, 0x12345678 } },
|
|
{ { 0x08802004, 0x22222222 } },
|
|
{ { 0x08802004, 0x11111111 } },
|
|
{ { 0x08802000, 0x12345678 } },
|
|
{ { 0x08802004, 0x11111111 }, { 0x08802008, 0 }, { 0x0880200C, 0x33333333 }, { 0x08802010, 0x44444444 }, { 0x08802014, 0x55 } },
|
|
};
|
|
|
|
CWCheatEngine engine("TEST00000");
|
|
bool success = cheats.size() == 14;
|
|
for (size_t i = 0; success && i < expected.size(); i++) {
|
|
success = RunCheatTest(engine, cheats[i], expected[i]);
|
|
}
|
|
|
|
if (success) {
|
|
__CtrlUpdateButtons(0, 0xFFFFFFFF);
|
|
success = RunCheatTest(engine, cheats[11], { { 0x08802000, 0 }, { 0x08802004, 0x22222222 } });
|
|
success = success && RunCheatTest(engine, cheats[12], { { 0x08802000, 0 } });
|
|
__CtrlUpdateButtons(0x00000200 | 0x00004000, 0);
|
|
success = success && RunCheatTest(engine, cheats[11], { { 0x08802000, 0x11111111 }, { 0x08802004, 0 } });
|
|
success = success && RunCheatTest(engine, cheats[12], { { 0x08802000, 0x33333333 } });
|
|
__CtrlUpdateButtons(0, 0xFFFFFFFF);
|
|
success = success && RunCheatTest(engine, cheats[13], { { 0x08802000, 0x11111111 } });
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
static bool TestCheatParseErrors() {
|
|
const Path filename = UnitTestTempPath("unittest_cheats_bad.ini");
|
|
const char *content = "_S TEST00000\n_C1 Mixed\n_L 0x20002000 0x00000001\n_M 0x08802004 0x00000002\n_C1 Extended\n_N 0xD4000001 0x00000001\n";
|
|
if (!File::WriteStringToFile(true, content, filename)) {
|
|
return false;
|
|
}
|
|
bool parsed;
|
|
size_t errorCount;
|
|
std::vector<CheatCode> cheats;
|
|
{
|
|
CheatFileParser parser(filename, "TEST00000");
|
|
parsed = parser.Parse();
|
|
errorCount = parser.GetErrors().size();
|
|
cheats = parser.GetCheats();
|
|
}
|
|
File::Delete(filename);
|
|
EXPECT_FALSE(parsed);
|
|
EXPECT_EQ_INT((int)errorCount, 2);
|
|
EXPECT_EQ_INT((int)cheats.size(), 1);
|
|
EXPECT_EQ_INT((int)cheats[0].lines.size(), 1);
|
|
EXPECT_TRUE(cheats[0].format == CheatCodeFormat::CwCheat);
|
|
return true;
|
|
}
|
|
|
|
// Cheats pasted from websites, with a BOM, no _S line and a U+202C (pop directional formatting)
|
|
// after every address (#14955).
|
|
static bool TestCheatInvisibleCharacters() {
|
|
const Path filename = UnitTestTempPath("unittest_cheats_invisible.ini");
|
|
const char *content = "\xEF\xBB\xBF\n_C1 Blade Change\xE2\x80\xAC\n_L 0x002F614A\xE2\x80\xAC 0x0000003D\n_L\xC2\xA0" "0x002F614B 0x00000018\n";
|
|
if (!File::WriteStringToFile(true, content, filename)) {
|
|
return false;
|
|
}
|
|
bool parsed;
|
|
std::vector<CheatCode> cheats;
|
|
std::vector<CheatFileInfo> info;
|
|
{
|
|
CheatFileParser parser(filename, "TEST00000");
|
|
parsed = parser.Parse();
|
|
cheats = parser.GetCheats();
|
|
info = parser.GetFileInfo();
|
|
}
|
|
File::Delete(filename);
|
|
EXPECT_TRUE(parsed);
|
|
EXPECT_EQ_INT((int)cheats.size(), 1);
|
|
EXPECT_EQ_INT((int)cheats[0].lines.size(), 2);
|
|
EXPECT_EQ_HEX(cheats[0].lines[0].part1, 0x002F614A);
|
|
EXPECT_EQ_HEX(cheats[0].lines[1].part2, 0x00000018);
|
|
EXPECT_EQ_INT((int)info.size(), 1);
|
|
EXPECT_EQ_STR(info[0].name, std::string("Blade Change"));
|
|
|
|
// The length limits still apply, after the characters are dropped.
|
|
EXPECT_EQ_STR(SanitizeString("a\xE2\x80\x8B" "bcd", StringRestriction::NoInvisibleFormatting, 0, 3), std::string("abc"));
|
|
EXPECT_EQ_STR(SanitizeString("a\xE2\x80\x8B" "b", StringRestriction::NoInvisibleFormatting, 3), std::string());
|
|
return true;
|
|
}
|
|
|
|
bool TestCwCheat() {
|
|
// Other tests may leave currentMIPS pointing elsewhere; the cheat engine needs the real one.
|
|
currentMIPS = &mipsr4k;
|
|
Memory::g_MemorySize = Memory::RAM_NORMAL_SIZE;
|
|
if (!Memory::Init(Memory::MemMapSetupFlags::Default)) {
|
|
printf("Memory::Init failed\n");
|
|
return false;
|
|
}
|
|
const bool success = RunCheatTests() && TestCheatParseErrors() && TestCheatInvisibleCharacters();
|
|
Memory::Shutdown();
|
|
return success;
|
|
}
|