mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-08-31 17:55:23 +02:00
Module symbols are keyed by module+crc so they're shared by every game that
loads that module. But symbols the user attaches to addresses that aren't in
any module - the heap, the stack, scratchpad, a hardware register, typically
after a memory.search - describe one game's own memory layout and are worthless
to any other game. Those now go to PSP/SYSTEM/SYMBOLS/<gameID>_syms.ppsym.
They're module index 0 ("absolute"), which already round-trips through the
existing per-module code: GetModuleRelativeAddr/GetModuleAbsoluteAddr are
identity for it, so the file format is unchanged, just with absolute addresses.
SaveModuleSymbols only needed to stop requiring a ModuleEntry. Auto-load/save
hang off CPU_Init/CPU_Shutdown rather than module load/unload, gated on the same
bAutoSaveLoadSymbols setting - and deliberately not on SYSPROP_HAS_DEBUGGER,
which only the Windows port reports true for, so LoadSymbolsIfSupported next to
it does nothing at all on headless. hle.game.saveSymbols/loadSymbols expose it.
Four things found while doing it:
- Symbols outside any module were being dropped on the floor. AddFunction/
AddData/AddLabel take moduleIndex -1 as "work it out", pass it to
GetModuleIndex(), and store whatever comes back - but that returns -1 when no
module contains the address, and -1 is never an active module, so the symbol
never reached the active maps: invisible to every lookup and to any save.
hle.data.add had spotted this and normalized -1 to 0 locally; nothing else
did, so e.g. hle.func.add outside a module silently did nothing. Fixed
centrally in a new ResolveModuleIndex() the three of them share.
(This only became reachable with the GetModuleIndex() fix in 29a38af37e -
before that it returned a wrong-but-valid module index instead.)
- The saved files were almost entirely noise. Every function the analyzer finds
is named z_un_<addr> and every import stub zz_<name>, both regenerated from
scratch on each load. One real module wrote 13KB - 443 unnamed functions and
64 stub names - for the four names a human had actually chosen. Worse, on the
next run those were loaded back as authoritative and would beat the module's
own symbols to the address. Now only names that aren't regenerated get saved,
and a module with none writes no file at all (and removes a stale one, so
deleting a symbol sticks). That module's file went 13020 -> 81 bytes.
- LoadModuleSymbols trusted the addresses in the file. It's meant to be
hand-edited and can outlive the build it came from, so relative addresses past
the end of the module are now skipped with a warning instead of landing at
nonsense addresses.
- AddFunction and AddData both erased the map entry they were updating and then
read back through the now-dangling iterator to refresh the active copy. Only
latent: the refresh is guarded on the active copy's module matching the new
one, which is false exactly when the erase happens. Re-point the iterator at
the entry's new home instead, so it can't rot if that guard ever changes.
AddLabel already did the equivalent correctly, via a local copy.
Filename sanitizing goes through SanitizeString with a new FileName restriction
rather than being open-coded; unlike the existing restrictions it substitutes
'_' instead of dropping, so two module names can't collapse onto one file.
Verified end to end on headless with cpu_alu.prx: named a function inside the
module and data/functions in scratchpad and the heap, let it exit, checked both
files, rebooted and confirmed all of it came back at the right addresses.
Unit tests 51/51, pspautotests 314/314.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
593 lines
14 KiB
C++
593 lines
14 KiB
C++
// Copyright (C) 2003 Dolphin 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 SVN repository and contact information can be found at
|
|
// http://code.google.com/p/dolphin-emu/
|
|
|
|
#include <cstring>
|
|
#include <unordered_set>
|
|
#include "ppsspp_config.h"
|
|
|
|
#ifdef _WIN32
|
|
#include "Common/CommonWindows.h"
|
|
#endif
|
|
|
|
#if PPSSPP_PLATFORM(SWITCH)
|
|
#define _GNU_SOURCE
|
|
#include <cstdio>
|
|
#endif
|
|
|
|
#include <cstdarg>
|
|
|
|
#include <string>
|
|
#include <sstream>
|
|
|
|
#include <algorithm>
|
|
#include <iomanip>
|
|
#include <cctype>
|
|
|
|
#include "Common/Buffer.h"
|
|
#include "Common/StringUtils.h"
|
|
|
|
size_t truncate_cpy(char *dest, size_t destSize, const char *src) {
|
|
size_t len = strlen(src);
|
|
if (len >= destSize - 1) {
|
|
memcpy(dest, src, destSize - 1);
|
|
len = destSize - 1;
|
|
} else {
|
|
memcpy(dest, src, len);
|
|
}
|
|
dest[len] = '\0';
|
|
return len;
|
|
}
|
|
|
|
size_t truncate_cpy(char *dest, size_t destSize, std::string_view src) {
|
|
if (src.size() > destSize - 1) {
|
|
memcpy(dest, src.data(), destSize - 1);
|
|
dest[destSize - 1] = 0;
|
|
return destSize - 1;
|
|
} else {
|
|
memcpy(dest, src.data(), src.size());
|
|
dest[src.size()] = 0;
|
|
return src.size();
|
|
}
|
|
}
|
|
|
|
long parseHexLong(const std::string &s) {
|
|
long value = 0;
|
|
|
|
if (s.substr(0,2) == "0x") {
|
|
//s = s.substr(2);
|
|
}
|
|
value = strtoul(s.c_str(),0, 0);
|
|
return value;
|
|
}
|
|
|
|
long parseLong(std::string s) {
|
|
long value = 0;
|
|
if (s.substr(0,2) == "0x") {
|
|
s = s.substr(2);
|
|
value = strtol(s.c_str(),NULL, 16);
|
|
} else {
|
|
value = strtol(s.c_str(),NULL, 10);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
bool containsNoCase(std::string_view haystack, std::string_view needle) {
|
|
auto pred = [](char ch1, char ch2) { return std::toupper(ch1) == std::toupper(ch2); };
|
|
auto found = std::search(haystack.begin(), haystack.end(), needle.begin(), needle.end(), pred);
|
|
return found != haystack.end();
|
|
}
|
|
|
|
int CountChar(std::string_view haystack, char needle) {
|
|
int count = 0;
|
|
for (int i = 0; i < (int)haystack.size(); i++) {
|
|
if (haystack[i] == needle) {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
std::string SanitizeString(std::string_view input, StringRestriction restriction, int minLength, int maxLength) {
|
|
if (restriction == StringRestriction::None) {
|
|
return std::string(input);
|
|
}
|
|
// First, remove any chars not in A-Za-z0-9_-. This will effectively get rid of any Unicode char, emojis etc too.
|
|
std::string sanitized;
|
|
sanitized.reserve(input.size());
|
|
bool lastWasLineBreak = false;
|
|
for (char c : input) {
|
|
switch (restriction) {
|
|
case StringRestriction::None:
|
|
sanitized.push_back(c);
|
|
break;
|
|
case StringRestriction::AlphaNumDashUnderscore:
|
|
if ((c >= 'A' && c <= 'Z') ||
|
|
(c >= 'a' && c <= 'z') ||
|
|
(c >= '0' && c <= '9') || c == '-' || c == '_') {
|
|
// Allowed chars.
|
|
sanitized.push_back(c);
|
|
}
|
|
break;
|
|
case StringRestriction::FileName:
|
|
if ((c >= 'A' && c <= 'Z') ||
|
|
(c >= 'a' && c <= 'z') ||
|
|
(c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.') {
|
|
sanitized.push_back(c);
|
|
} else {
|
|
sanitized.push_back('_');
|
|
}
|
|
break;
|
|
case StringRestriction::NoLineBreaksOrSpecials:
|
|
if ((uint8_t)c >= 32) {
|
|
sanitized.push_back(c);
|
|
lastWasLineBreak = false;
|
|
} else if (c == 10 || c == 13) {
|
|
// Collapse line breaks/feeds to single spaces.
|
|
if (!lastWasLineBreak) {
|
|
sanitized.push_back(' ');
|
|
lastWasLineBreak = true;
|
|
}
|
|
}
|
|
break;
|
|
case StringRestriction::ConvertToUnixEndings: // Strips off carriage returns, keeps line feeds.
|
|
if (c != '\r') {
|
|
sanitized.push_back(c);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (minLength > 0) {
|
|
if ((int)sanitized.size() < minLength) {
|
|
// Just reject it by returning an empty string, as we can't really
|
|
// conjure up new characters here.
|
|
return std::string();
|
|
}
|
|
}
|
|
|
|
if (maxLength >= 0) {
|
|
// TODO: Cut at whole UTF-8 chars!
|
|
if ((int)sanitized.size() > maxLength) {
|
|
sanitized.resize(maxLength);
|
|
}
|
|
}
|
|
|
|
if (restriction == StringRestriction::NoLineBreaksOrSpecials) {
|
|
// Additionally, cut off the string if we find an overlong UTF-8 character, such as in Jak & Daxter's title.
|
|
size_t pos = sanitized.find("\xc0\x80");
|
|
if (pos != (size_t)std::string::npos) {
|
|
sanitized.resize(pos);
|
|
}
|
|
}
|
|
return sanitized;
|
|
}
|
|
|
|
bool CharArrayFromFormatV(char* out, int outsize, const char* format, va_list args)
|
|
{
|
|
int writtenCount = vsnprintf(out, outsize, format, args);
|
|
|
|
if (writtenCount > 0 && writtenCount < outsize)
|
|
{
|
|
out[writtenCount] = '\0';
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
out[outsize - 1] = '\0';
|
|
return false;
|
|
}
|
|
}
|
|
|
|
std::string LineNumberString(const std::string &str) {
|
|
std::stringstream input(str);
|
|
std::stringstream output;
|
|
std::string line;
|
|
|
|
int lineNumber = 1;
|
|
while (std::getline(input, line)) {
|
|
output << std::setw(4) << lineNumber++ << ": " << line << std::endl;
|
|
}
|
|
|
|
return output.str();
|
|
}
|
|
|
|
std::string IndentString(const std::string &str, std::string_view sep, bool skipFirst) {
|
|
std::stringstream input(str);
|
|
std::stringstream output;
|
|
std::string line;
|
|
|
|
bool doIndent = !skipFirst;
|
|
while (std::getline(input, line)) {
|
|
if (doIndent) {
|
|
output << sep;
|
|
}
|
|
doIndent = true;
|
|
output << line << "\n";
|
|
}
|
|
|
|
return output.str();
|
|
}
|
|
|
|
std::string_view StripPrefix(std::string_view prefix, std::string_view s) {
|
|
if (startsWith(s, prefix)) {
|
|
return s.substr(prefix.size(), s.size() - prefix.size());
|
|
} else {
|
|
return s;
|
|
}
|
|
}
|
|
|
|
std::string_view KeepAfterLast(std::string_view s, char c) {
|
|
size_t pos = s.rfind(c);
|
|
if (pos != std::string_view::npos) {
|
|
return s.substr(pos + 1);
|
|
} else {
|
|
return s;
|
|
}
|
|
}
|
|
|
|
std::string_view KeepIncludingLast(std::string_view s, char c) {
|
|
size_t pos = s.rfind(c);
|
|
if (pos != std::string_view::npos) {
|
|
return s.substr(pos);
|
|
} else {
|
|
return s;
|
|
}
|
|
}
|
|
|
|
void SkipSpace(const char **ptr) {
|
|
while (**ptr && isspace(**ptr)) {
|
|
(*ptr)++;
|
|
}
|
|
}
|
|
|
|
void DataToHexString(const uint8_t *data, size_t size, std::string *output, bool lineBreaks) {
|
|
Buffer buffer;
|
|
for (size_t i = 0; i < size; i++) {
|
|
if (i && !(i & 15) && lineBreaks)
|
|
buffer.Printf("\n");
|
|
buffer.Printf("%02x ", data[i]);
|
|
}
|
|
buffer.TakeAll(output);
|
|
}
|
|
|
|
void DataToHexString(int indent, uint32_t startAddr, const uint8_t* data, size_t size, std::string *output) {
|
|
Buffer buffer;
|
|
size_t i = 0;
|
|
for (; i < size; i++) {
|
|
if (i && !(i & 15)) {
|
|
buffer.Printf(" ");
|
|
for (size_t j = i - 16; j < i; j++) {
|
|
buffer.Printf("%c", ((data[j] < 0x20) || (data[j] > 0x7e)) ? 0x2e : data[j]);
|
|
}
|
|
buffer.Printf("\n");
|
|
}
|
|
if (!(i & 15))
|
|
buffer.Printf("%*s%08x ", indent, "", (u32)(startAddr + i));
|
|
buffer.Printf("%02x ", data[i]);
|
|
}
|
|
if (size & 15) {
|
|
size_t padded_size = ((size - 1) | 15) + 1;
|
|
for (size_t j = size; j < padded_size; j++) {
|
|
buffer.Printf(" ");
|
|
}
|
|
}
|
|
if (size > 0) {
|
|
buffer.Printf(" ");
|
|
for (size_t j = (size - 1ULL) & ~UINT64_C(0xF); j < size; j++) {
|
|
buffer.Printf("%c", ((data[j] < 0x20) || (data[j] > 0x7e)) ? 0x2e : data[j]);
|
|
}
|
|
}
|
|
buffer.TakeAll(output);
|
|
}
|
|
|
|
std::string StringFromFormat(const char* format, ...) {
|
|
va_list args;
|
|
std::string temp;
|
|
#ifdef _WIN32
|
|
int required = 0;
|
|
|
|
va_start(args, format);
|
|
required = _vscprintf(format, args);
|
|
// Using + 2 to be safe between MSVC versions.
|
|
// In MSVC 2015 and later, vsnprintf counts the trailing zero (per c++11.)
|
|
temp.resize(required + 2);
|
|
if (vsnprintf(&temp[0], required + 1, format, args) < 0) {
|
|
temp.resize(0);
|
|
} else {
|
|
temp.resize(required);
|
|
}
|
|
va_end(args);
|
|
#else
|
|
char *buf = nullptr;
|
|
|
|
va_start(args, format);
|
|
if (vasprintf(&buf, format, args) < 0)
|
|
buf = nullptr;
|
|
va_end(args);
|
|
|
|
if (buf != nullptr) {
|
|
temp = buf;
|
|
free(buf);
|
|
}
|
|
#endif
|
|
return temp;
|
|
}
|
|
|
|
std::string StringFromInt(int value) {
|
|
char temp[16];
|
|
snprintf(temp, sizeof(temp), "%d", value);
|
|
return temp;
|
|
}
|
|
|
|
// Turns " hej " into "hej". Also handles tabs and line breaks.
|
|
std::string_view StripSpaces(std::string_view str) {
|
|
const size_t s = str.find_first_not_of(" \t\r\n");
|
|
if (std::string::npos != s)
|
|
return str.substr(s, str.find_last_not_of(" \t\r\n") - s + 1);
|
|
else
|
|
return "";
|
|
}
|
|
|
|
// "\"hello\"" is turned to "hello"
|
|
// This one assumes that the string has already been space stripped in both
|
|
// ends, as done by StripSpaces above, for example.
|
|
std::string_view StripQuotes(std::string_view s) {
|
|
if (s.size() && '\"' == s[0] && '\"' == *s.rbegin())
|
|
return s.substr(1, s.size() - 2);
|
|
else
|
|
return s;
|
|
}
|
|
|
|
// NOTE: str must live at least as long as all uses of output.
|
|
void SplitString(std::string_view str, const char delim, std::vector<std::string_view> &output) {
|
|
size_t next = 0;
|
|
for (size_t pos = 0, len = str.length(); pos < len; ++pos) {
|
|
if (str[pos] == delim) {
|
|
output.emplace_back(str.substr(next, pos - next));
|
|
// Skip the delimiter itself.
|
|
next = pos + 1;
|
|
}
|
|
}
|
|
|
|
if (next == 0) {
|
|
output.push_back(str);
|
|
} else if (next < str.length()) {
|
|
output.emplace_back(str.substr(next));
|
|
}
|
|
}
|
|
|
|
bool SplitStringOnce(std::string_view str, std::string_view *firstPart, std::string_view *secondPart, char delim) {
|
|
size_t pos = str.find(delim);
|
|
if (pos == std::string_view::npos) {
|
|
return false;
|
|
}
|
|
*firstPart = str.substr(0, pos);
|
|
*secondPart = str.substr(pos + 1);
|
|
return true;
|
|
}
|
|
|
|
void SplitString(std::string_view str, const char delim, std::vector<std::string> &output, bool trimOutput) {
|
|
size_t next = 0;
|
|
size_t pos = 0;
|
|
while (pos < str.length()) {
|
|
size_t delimPos = str.find(delim, pos);
|
|
if (delimPos == std::string_view::npos) {
|
|
break;
|
|
}
|
|
output.emplace_back(str.substr(next, delimPos - next));
|
|
if (trimOutput) {
|
|
output.back() = StripSpaces(output.back());
|
|
}
|
|
next = delimPos + 1;
|
|
pos = delimPos + 1;
|
|
}
|
|
|
|
if (next == 0) {
|
|
output.emplace_back(str);
|
|
} else if (next < str.length()) {
|
|
output.emplace_back(str.substr(next));
|
|
} else {
|
|
return;
|
|
}
|
|
if (trimOutput) {
|
|
output.back() = StripSpaces(output.back());
|
|
}
|
|
}
|
|
|
|
static std::string ApplyHtmlEscapes(std::string_view str_view) {
|
|
struct Repl {
|
|
const char *a;
|
|
const char *b;
|
|
};
|
|
|
|
static const Repl replacements[] = {
|
|
{ "&", "&" },
|
|
// Easy to add more cases.
|
|
};
|
|
|
|
std::string str(str_view);
|
|
for (const Repl &r : replacements) {
|
|
str = ReplaceAll(str, r.a, r.b);
|
|
}
|
|
return str;
|
|
}
|
|
|
|
// Meant for HTML listings and similar, so supports some HTML escapes.
|
|
void GetQuotedStrings(std::string_view str, std::vector<std::string> &output) {
|
|
size_t next = 0;
|
|
bool even = 0;
|
|
for (size_t pos = 0, len = str.length(); pos < len; ++pos) {
|
|
if (str[pos] == '\"' || str[pos] == '\'') {
|
|
if (even) {
|
|
//quoted text
|
|
output.emplace_back(ApplyHtmlEscapes(str.substr(next, pos - next)));
|
|
even = 0;
|
|
} else {
|
|
//non quoted text
|
|
even = 1;
|
|
}
|
|
// Skip the delimiter itself.
|
|
next = pos + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: this is quite inefficient.
|
|
std::string ReplaceAll(std::string_view input, std::string_view src, std::string_view dest) {
|
|
size_t pos = 0;
|
|
|
|
std::string result(input);
|
|
if (src == dest)
|
|
return result;
|
|
|
|
// TODO: Don't mutate the input, just append stuff to the output instead.
|
|
while (true) {
|
|
pos = result.find(src, pos);
|
|
if (pos == std::string_view::npos)
|
|
break;
|
|
result.replace(pos, src.size(), dest);
|
|
pos += dest.size();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string UnescapeMenuString(std::string_view input, char *shortcutChar) {
|
|
size_t len = input.length();
|
|
std::string output;
|
|
output.reserve(len);
|
|
bool escaping = false;
|
|
bool escapeFound = false;
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (input[i] == '&') {
|
|
if (escaping) {
|
|
output.push_back(input[i]);
|
|
escaping = false;
|
|
} else {
|
|
escaping = true;
|
|
}
|
|
} else {
|
|
output.push_back(input[i]);
|
|
if (escaping && shortcutChar && !escapeFound) {
|
|
*shortcutChar = input[i];
|
|
escapeFound = true;
|
|
}
|
|
escaping = false;
|
|
}
|
|
}
|
|
return output;
|
|
}
|
|
|
|
std::string ApplySafeSubstitutions(std::string_view format, std::string_view string1, std::string_view string2, std::string_view string3, std::string_view string4) {
|
|
size_t formatLen = format.length();
|
|
std::string output;
|
|
output.reserve(formatLen + 20);
|
|
for (size_t i = 0; i < formatLen; i++) {
|
|
char c = format[i];
|
|
if (c != '%') {
|
|
output.push_back(c);
|
|
continue;
|
|
}
|
|
if (i >= formatLen - 1) {
|
|
break;
|
|
}
|
|
switch (format[i + 1]) {
|
|
case '1':
|
|
output += string1; i++;
|
|
break;
|
|
case '2':
|
|
output += string2; i++;
|
|
break;
|
|
case '3':
|
|
output += string3; i++;
|
|
break;
|
|
case '4':
|
|
output += string4; i++;
|
|
break;
|
|
}
|
|
}
|
|
return output;
|
|
}
|
|
|
|
std::string ApplySafeSubstitutions(std::string_view format, int i1, int i2, int i3, int i4) {
|
|
size_t formatLen = format.length();
|
|
std::string output;
|
|
output.reserve(formatLen + 20);
|
|
for (size_t i = 0; i < formatLen; i++) {
|
|
char c = format[i];
|
|
if (c != '%') {
|
|
output.push_back(c);
|
|
continue;
|
|
}
|
|
if (i >= formatLen - 1) {
|
|
break;
|
|
}
|
|
switch (format[i + 1]) {
|
|
case '1':
|
|
output += StringFromInt(i1); i++;
|
|
break;
|
|
case '2':
|
|
output += StringFromInt(i2); i++;
|
|
break;
|
|
case '3':
|
|
output += StringFromInt(i3); i++;
|
|
break;
|
|
case '4':
|
|
output += StringFromInt(i4); i++;
|
|
break;
|
|
}
|
|
}
|
|
return output;
|
|
}
|
|
|
|
void MakeUnique(std::vector<std::string> &v) {
|
|
std::unordered_set<std::string> seen;
|
|
std::vector<std::string> result;
|
|
result.reserve(v.size()); // minimize reallocations
|
|
for (const auto &s : v) {
|
|
if (seen.insert(s).second) {
|
|
// insert returns {iterator, bool}
|
|
// bool == true if it was newly inserted (didn't already exist)
|
|
result.push_back(s);
|
|
}
|
|
}
|
|
v.swap(result);
|
|
}
|
|
|
|
size_t SplitSearch(std::string_view needle, std::string_view part1, std::string_view part2) {
|
|
if (part1.find(needle) != std::string_view::npos) {
|
|
// Easy case, found in part1.
|
|
return part1.find(needle);
|
|
}
|
|
size_t part1Size = part1.size();
|
|
size_t maxOverlap = std::min(needle.size() - 1, part1Size);
|
|
for (size_t overlap = maxOverlap; overlap > 0; overlap--) {
|
|
if (part1.substr(part1Size - overlap) == needle.substr(0, overlap)) {
|
|
// Found an overlap.
|
|
size_t remaining = needle.size() - overlap;
|
|
if (part2.substr(0, remaining) == needle.substr(overlap)) {
|
|
return part1Size - overlap;
|
|
}
|
|
}
|
|
}
|
|
// Now, check if it's found in part2 instead.
|
|
size_t posInPart2 = part2.find(needle);
|
|
if (posInPart2 != std::string_view::npos) {
|
|
return part1Size + posInPart2;
|
|
}
|
|
return std::string_view::npos;
|
|
}
|