Files
ppsspp/Core/Util/PathUtil.cpp
T
Henrik RydgårdandClaude Opus 5 b5bafa43b8 Prefer upper case PSP/SHADERS and PSP/THEMES
These two were lower case for legacy reasons, unlike every other
directory, which actually matters on case sensitive file systems like
the one iOS uses. An existing lower case directory is still used if
there's no upper case one, so nobody has to move their files.

Fixes #20527

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 13:10:53 -06:00

267 lines
8.4 KiB
C++

#include <algorithm>
#include <string_view>
#include "Common/File/Path.h"
#include "Common/File/FileUtil.h"
#include "Common/StringUtils.h"
#include "Common/System/System.h"
#include "Common/Log.h"
#include "Core/Util/PathUtil.h"
#include "Core/Util/DarwinFileSystemServices.h"
#include "Core/Config.h"
#include "Common/VR/PPSSPPVR.h"
bool HasParentDirComponent(std::string_view path) {
for (size_t i = 0; i < path.size(); ) {
size_t end = path.find_first_of("/\\", i);
size_t len = end == std::string_view::npos ? path.size() - i : end - i;
if (len == 2 && path[i] == '.' && path[i + 1] == '.')
return true;
if (end == std::string_view::npos)
break;
i = end + 1;
}
return false;
}
bool HasPathTraversal(std::string_view path) {
if (path.find_first_of("/\\") != std::string_view::npos)
return true;
return path == "." || path == "..";
}
Path FindConfigFile(const Path &searchPath, std::string_view baseFilename, bool *exists) {
// Don't search for an absolute path.
if (baseFilename.size() > 1 && baseFilename[0] == '/') {
Path path(baseFilename);
*exists = File::Exists(path);
return path;
}
#ifdef _WIN32
// Handle paths starting with a drive letter.
if (baseFilename.size() > 3 && baseFilename[1] == ':' && (baseFilename[2] == '/' || baseFilename[2] == '\\')) {
Path path(baseFilename);
*exists = File::Exists(path);
return path;
}
#endif
Path filename = searchPath / baseFilename;
if (File::Exists(filename)) {
*exists = true;
return filename;
}
*exists = false;
// Make sure at least the directory it's supposed to be in exists.
Path parent = filename.NavigateUp();
// We try to create the path and ignore if it fails (already exists).
if (parent != GetSysDirectory(DIRECTORY_SYSTEM)) {
File::CreateFullPath(parent);
}
return filename;
}
// The custom shader and theme directories are lower case for legacy reasons, which is confusing
// since every other directory is upper case - and on a case sensitive file system, like the one
// iOS uses, the case actually matters. So we prefer the upper case name now, but keep using a
// lower case directory that already exists, so nobody has to move their files.
static Path PreferUpperCaseDir(const Path &parent, const char *upperCase, const char *lowerCase) {
const Path upper = parent / upperCase;
if (File::Exists(upper)) {
return upper;
}
const Path lower = parent / lowerCase;
if (File::Exists(lower)) {
return lower;
}
return upper;
}
Path GetSysDirectory(PSPDirectories directoryType) {
const Path &memStickDirectory = g_Config.memStickDirectory;
Path pspDirectory;
if (!strcasecmp(memStickDirectory.GetFilename().c_str(), "PSP")) {
// Let's strip this off, to easily allow choosing a root directory named "PSP" on Android.
pspDirectory = memStickDirectory;
} else {
pspDirectory = memStickDirectory / "PSP";
}
switch (directoryType) {
case DIRECTORY_PSP:
return pspDirectory;
case DIRECTORY_CHEATS:
return pspDirectory / "Cheats";
case DIRECTORY_GAME:
return pspDirectory / "GAME";
case DIRECTORY_SAVEDATA:
return pspDirectory / "SAVEDATA";
case DIRECTORY_SCREENSHOT:
return pspDirectory / "SCREENSHOT";
case DIRECTORY_SYSTEM:
return pspDirectory / "SYSTEM";
case DIRECTORY_PAUTH:
return memStickDirectory / "PAUTH"; // This one's at the root...
case DIRECTORY_EXDATA:
return memStickDirectory / "EXDATA"; // This one's traditionally at the root...
case DIRECTORY_DUMP:
return pspDirectory / "SYSTEM/DUMP";
case DIRECTORY_SAVESTATE:
return pspDirectory / "PPSSPP_STATE";
case DIRECTORY_CACHE:
return pspDirectory / "SYSTEM/CACHE";
case DIRECTORY_TEXTURES:
return pspDirectory / "TEXTURES";
case DIRECTORY_PLUGINS:
return pspDirectory / "PLUGINS";
case DIRECTORY_APP_CACHE:
if (!g_Config.appCacheDirectory.empty()) {
return g_Config.appCacheDirectory;
}
return pspDirectory / "SYSTEM/CACHE";
case DIRECTORY_VIDEO:
return pspDirectory / "VIDEO";
case DIRECTORY_AUDIO:
return pspDirectory / "AUDIO";
case DIRECTORY_CUSTOM_SHADERS:
return PreferUpperCaseDir(pspDirectory, "SHADERS", "shaders");
case DIRECTORY_CUSTOM_THEMES:
return PreferUpperCaseDir(pspDirectory, "THEMES", "themes");
case DIRECTORY_NAND:
return pspDirectory / "NAND";
case DIRECTORY_MEMSTICK_ROOT:
return g_Config.memStickDirectory;
// Just return the memory stick root if we run into some sort of problem.
default:
ERROR_LOG(Log::FileSystem, "Unknown directory type.");
return g_Config.memStickDirectory;
}
}
bool CreateSysDirectories() {
#if PPSSPP_PLATFORM(ANDROID)
const bool createNoMedia = true;
#else
const bool createNoMedia = false;
#endif
Path pspDir = GetSysDirectory(DIRECTORY_PSP);
INFO_LOG(Log::IO, "Creating '%s' and subdirs:", pspDir.c_str());
File::CreateFullPath(pspDir);
if (!File::Exists(pspDir)) {
INFO_LOG(Log::IO, "Not a workable memstick directory (%s). Giving up for now", pspDir.c_str());
return false;
}
// Create the default directories that a real PSP creates. Good for homebrew so they can
// expect a standard environment. Skipping THEME though, that's pointless.
static const PSPDirectories sysDirs[] = {
DIRECTORY_CHEATS,
DIRECTORY_SAVEDATA,
DIRECTORY_SAVESTATE,
DIRECTORY_GAME,
DIRECTORY_SYSTEM,
DIRECTORY_TEXTURES,
DIRECTORY_PLUGINS,
DIRECTORY_CACHE,
};
for (auto dir : sysDirs) {
Path path = GetSysDirectory(dir);
File::CreateFullPath(path);
if (createNoMedia) {
// Create a nomedia file in each specified subdirectory.
File::CreateEmptyFile(path / ".nomedia");
}
}
return true;
}
Path GetGameConfigFilePath(const Path &searchPath, std::string_view gameId, bool *exists) {
std::string_view ppssppIniFilename = IsVREnabled() ? "_ppssppvr.ini" : "_ppsspp.ini";
std::string iniFileName = join(gameId, ppssppIniFilename);
Path iniFileNameFull = FindConfigFile(searchPath, iniFileName, exists);
return iniFileNameFull;
}
// On iOS, the path to the app documents directory changes on each launch.
// Example path:
// /var/mobile/Containers/Data/Application/0E0E89DE-8D8E-485A-860C-700D8BC87B86/Documents/PSP/GAME/SuicideBarbie
// The GUID part changes on each launch.
bool TryUpdateSavedPath(Path *path) {
#if PPSSPP_PLATFORM(IOS) && !defined(__LIBRETRO__)
// DEBUG_LOG(Log::Loader, "Original path: %s", path->c_str());
std::string pathStr = path->ToString();
if (startsWith(pathStr, "/private/var/mobile/Containers/Data/Application/")) {
// For in-application files only, strip off the /private prefix if present - it's sometimes there, sometimes not.
pathStr = pathStr.substr(8);
}
const std::string_view applicationRoot = "/var/mobile/Containers/Data/Application/";
if (startsWith(pathStr, applicationRoot)) {
size_t documentsPos = pathStr.find("/Documents/");
if (documentsPos == std::string::npos) {
return false;
}
std::string memstick = g_Config.memStickDirectory.ToString();
size_t memstickDocumentsPos = memstick.find("/Documents"); // Note: No trailing slash, or we won't find it.
*path = Path(memstick.substr(0, memstickDocumentsPos) + pathStr.substr(documentsPos));
}
if (File::Exists(*path)) {
return true;
}
// Still doesn't exist? Maybe got de-authorized
// Try to "unlock" the path before the file loader hits it
Path newFilename = DarwinFileSystemServices::reauthorizeBookmarkByPath(*path);
if (!newFilename.empty()) {
INFO_LOG(Log::UI, "Bookmark rename: %s -> %s", path->c_str(), newFilename.c_str());
*path = newFilename;
return true;
}
// Path can't be auto-updated.
#endif
return false;
}
Path GetFailedBackendsDir() {
Path failedBackendsDir;
if (System_GetPropertyBool(SYSPROP_SUPPORTS_PERMISSIONS)) {
failedBackendsDir = GetSysDirectory(DIRECTORY_APP_CACHE);
} else {
failedBackendsDir = GetSysDirectory(DIRECTORY_SYSTEM);
}
return failedBackendsDir;
}
std::string GetFriendlyPath(Path path, const Path &rootMatch, std::string_view rootDisplay) {
const Path &root = rootMatch.empty() ? g_Config.memStickDirectory : rootMatch;
// Show relative to memstick root if there.
if (path.StartsWith(root)) {
std::string p;
if (root.ComputePathTo(path, p)) {
return join(rootDisplay, p);
}
std::string str = path.ToString();
if (root.size() < str.length()) {
return join(rootDisplay, str.substr(root.size()));
} else {
return std::string(rootDisplay);
}
}
#if !PPSSPP_PLATFORM(ANDROID) && (PPSSPP_PLATFORM(LINUX) || PPSSPP_PLATFORM(MAC))
std::string str = path.ToString();
char *home = getenv("HOME");
if (home != nullptr && !strncmp(str.c_str(), home, strlen(home))) {
return "~" + str.substr(strlen(home));
}
#endif
return path.ToVisualString();
}