Files
ppsspp/Common/StringUtils.h
Henrik RydgårdandClaude Opus 5 05f5668dfe Save symbols outside any module to a per-game file, and only save real names
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
2026-08-17 16:53:48 +02:00

256 lines
8.0 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/
#pragma once
#include <cstdarg>
#include <cstdint>
#include <string>
#include <cstring>
#include <string_view>
#include <vector>
#ifdef _MSC_VER
#define strncasecmp _strnicmp
#define strcasecmp _stricmp
#else
#include <strings.h>
#endif
// Useful for shaders with error messages..
std::string LineNumberString(const std::string &str);
std::string IndentString(const std::string &str, std::string_view sep, bool skipFirst = false);
// Other simple string utilities.
inline bool startsWith(std::string_view str, std::string_view key) {
if (str.size() < key.size())
return false;
return !memcmp(str.data(), key.data(), key.size());
}
inline bool endsWith(std::string_view str, std::string_view what) {
if (str.size() < what.size())
return false;
return str.substr(str.size() - what.size()) == what;
}
// Only use on strings where you're only concerned about ASCII.
inline bool startsWithNoCase(std::string_view str, std::string_view key) {
if (str.size() < key.size())
return false;
return strncasecmp(str.data(), key.data(), key.size()) == 0;
}
inline bool endsWithNoCase(std::string_view str, std::string_view key) {
if (str.size() < key.size())
return false;
const size_t offset = str.size() - key.size();
return strncasecmp(str.data() + offset, key.data(), key.size()) == 0;
}
inline bool equals(std::string_view str, std::string_view key) {
return str == key;
}
inline bool equalsNoCase(std::string_view str, std::string_view key) {
if (str.size() != key.size())
return false;
if (str.empty())
return true; // due to the check above, the other one is also empty.
return strncasecmp(str.data(), key.data(), key.size()) == 0;
}
bool containsNoCase(std::string_view haystack, std::string_view needle);
// Sigh
#ifdef None
#undef None
#endif
enum class StringRestriction {
None,
AlphaNumDashUnderscore, // Used for infrastructure usernames
// For deriving a filename from untrusted data (an ELF's module name, a disc ID). Unlike the
// above, disallowed characters become '_' rather than disappearing, so two different names
// can't silently collapse onto the same filename.
FileName,
NoLineBreaksOrSpecials, // Used for savedata UI. Removes line breaks, backslashes and similar.
ConvertToUnixEndings,
};
std::string SanitizeString(std::string_view username, StringRestriction restriction, int minLength = 0, int maxLength = -1);
void DataToHexString(const uint8_t *data, size_t size, std::string *output, bool lineBreaks = true);
void DataToHexString(int indent, uint32_t startAddr, const uint8_t* data, size_t size, std::string *output);
std::string StringFromFormat(const char* format, ...);
std::string StringFromInt(int value);
std::string_view KeepAfterLast(std::string_view s, char c);
std::string_view KeepIncludingLast(std::string_view s, char c);
std::string_view StripSpaces(std::string_view s);
std::string_view StripQuotes(std::string_view s);
std::string_view StripPrefix(std::string_view prefix, std::string_view s);
int CountChar(std::string_view haystack, char needle);
// 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);
// Try to avoid this when possible, in favor of the string_view version.
void SplitString(std::string_view str, const char delim, std::vector<std::string> &output, bool trimOutput = false);
// Splits on the first occurrence of delim. Returns true if the delimiter was found.
bool SplitStringOnce(std::string_view str, std::string_view *firstPart, std::string_view *secondPart, char delim);
void GetQuotedStrings(std::string_view str, std::vector<std::string> &output);
std::string ReplaceAll(std::string_view input, std::string_view src, std::string_view dest);
// Takes something like R&eplace and returns Replace, plus writes 'e' to *shortcutChar
// if not nullptr. Useful for Windows menu strings.
std::string UnescapeMenuString(std::string_view input, char *shortcutChar);
void SkipSpace(const char **ptr);
size_t truncate_cpy(char *dest, size_t destSize, const char *src);
template<size_t Count>
inline size_t truncate_cpy(char(&out)[Count], const char *src) {
return truncate_cpy(out, Count, src);
}
size_t truncate_cpy(char *dest, size_t destSize, std::string_view src);
template<size_t Count>
inline size_t truncate_cpy(char(&out)[Count], std::string_view src) {
return truncate_cpy(out, Count, src);
}
template<size_t Count>
inline std::string_view StringViewFromFixedSizeField(const char(&field)[Count]) {
return std::string_view(field, strnlen(field, Count));
}
inline std::string join(std::string_view a, std::string_view b) {
std::string result;
result.reserve(a.size() + b.size());
result.append(a);
result.append(b);
return result;
}
inline const char *safe_string(const char *s) {
return s ? s : "(null)";
}
template<size_t Count>
inline size_t truncate_cpy_len(char(&out)[Count], const char *src, size_t srcLen) {
if (srcLen >= Count) {
memcpy(out, src, Count - 1);
out[Count - 1] = '\0';
return Count - 1;
} else {
memcpy(out, src, srcLen);
out[srcLen] = '\0';
return srcLen;
}
}
inline size_t truncate_cat(char *out, size_t outLen, const char *src1, size_t src1Len, const char *src2, size_t src2Len) {
if (src1Len >= outLen) {
memcpy(out, src1, outLen);
out[outLen - 1] = '\0';
return outLen - 1;
}
memcpy(out, src1, src1Len);
size_t pos = src1Len;
size_t remaining = outLen - src1Len;
if (src2Len >= remaining) {
memcpy(out + pos, src2, remaining);
out[outLen - 1] = '\0';
return outLen - 1;
}
memcpy(out + pos, src2, src2Len);
out[pos + src2Len] = '\0';
return src1Len + src2Len;
}
long parseHexLong(const std::string &s);
long parseLong(std::string s);
// Cheap!
bool CharArrayFromFormatV(char* out, int outsize, const char* format, va_list args);
template<size_t Count>
inline void CharArrayFromFormat(char (& out)[Count], const char* format, ...)
{
va_list args;
va_start(args, format);
CharArrayFromFormatV(out, Count, format, args);
va_end(args);
}
inline void CopyStrings(std::vector<std::string> *output, const std::vector<std::string_view> &input) {
output->clear();
output->reserve(input.size());
for (auto str : input) {
output->emplace_back(str);
}
}
void MakeUnique(std::vector<std::string> &vec);
size_t SplitSearch(std::string_view needle, std::string_view part1, std::string_view part2);
// Replaces %1, %2, %3 in format with arg1, arg2, arg3.
// Much safer than snprintf and friends.
// For mixes of strings and ints, manually convert the ints to strings.
std::string ApplySafeSubstitutions(std::string_view format, std::string_view string1, std::string_view string2 = "", std::string_view string3 = "", std::string_view string4 = "");
std::string ApplySafeSubstitutions(std::string_view format, int i1, int i2 = 0, int i3 = 0, int i4 = 0);
// Not really a string util.
template<class T>
bool Contains(const std::vector<T> &vec, const T &needle) {
for (const auto &item : vec) {
if (item == needle) {
return true;
}
}
return false;
}
inline bool ContainsNoCase(const std::vector<std::string> &vec, std::string_view needle) {
for (const auto &item : vec) {
if (equalsNoCase(item, needle)) {
return true;
}
}
return false;
}
inline bool RemoveNoCase(std::vector<std::string> &vec, std::string_view needle) {
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (equalsNoCase(*it, needle)) {
vec.erase(it);
return true;
}
}
return false;
}