sceIo: generate and resolve FAT 8.3 short names

sceIoDread hands back a dirent whose d_private holds the 8.3 short name
ahead of the long name, and we never wrote the short name at all - the game
got whatever was on the stack there. Crazy Taxi: Fare Wars reads it rather
than d_name, so it rejected every file in ms0:/MUSIC, ended up with an empty
playlist and never even reserved an mp3 handle: custom soundtracks were
silently dead, with the game spinning on InitResource/SetLoopNum forever.

Generate the names from the directory listing, and resolve them back in
DirectoryFileSystem so a game can open a file by the short name it was given.
Both sides come from the same function, so they agree.

We can't lean on the host for any of this. Linux, macOS and Android have no
8.3 names at all, and while Windows does keep aliases it generates them by a
different rule - it counts to ~4 and then switches to a hash - so resolution
runs before the literal path is tried rather than as a fallback, or on
Windows we'd quietly open a different file than the one we handed the game.

The exact names a real PSP produces are still unverified - no pspautotest
covers d_private - so this implements the ordinary FAT rule and the new
FatShortNames unit test pins that down until hardware can settle it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Henrik Rydgård
2026-09-07 10:20:49 -06:00
co-authored by Claude Opus 5
parent 284472ae13
commit fb8c99ad49
6 changed files with 269 additions and 4 deletions
+57 -2
View File
@@ -612,14 +612,16 @@ int DirectoryFileSystem::RenameFile(const std::string &from, const std::string &
}
bool DirectoryFileSystem::RemoveFile(const std::string &filename) {
Path localPath = GetLocalPath(filename);
std::string resolved = filename;
ResolveShortNames(resolved);
Path localPath = GetLocalPath(resolved);
bool retValue = File::Delete(localPath);
if (flags & FileSystemFlags::CASE_SENSITIVE) {
if (!retValue) {
// May have failed due to case sensitivity, so try again. Try even if it fails?
std::string fullNamePath = filename;
std::string fullNamePath = resolved;
if (!FixPathCase(basePath, fullNamePath, FPC_FILE_MUST_EXIST))
return (bool)ReplayApplyDisk(ReplayAction::FILE_REMOVE, false, CoreTiming::GetGlobalTimeUs());
localPath = GetLocalPath(fullNamePath);
@@ -632,7 +634,53 @@ bool DirectoryFileSystem::RemoveFile(const std::string &filename) {
return ReplayApplyDisk(ReplayAction::FILE_REMOVE, retValue, CoreTiming::GetGlobalTimeUs()) != 0;
}
// Note that this runs *before* the literal path is tried, not as a fallback after it fails. That's
// deliberate: on Windows the host resolves its own 8.3 aliases, which are generated by a different
// rule than ours, so opening the literal name can quietly land on a different file than the one we
// handed the game in sceIoDread. Everywhere else the literal name simply wouldn't exist.
void DirectoryFileSystem::ResolveShortNames(std::string &path) {
// Only the memory stick is FAT, and only a name with a counter in it can be a short name, so
// ordinary paths cost one character scan and nothing more.
if (!(flags & FileSystemFlags::SIMULATE_FAT32) || path.find('~') == std::string::npos) {
return;
}
if (resolvingShortNames_) {
return;
}
resolvingShortNames_ = true;
std::vector<std::string_view> parts;
SplitString(path, '/', parts);
std::string resolved;
for (std::string_view rawPart : parts) {
std::string part(rawPart);
if (part.find('~') != std::string::npos) {
bool exists = false;
std::vector<PSPFileInfo> listing = GetDirListing(resolved, &exists);
if (exists) {
std::vector<std::string> shortNames;
GenerateFatShortNames(listing, &shortNames);
for (size_t i = 0; i < shortNames.size(); i++) {
if (equalsNoCase(shortNames[i], part)) {
part = listing[i].name;
break;
}
}
}
}
if (!resolved.empty()) {
resolved += "/";
}
resolved += part;
}
resolvingShortNames_ = false;
path = resolved;
}
int DirectoryFileSystem::OpenFile(std::string filename, FileAccess access, const char *devicename) {
ResolveShortNames(filename);
OpenFileEntry entry;
entry.hFile.fileSystemFlags_ = flags;
u32 err = 0;
@@ -761,6 +809,8 @@ static u32 PspAccessBits(bool isDirectory, bool isWritable) {
}
PSPFileInfo DirectoryFileSystem::GetFileInfo(std::string filename) {
ResolveShortNames(filename);
PSPFileInfo x;
x.name = filename;
@@ -894,6 +944,11 @@ bool DirectoryFileSystem::ComputeRecursiveDirSizeIfFast(const std::string &path,
std::vector<PSPFileInfo> DirectoryFileSystem::GetDirListing(std::string_view path, bool *exists) {
std::vector<PSPFileInfo> myVector;
// A game can open a subdirectory by its short name too.
std::string resolvedPath(path);
ResolveShortNames(resolvedPath);
path = resolvedPath;
std::vector<File::FileInfo> files;
Path localPath = GetLocalPath(path);
const int flags = File::GETFILES_GETHIDDEN | File::GETFILES_GET_NAVIGATION_ENTRIES;