Path: blob/master/Core/FileSystems/DirectoryFileSystem.cpp
3186 views
// Copyright (c) 2012- PPSSPP Project.12// This program is free software: you can redistribute it and/or modify3// it under the terms of the GNU General Public License as published by4// the Free Software Foundation, version 2.0 or later versions.56// This program is distributed in the hope that it will be useful,7// but WITHOUT ANY WARRANTY; without even the implied warranty of8// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the9// GNU General Public License 2.0 for more details.1011// A copy of the GPL 2.0 should have been included with the program.12// If not, see http://www.gnu.org/licenses/1314// Official git repository and contact information can be found at15// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.1617#include "ppsspp_config.h"1819#include <algorithm>20#include <ctime>21#include <limits>2223#include "Common/Data/Text/I18n.h"24#include "Common/Data/Encoding/Utf8.h"25#include "Common/Serialize/Serializer.h"26#include "Common/Serialize/SerializeFuncs.h"27#include "Common/StringUtils.h"28#include "Common/System/OSD.h"29#include "Common/File/FileUtil.h"30#include "Common/File/DiskFree.h"31#include "Common/File/VFS/VFS.h"32#include "Common/SysError.h"33#include "Core/FileSystems/DirectoryFileSystem.h"34#include "Core/HLE/sceKernel.h"35#include "Core/HW/MemoryStick.h"36#include "Core/CoreTiming.h"37#include "Core/System.h"38#include "Core/Replay.h"39#include "Core/Reporting.h"40#include "Core/ELF/ParamSFO.h"4142#ifdef _WIN3243#include "Common/CommonWindows.h"44#include <sys/stat.h>45#if PPSSPP_PLATFORM(UWP)46#include <fileapifromapp.h>47#endif48#undef FILE_OPEN49#else50#include <dirent.h>51#include <unistd.h>52#include <sys/stat.h>53#if defined(__ANDROID__)54#include <sys/types.h>55#include <sys/vfs.h>56#define statvfs statfs57#else58#include <sys/statvfs.h>59#endif60#include <ctype.h>61#include <fcntl.h>62#endif6364DirectoryFileSystem::DirectoryFileSystem(IHandleAllocator *_hAlloc, const Path & _basePath, FileSystemFlags _flags) : basePath(_basePath), flags(_flags) {65File::CreateFullPath(basePath);6667static const std::string_view mixedCase = "wJpCzSBNnZfxSgoS";68static const std::string_view upperCase = "WJPCZSBNNZFXSGOS";6970// Check for case sensitivity71bool checkSucceeded = false;72File::CreateEmptyFile(basePath / mixedCase);73if (File::Exists(basePath / mixedCase)) {74checkSucceeded = true;75if (!File::Exists(basePath / upperCase)) {76flags |= FileSystemFlags::CASE_SENSITIVE;77}78}79File::Delete(basePath / mixedCase);8081INFO_LOG(Log::IO, "Is file system case sensitive? %s (base: '%s') (checkOK: %d)", (flags & FileSystemFlags::CASE_SENSITIVE) ? "yes" : "no", _basePath.c_str(), checkSucceeded);8283hAlloc = _hAlloc;84}8586DirectoryFileSystem::~DirectoryFileSystem() {87CloseAll();88}8990// TODO(scoped): Merge the two below functions somehow.9192Path DirectoryFileHandle::GetLocalPath(const Path &basePath, std::string localPath) const {93if (localPath.empty())94return basePath;9596if (localPath[0] == '/')97localPath.erase(0, 1);9899if (fileSystemFlags_ & FileSystemFlags::STRIP_PSP) {100if (localPath == "PSP") {101localPath = "/";102} else if (startsWithNoCase(localPath, "PSP/")) {103localPath = localPath.substr(4);104}105}106107return basePath / localPath;108}109110Path DirectoryFileSystem::GetLocalPath(std::string internalPath) const {111if (internalPath.empty())112return basePath;113114if (internalPath[0] == '/')115internalPath.erase(0, 1);116117if (flags & FileSystemFlags::STRIP_PSP) {118if (internalPath == "PSP") {119internalPath = "/";120} else if (startsWithNoCase(internalPath, "PSP/")) {121internalPath = internalPath.substr(4);122}123}124125return basePath / internalPath;126}127128bool DirectoryFileHandle::Open(const Path &basePath, std::string &fileName, FileAccess access, u32 &error) {129error = 0;130131if (access == FILEACCESS_NONE) {132error = SCE_KERNEL_ERROR_ERRNO_INVALID_ARGUMENT;133return false;134}135136if (fileSystemFlags_ & FileSystemFlags::CASE_SENSITIVE) {137if (access & (FILEACCESS_APPEND | FILEACCESS_CREATE | FILEACCESS_WRITE)) {138DEBUG_LOG(Log::FileSystem, "Checking case for path %s", fileName.c_str());139if (!FixPathCase(basePath, fileName, FPC_PATH_MUST_EXIST)) {140error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;141return false; // or go on and attempt (for a better error code than just 0?)142}143}144}145// else we try fopen first (in case we're lucky) before simulating case insensitivity146147Path fullName = GetLocalPath(basePath, fileName);148149// On the PSP, truncating doesn't lose data. If you seek later, you'll recover it.150// This is abnormal, so we deviate from the PSP's behavior and truncate on write/close.151// This means it's incorrectly not truncated before the write.152if (access & FILEACCESS_TRUNCATE) {153needsTrunc_ = 0;154}155156//TODO: tests, should append seek to end of file? seeking in a file opened for append?157#if PPSSPP_PLATFORM(WINDOWS)158// Convert parameters to Windows permissions and access159DWORD desired = 0;160DWORD sharemode = 0;161DWORD openmode = 0;162if (access & FILEACCESS_READ) {163desired |= GENERIC_READ;164sharemode |= FILE_SHARE_READ;165}166if (access & FILEACCESS_WRITE) {167desired |= GENERIC_WRITE;168sharemode |= FILE_SHARE_WRITE | FILE_SHARE_READ;169}170if (access & FILEACCESS_CREATE) {171if (access & FILEACCESS_EXCL) {172openmode = CREATE_NEW;173} else {174openmode = OPEN_ALWAYS;175}176} else {177openmode = OPEN_EXISTING;178}179180// Let's do it!181#if PPSSPP_PLATFORM(UWP)182hFile = CreateFile2FromAppW(fullName.ToWString().c_str(), desired, sharemode, openmode, nullptr);183#else184hFile = CreateFile(fullName.ToWString().c_str(), desired, sharemode, 0, openmode, 0, 0);185#endif186bool success = hFile != INVALID_HANDLE_VALUE;187if (!success) {188DWORD w32err = GetLastError();189190if (w32err == ERROR_SHARING_VIOLATION) {191// Sometimes, the file is locked for write, let's try again.192sharemode |= FILE_SHARE_WRITE;193#if PPSSPP_PLATFORM(UWP)194hFile = CreateFile2FromAppW(fullName.ToWString().c_str(), desired, sharemode, openmode, nullptr);195#else196hFile = CreateFile(fullName.ToWString().c_str(), desired, sharemode, 0, openmode, 0, 0);197#endif198success = hFile != INVALID_HANDLE_VALUE;199if (!success) {200w32err = GetLastError();201}202}203204if (w32err == ERROR_DISK_FULL || w32err == ERROR_NOT_ENOUGH_QUOTA) {205// This is returned when the disk is full.206auto err = GetI18NCategory(I18NCat::ERRORS);207g_OSD.Show(OSDType::MESSAGE_ERROR, err->T("Disk full while writing data"), 0.0f, "diskfull");208error = SCE_KERNEL_ERROR_ERRNO_NO_PERM;209} else if (!success) {210error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;211}212}213#else214if (fullName.Type() == PathType::CONTENT_URI) {215// Convert flags. Don't want to share this type, bad dependency.216u32 flags = File::OPEN_NONE;217if (access & FILEACCESS_READ)218flags |= File::OPEN_READ;219if (access & FILEACCESS_WRITE)220flags |= File::OPEN_WRITE;221if (access & FILEACCESS_APPEND)222flags |= File::OPEN_APPEND;223if (access & FILEACCESS_CREATE)224flags |= File::OPEN_CREATE;225// Important: don't pass TRUNCATE here, the PSP truncates weirdly. See #579.226// See above about truncate behavior. Just add READ to preserve data here.227if (access & FILEACCESS_TRUNCATE)228flags |= File::OPEN_READ;229230int fd = File::OpenFD(fullName, (File::OpenFlag)flags);231// Try to detect reads/writes to PSP/GAME to avoid them in replays.232if (fullName.FilePathContainsNoCase("PSP/GAME/")) {233inGameDir_ = true;234}235hFile = fd;236if (fd != -1) {237// Success238return true;239} else {240ERROR_LOG(Log::FileSystem, "File::OpenFD returned an error");241// TODO: Need better error codes from OpenFD so we can distinguish242// disk full. Just set not found for now.243error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;244return false;245}246}247248int flags = 0;249if (access & FILEACCESS_APPEND) {250flags |= O_APPEND;251}252if ((access & FILEACCESS_READ) && (access & FILEACCESS_WRITE)) {253flags |= O_RDWR;254} else if (access & FILEACCESS_READ) {255flags |= O_RDONLY;256} else if (access & FILEACCESS_WRITE) {257flags |= O_WRONLY;258}259if (access & FILEACCESS_CREATE) {260flags |= O_CREAT;261}262if (access & FILEACCESS_EXCL) {263flags |= O_EXCL;264}265266hFile = open(fullName.c_str(), flags, 0666);267bool success = hFile != -1;268#endif269270if (fileSystemFlags_ & FileSystemFlags::CASE_SENSITIVE) {271if (!success && !(access & FILEACCESS_CREATE)) {272if (!FixPathCase(basePath, fileName, FPC_PATH_MUST_EXIST)) {273error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;274return false;275}276fullName = GetLocalPath(basePath, fileName);277DEBUG_LOG(Log::FileSystem, "Case may have been incorrect, second try opening %s (%s)", fullName.c_str(), fileName.c_str());278279// And try again with the correct case this time280#if PPSSPP_PLATFORM(UWP)281// Should never get here.282#elif PPSSPP_PLATFORM(WINDOWS)283// Unlikely to get here, heh.284hFile = CreateFile(fullName.ToWString().c_str(), desired, sharemode, 0, openmode, 0, 0);285success = hFile != INVALID_HANDLE_VALUE;286#else287hFile = open(fullName.c_str(), flags, 0666);288success = hFile != -1;289#endif290}291}292293#if !PPSSPP_PLATFORM(WINDOWS)294if (success) {295// Reject directories, even if we succeed in opening them.296// TODO: Might want to do this stat first...297struct stat st;298if (fstat(hFile, &st) == 0 && S_ISDIR(st.st_mode)) {299close(hFile);300errno = EISDIR;301success = false;302}303} else if (errno == ENOSPC) {304// This is returned when the disk is full.305auto err = GetI18NCategory(I18NCat::ERRORS);306g_OSD.Show(OSDType::MESSAGE_ERROR, err->T("Disk full while writing data"), 0.0f, "diskfull");307error = SCE_KERNEL_ERROR_ERRNO_NO_PERM;308} else {309error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;310}311#endif312313// Try to detect reads/writes to PSP/GAME to avoid them in replays.314if (fullName.FilePathContainsNoCase("PSP/GAME/")) {315inGameDir_ = true;316}317if (access & (FILEACCESS_APPEND | FILEACCESS_CREATE | FILEACCESS_WRITE)) {318MemoryStick_NotifyWrite();319}320321return success;322}323324size_t DirectoryFileHandle::Read(u8* pointer, s64 size)325{326size_t bytesRead = 0;327if (needsTrunc_ != -1) {328// If the file was marked to be truncated, pretend there's nothing.329// On a PSP. it actually is truncated, but the data wasn't erased.330off_t off = (off_t)Seek(0, FILEMOVE_CURRENT);331if (needsTrunc_ <= off) {332return replay_ ? ReplayApplyDiskRead(pointer, 0, (uint32_t)size, inGameDir_, CoreTiming::GetGlobalTimeUs()) : 0;333}334if (needsTrunc_ < off + size) {335size = needsTrunc_ - off;336}337}338if (size > 0) {339#ifdef _WIN32340::ReadFile(hFile, (LPVOID)pointer, (DWORD)size, (LPDWORD)&bytesRead, 0);341#else342bytesRead = read(hFile, pointer, size);343#endif344}345return replay_ ? ReplayApplyDiskRead(pointer, (uint32_t)bytesRead, (uint32_t)size, inGameDir_, CoreTiming::GetGlobalTimeUs()) : bytesRead;346}347348size_t DirectoryFileHandle::Write(const u8* pointer, s64 size)349{350size_t bytesWritten = 0;351bool diskFull = false;352353#ifdef _WIN32354BOOL success = ::WriteFile(hFile, (LPVOID)pointer, (DWORD)size, (LPDWORD)&bytesWritten, 0);355if (success == FALSE) {356DWORD err = GetLastError();357diskFull = err == ERROR_DISK_FULL || err == ERROR_NOT_ENOUGH_QUOTA;358}359#else360bytesWritten = write(hFile, pointer, size);361if (bytesWritten == (size_t)-1) {362diskFull = errno == ENOSPC;363}364#endif365if (needsTrunc_ != -1) {366off_t off = (off_t)Seek(0, FILEMOVE_CURRENT);367if (needsTrunc_ < off) {368needsTrunc_ = off;369}370}371372if (replay_) {373bytesWritten = ReplayApplyDiskWrite(pointer, (uint64_t)bytesWritten, (uint64_t)size, &diskFull, inGameDir_, CoreTiming::GetGlobalTimeUs());374}375376MemoryStick_NotifyWrite();377378if (diskFull) {379ERROR_LOG(Log::FileSystem, "Disk full");380auto err = GetI18NCategory(I18NCat::ERRORS);381g_OSD.Show(OSDType::MESSAGE_ERROR, err->T("Disk full while writing data"), 0.0f, "diskfull");382// We only return an error when the disk is actually full.383// When writing this would cause the disk to be full, so it wasn't written, we return 0.384Path saveFolder = GetSysDirectory(DIRECTORY_SAVEDATA);385int64_t space;386if (free_disk_space(saveFolder, space)) {387if (space < size) {388// Sign extend to a 64-bit value.389return (size_t)(s64)(s32)SCE_KERNEL_ERROR_ERRNO_DEVICE_NO_FREE_SPACE;390}391}392}393return bytesWritten;394}395396size_t DirectoryFileHandle::Seek(s32 position, FileMove type)397{398if (needsTrunc_ != -1) {399// If the file is "currently truncated" move to the end based on that position.400// The actual, underlying file hasn't been truncated (yet.)401if (type == FILEMOVE_END) {402type = FILEMOVE_BEGIN;403position = (s32)(needsTrunc_ + position);404}405}406407size_t result;408#ifdef _WIN32409DWORD moveMethod = 0;410switch (type) {411case FILEMOVE_BEGIN: moveMethod = FILE_BEGIN; break;412case FILEMOVE_CURRENT: moveMethod = FILE_CURRENT; break;413case FILEMOVE_END: moveMethod = FILE_END; break;414}415416LARGE_INTEGER distance;417distance.QuadPart = position;418LARGE_INTEGER cursor;419SetFilePointerEx(hFile, distance, &cursor, moveMethod);420result = (size_t)cursor.QuadPart;421#else422int moveMethod = 0;423switch (type) {424case FILEMOVE_BEGIN: moveMethod = SEEK_SET; break;425case FILEMOVE_CURRENT: moveMethod = SEEK_CUR; break;426case FILEMOVE_END: moveMethod = SEEK_END; break;427}428result = lseek(hFile, position, moveMethod);429#endif430431return replay_ ? (size_t)ReplayApplyDisk64(ReplayAction::FILE_SEEK, result, CoreTiming::GetGlobalTimeUs()) : result;432}433434void DirectoryFileHandle::Close() {435if (needsTrunc_ != -1) {436#ifdef _WIN32437Seek((s32)needsTrunc_, FILEMOVE_BEGIN);438if (SetEndOfFile(hFile) == 0) {439ERROR_LOG(Log::FileSystem, "Failed to truncate file to %d bytes", (int)needsTrunc_);440}441#elif !PPSSPP_PLATFORM(SWITCH)442// Note: it's not great that Switch cannot truncate appropriately...443if (ftruncate(hFile, (off_t)needsTrunc_) != 0) {444ERROR_LOG(Log::FileSystem, "Failed to truncate file to %d bytes", (int)needsTrunc_);445}446#endif447}448449#ifdef _WIN32450if (hFile != (HANDLE)-1)451CloseHandle(hFile);452#else453if (hFile != -1)454close(hFile);455#endif456}457458void DirectoryFileSystem::CloseAll() {459for (auto iter = entries.begin(); iter != entries.end(); ++iter) {460INFO_LOG(Log::FileSystem, "DirectoryFileSystem::CloseAll(): Force closing %d (%s)", (int)iter->first, iter->second.guestFilename.c_str());461iter->second.hFile.Close();462}463entries.clear();464}465466bool DirectoryFileSystem::MkDir(const std::string &dirname) {467bool result;468if (flags & FileSystemFlags::CASE_SENSITIVE) {469// Must fix case BEFORE attempting, because MkDir would create470// duplicate (different case) directories471std::string fixedCase = dirname;472if (!FixPathCase(basePath, fixedCase, FPC_PARTIAL_ALLOWED)) {473result = false;474} else {475result = File::CreateFullPath(GetLocalPath(fixedCase));476}477} else {478result = File::CreateFullPath(GetLocalPath(dirname));479}480MemoryStick_NotifyWrite();481return ReplayApplyDisk(ReplayAction::MKDIR, result, CoreTiming::GetGlobalTimeUs()) != 0;482}483484bool DirectoryFileSystem::RmDir(const std::string &dirname) {485Path fullName = GetLocalPath(dirname);486487if (flags & FileSystemFlags::CASE_SENSITIVE) {488// Maybe we're lucky?489if (File::DeleteDirRecursively(fullName)) {490MemoryStick_NotifyWrite();491return (bool)ReplayApplyDisk(ReplayAction::RMDIR, true, CoreTiming::GetGlobalTimeUs());492}493494// Nope, fix case and try again. Should we try again?495std::string fullPath = dirname;496if (!FixPathCase(basePath, fullPath, FPC_FILE_MUST_EXIST))497return (bool)ReplayApplyDisk(ReplayAction::RMDIR, false, CoreTiming::GetGlobalTimeUs());498499fullName = GetLocalPath(fullPath);500}501502bool result = File::DeleteDirRecursively(fullName);503MemoryStick_NotifyWrite();504return ReplayApplyDisk(ReplayAction::RMDIR, result, CoreTiming::GetGlobalTimeUs()) != 0;505}506507int DirectoryFileSystem::RenameFile(const std::string &from, const std::string &to) {508std::string fullTo = to;509510// Rename ignores the path (even if specified) on to.511size_t chop_at = to.find_last_of('/');512if (chop_at != to.npos)513fullTo = to.substr(chop_at + 1);514515// Now put it in the same directory as from.516size_t dirname_end = from.find_last_of('/');517if (dirname_end != from.npos)518fullTo = from.substr(0, dirname_end + 1) + fullTo;519520// At this point, we should check if the paths match and give an already exists error.521if (from == fullTo)522return ReplayApplyDisk(ReplayAction::FILE_RENAME, SCE_KERNEL_ERROR_ERRNO_FILE_ALREADY_EXISTS, CoreTiming::GetGlobalTimeUs());523524Path fullFrom = GetLocalPath(from);525526if (flags & FileSystemFlags::CASE_SENSITIVE) {527// In case TO should overwrite a file with different case. Check error code?528if (!FixPathCase(basePath, fullTo, FPC_PATH_MUST_EXIST))529return ReplayApplyDisk(ReplayAction::FILE_RENAME, -1, CoreTiming::GetGlobalTimeUs());530}531532Path fullToPath = GetLocalPath(fullTo);533534bool retValue = File::Rename(fullFrom, fullToPath);535536if (flags & FileSystemFlags::CASE_SENSITIVE) {537if (!retValue) {538// May have failed due to case sensitivity on FROM, so try again. Check error code?539std::string fullFromPath = from;540if (!FixPathCase(basePath, fullFromPath, FPC_FILE_MUST_EXIST))541return ReplayApplyDisk(ReplayAction::FILE_RENAME, -1, CoreTiming::GetGlobalTimeUs());542fullFrom = GetLocalPath(fullFromPath);543544retValue = File::Rename(fullFrom, fullToPath);545}546}547548// TODO: Better error codes.549int result = retValue ? 0 : (int)SCE_KERNEL_ERROR_ERRNO_FILE_ALREADY_EXISTS;550MemoryStick_NotifyWrite();551return ReplayApplyDisk(ReplayAction::FILE_RENAME, result, CoreTiming::GetGlobalTimeUs());552}553554bool DirectoryFileSystem::RemoveFile(const std::string &filename) {555Path localPath = GetLocalPath(filename);556557bool retValue = File::Delete(localPath);558559if (flags & FileSystemFlags::CASE_SENSITIVE) {560if (!retValue) {561// May have failed due to case sensitivity, so try again. Try even if it fails?562std::string fullNamePath = filename;563if (!FixPathCase(basePath, fullNamePath, FPC_FILE_MUST_EXIST))564return (bool)ReplayApplyDisk(ReplayAction::FILE_REMOVE, false, CoreTiming::GetGlobalTimeUs());565localPath = GetLocalPath(fullNamePath);566567retValue = File::Delete(localPath);568}569}570571MemoryStick_NotifyWrite();572return ReplayApplyDisk(ReplayAction::FILE_REMOVE, retValue, CoreTiming::GetGlobalTimeUs()) != 0;573}574575int DirectoryFileSystem::OpenFile(std::string filename, FileAccess access, const char *devicename) {576OpenFileEntry entry;577entry.hFile.fileSystemFlags_ = flags;578u32 err = 0;579bool success = entry.hFile.Open(basePath, filename, (FileAccess)(access & FILEACCESS_PSP_FLAGS), err);580if (err == 0 && !success) {581err = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;582}583584err = ReplayApplyDisk(ReplayAction::FILE_OPEN, err, CoreTiming::GetGlobalTimeUs());585if (err != 0) {586std::string errorString;587int logError;588#ifdef _WIN32589auto win32err = GetLastError();590logError = (int)win32err;591errorString = GetStringErrorMsg(win32err);592#else593logError = (int)errno;594#endif595if (!(access & FILEACCESS_PPSSPP_QUIET)) {596ERROR_LOG(Log::FileSystem, "DirectoryFileSystem::OpenFile('%s'): FAILED, %d - access = %d '%s'", filename.c_str(), logError, (int)(access & FILEACCESS_PSP_FLAGS), errorString.c_str());597}598return err;599} else {600#ifdef _WIN32601if (access & FILEACCESS_APPEND) {602entry.hFile.Seek(0, FILEMOVE_END);603}604#endif605606u32 newHandle = hAlloc->GetNewHandle();607608entry.guestFilename = filename;609entry.access = (FileAccess)(access & FILEACCESS_PSP_FLAGS);610611entries[newHandle] = entry;612613return newHandle;614}615}616617void DirectoryFileSystem::CloseFile(u32 handle) {618EntryMap::iterator iter = entries.find(handle);619if (iter != entries.end()) {620hAlloc->FreeHandle(handle);621iter->second.hFile.Close();622entries.erase(iter);623} else {624//This shouldn't happen...625ERROR_LOG(Log::FileSystem,"Cannot close file that hasn't been opened: %08x", handle);626}627}628629bool DirectoryFileSystem::OwnsHandle(u32 handle) {630EntryMap::iterator iter = entries.find(handle);631return (iter != entries.end());632}633634int DirectoryFileSystem::Ioctl(u32 handle, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 outlen, int &usec) {635return SCE_KERNEL_ERROR_ERRNO_FUNCTION_NOT_SUPPORTED;636}637638PSPDevType DirectoryFileSystem::DevType(u32 handle) {639return PSPDevType::FILE;640}641642size_t DirectoryFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size) {643int ignored;644return ReadFile(handle, pointer, size, ignored);645}646647size_t DirectoryFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size, int &usec) {648EntryMap::iterator iter = entries.find(handle);649if (iter != entries.end()) {650if (size < 0) {651ERROR_LOG(Log::FileSystem, "Invalid read for %lld bytes from disk %s", size, iter->second.guestFilename.c_str());652return 0;653}654655size_t bytesRead = iter->second.hFile.Read(pointer,size);656return bytesRead;657} else {658// This shouldn't happen...659ERROR_LOG(Log::FileSystem,"Cannot read file that hasn't been opened: %08x", handle);660return 0;661}662}663664size_t DirectoryFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size) {665int ignored;666return WriteFile(handle, pointer, size, ignored);667}668669size_t DirectoryFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size, int &usec) {670EntryMap::iterator iter = entries.find(handle);671if (iter != entries.end()) {672size_t bytesWritten = iter->second.hFile.Write(pointer,size);673return bytesWritten;674} else {675//This shouldn't happen...676ERROR_LOG(Log::FileSystem,"Cannot write to file that hasn't been opened: %08x", handle);677return 0;678}679}680681size_t DirectoryFileSystem::SeekFile(u32 handle, s32 position, FileMove type) {682EntryMap::iterator iter = entries.find(handle);683if (iter != entries.end()) {684return iter->second.hFile.Seek(position,type);685} else {686//This shouldn't happen...687ERROR_LOG(Log::FileSystem,"Cannot seek in file that hasn't been opened: %08x", handle);688return 0;689}690}691692PSPFileInfo DirectoryFileSystem::GetFileInfo(std::string filename) {693PSPFileInfo x;694x.name = filename;695696File::FileInfo info;697Path fullName = GetLocalPath(filename);698if (!File::GetFileInfo(fullName, &info)) {699if (flags & FileSystemFlags::CASE_SENSITIVE) {700if (!FixPathCase(basePath, filename, FPC_FILE_MUST_EXIST))701return ReplayApplyDiskFileInfo(x, CoreTiming::GetGlobalTimeUs());702fullName = GetLocalPath(filename);703704if (!File::GetFileInfo(fullName, &info))705return ReplayApplyDiskFileInfo(x, CoreTiming::GetGlobalTimeUs());706} else {707return ReplayApplyDiskFileInfo(x, CoreTiming::GetGlobalTimeUs());708}709}710711x.type = info.isDirectory ? FILETYPE_DIRECTORY : FILETYPE_NORMAL;712x.exists = true;713714if (x.type != FILETYPE_DIRECTORY) {715x.size = info.size;716}717x.access = info.access;718time_t atime = info.atime;719time_t ctime = info.ctime;720time_t mtime = info.mtime;721722localtime_r((time_t*)&atime, &x.atime);723localtime_r((time_t*)&ctime, &x.ctime);724localtime_r((time_t*)&mtime, &x.mtime);725726return ReplayApplyDiskFileInfo(x, CoreTiming::GetGlobalTimeUs());727}728729PSPFileInfo DirectoryFileSystem::GetFileInfoByHandle(u32 handle) {730WARN_LOG(Log::FileSystem, "GetFileInfoByHandle not yet implemented for DirectoryFileSystem");731return PSPFileInfo();732}733734#ifdef _WIN32735#define FILETIME_FROM_UNIX_EPOCH_US 11644473600000000ULL736737static void tmFromFiletime(tm &dest, const FILETIME &src) {738u64 from_1601_us = (((u64) src.dwHighDateTime << 32ULL) + (u64) src.dwLowDateTime) / 10ULL;739u64 from_1970_us = from_1601_us - FILETIME_FROM_UNIX_EPOCH_US;740741time_t t = (time_t) (from_1970_us / 1000000UL);742localtime_s(&dest, &t);743}744#endif745746// This simulates a bug in the PSP VFAT driver.747//748// Windows NT VFAT optimizes valid DOS filenames that are in lowercase.749// The PSP VFAT driver doesn't support this optimization, and behaves like Windows 98.750// Some homebrew depends on this bug in the PSP firmware.751//752// This essentially tries to simulate the "Windows 98 world view" on modern operating systems.753// Essentially all lowercase files are seen as UPPERCASE.754//755// Note: PSP-created files would stay lowercase, but this uppercases them too.756// Hopefully no PSP games read directories after they create files in them...757static std::string SimulateVFATBug(std::string filename) {758// These are the characters allowed in DOS filenames.759static const char * const FAT_UPPER_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&'(){}-_`~";760static const char * const FAT_LOWER_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&'(){}-_`~";761static const char * const LOWER_CHARS = "abcdefghijklmnopqrstuvwxyz";762763// To avoid logging/comparing, skip all this if it has no lowercase chars to begin with.764size_t lowerchar = filename.find_first_of(LOWER_CHARS);765if (lowerchar == filename.npos) {766return filename;767}768769bool apply_hack = false;770size_t dot_pos = filename.find('.');771if (dot_pos == filename.npos && filename.length() <= 8) {772size_t badchar = filename.find_first_not_of(FAT_LOWER_CHARS);773if (badchar == filename.npos) {774// It's all lowercase. Convert to upper.775apply_hack = true;776}777} else {778// There's a separate flag for each, so we compare separately.779// But they both have to either be all upper or lowercase.780std::string base = filename.substr(0, dot_pos);781std::string ext = filename.substr(dot_pos + 1);782783// The filename must be short enough to fit.784if (base.length() <= 8 && ext.length() <= 3) {785size_t base_non_lower = base.find_first_not_of(FAT_LOWER_CHARS);786size_t base_non_upper = base.find_first_not_of(FAT_UPPER_CHARS);787size_t ext_non_lower = ext.find_first_not_of(FAT_LOWER_CHARS);788size_t ext_non_upper = ext.find_first_not_of(FAT_UPPER_CHARS);789790// As long as neither is mixed, we apply the hack.791bool base_apply_hack = base_non_lower == base.npos || base_non_upper == base.npos;792bool ext_apply_hack = ext_non_lower == ext.npos || ext_non_upper == ext.npos;793apply_hack = base_apply_hack && ext_apply_hack;794}795}796797if (apply_hack) {798VERBOSE_LOG(Log::FileSystem, "Applying VFAT hack to filename: %s", filename.c_str());799// In this situation, NT would write UPPERCASE, and just set a flag to say "actually lowercase".800// That VFAT flag isn't read by the PSP firmware, so let's pretend to "not read it."801std::transform(filename.begin(), filename.end(), filename.begin(), toupper);802}803804return filename;805}806807bool DirectoryFileSystem::ComputeRecursiveDirSizeIfFast(const std::string &path, int64_t *size) {808Path localPath = GetLocalPath(path);809810int64_t sizeTemp = File::ComputeRecursiveDirectorySize(localPath);811if (sizeTemp >= 0) {812*size = sizeTemp;813return true;814} else {815return false;816}817}818819std::vector<PSPFileInfo> DirectoryFileSystem::GetDirListing(const std::string &path, bool *exists) {820std::vector<PSPFileInfo> myVector;821822std::vector<File::FileInfo> files;823Path localPath = GetLocalPath(path);824const int flags = File::GETFILES_GETHIDDEN | File::GETFILES_GET_NAVIGATION_ENTRIES;825bool success = File::GetFilesInDir(localPath, &files, nullptr, flags);826827if (this->flags & FileSystemFlags::CASE_SENSITIVE) {828if (!success) {829// TODO: Case sensitivity should be checked on a file system basis, right?830std::string fixedPath = path;831if (FixPathCase(basePath, fixedPath, FPC_FILE_MUST_EXIST)) {832// May have failed due to case sensitivity, try again833localPath = GetLocalPath(fixedPath);834success = File::GetFilesInDir(localPath, &files, nullptr, flags);835}836}837}838839if (!success) {840if (exists)841*exists = false;842return ReplayApplyDiskListing(myVector, CoreTiming::GetGlobalTimeUs());843}844845bool hideISOFiles = PSP_CoreParameter().compat.flags().HideISOFiles;846847// Then apply transforms to match PSP idiosynchrasies, as we convert the entries.848for (auto &file : files) {849PSPFileInfo entry;850if (Flags() & FileSystemFlags::SIMULATE_FAT32) {851entry.name = SimulateVFATBug(file.name);852} else {853entry.name = file.name;854}855if (hideISOFiles) {856if (endsWithNoCase(entry.name, ".cso") || endsWithNoCase(entry.name, ".iso") || endsWithNoCase(entry.name, ".chd")) { // chd not really necessary, but let's hide them too.857// Workaround for DJ Max Portable, see compat.ini.858continue;859} else if (file.isDirectory) {860if (endsWithNoCase(path, "SAVEDATA")) {861// Don't let it see savedata from other games, it can misinterpret stuff.862std::string gameID = g_paramSFO.GetDiscID();863if (entry.name.size() > 2 && !startsWithNoCase(entry.name, gameID)) {864continue;865}866} else if (file.name == "GAME" || file.name == "TEXTURES" || file.name == "PPSSPP_STATE" || file.name == "PLUGINS" || file.name == "SYSTEM" || equalsNoCase(file.name, "Cheats")) {867// The game scans these folders on startup which can take time. Skip them.868continue;869}870}871}872if (file.name == "..") {873entry.size = 4096;874} else {875entry.size = file.size;876}877if (file.isDirectory) {878entry.type = FILETYPE_DIRECTORY;879} else {880entry.type = FILETYPE_NORMAL;881}882entry.access = file.access;883entry.exists = file.exists;884885localtime_r((time_t*)&file.atime, &entry.atime);886localtime_r((time_t*)&file.ctime, &entry.ctime);887localtime_r((time_t*)&file.mtime, &entry.mtime);888889myVector.push_back(entry);890}891892if (this->flags & FileSystemFlags::STRIP_PSP) {893if (path == "/") {894// Artificially add the /PSP directory to the root listing.895PSPFileInfo pspInfo{};896pspInfo.name = "PSP";897pspInfo.type = FILETYPE_DIRECTORY;898pspInfo.size = 4096;899pspInfo.access = 0x777;900pspInfo.exists = true;901myVector.push_back(pspInfo);902}903}904905if (exists)906*exists = true;907return ReplayApplyDiskListing(myVector, CoreTiming::GetGlobalTimeUs());908}909910u64 DirectoryFileSystem::FreeDiskSpace(const std::string &path) {911int64_t result = 0;912if (free_disk_space(GetLocalPath(path), result)) {913return ReplayApplyDisk64(ReplayAction::FREESPACE, (uint64_t)result, CoreTiming::GetGlobalTimeUs());914}915916if (flags & FileSystemFlags::CASE_SENSITIVE) {917std::string fixedCase = path;918if (FixPathCase(basePath, fixedCase, FPC_FILE_MUST_EXIST)) {919// May have failed due to case sensitivity, try again.920if (free_disk_space(GetLocalPath(fixedCase), result)) {921return ReplayApplyDisk64(ReplayAction::FREESPACE, result, CoreTiming::GetGlobalTimeUs());922}923}924}925926// Just assume they're swimming in free disk space if we don't know otherwise.927return ReplayApplyDisk64(ReplayAction::FREESPACE, std::numeric_limits<u64>::max(), CoreTiming::GetGlobalTimeUs());928}929930void DirectoryFileSystem::DoState(PointerWrap &p) {931auto s = p.Section("DirectoryFileSystem", 0, 2);932if (!s)933return;934935// Savestate layout:936// u32: number of entries937// per-entry:938// u32: handle number939// std::string filename (in guest's terms, untranslated)940// enum FileAccess file access mode941// u32 seek position942// s64 current truncate position (v2+ only)943944u32 num = (u32) entries.size();945Do(p, num);946947if (p.mode == p.MODE_READ) {948CloseAll();949u32 key;950OpenFileEntry entry;951entry.hFile.fileSystemFlags_ = flags;952for (u32 i = 0; i < num; i++) {953Do(p, key);954Do(p, entry.guestFilename);955Do(p, entry.access);956u32 err;957bool brokenFile = false;958if (!entry.hFile.Open(basePath,entry.guestFilename,entry.access, err)) {959ERROR_LOG(Log::FileSystem, "Failed to reopen file while loading state: %s", entry.guestFilename.c_str());960brokenFile = true;961}962u32 position;963Do(p, position);964if (position != entry.hFile.Seek(position, FILEMOVE_BEGIN)) {965ERROR_LOG(Log::FileSystem, "Failed to restore seek position while loading state: %s", entry.guestFilename.c_str());966brokenFile = true;967}968if (s >= 2) {969Do(p, entry.hFile.needsTrunc_);970}971// Let's hope that things don't go that badly with the file mysteriously auto-closed.972// Better than not loading the save state at all, hopefully.973if (!brokenFile) {974entries[key] = entry;975}976}977} else {978for (auto iter = entries.begin(); iter != entries.end(); ++iter) {979u32 key = iter->first;980Do(p, key);981Do(p, iter->second.guestFilename);982Do(p, iter->second.access);983u32 position = (u32)iter->second.hFile.Seek(0, FILEMOVE_CURRENT);984Do(p, position);985Do(p, iter->second.hFile.needsTrunc_);986}987}988}989990991992VFSFileSystem::VFSFileSystem(IHandleAllocator *_hAlloc, std::string _basePath) : basePath(_basePath) {993hAlloc = _hAlloc;994}995996VFSFileSystem::~VFSFileSystem() {997for (auto iter = entries.begin(); iter != entries.end(); ++iter) {998delete [] iter->second.fileData;999}1000entries.clear();1001}10021003std::string VFSFileSystem::GetLocalPath(const std::string &localPath) const {1004return basePath + localPath;1005}10061007bool VFSFileSystem::MkDir(const std::string &dirname) {1008// NOT SUPPORTED - READ ONLY1009return false;1010}10111012bool VFSFileSystem::RmDir(const std::string &dirname) {1013// NOT SUPPORTED - READ ONLY1014return false;1015}10161017int VFSFileSystem::RenameFile(const std::string &from, const std::string &to) {1018// NOT SUPPORTED - READ ONLY1019return -1;1020}10211022bool VFSFileSystem::RemoveFile(const std::string &filename) {1023// NOT SUPPORTED - READ ONLY1024return false;1025}10261027int VFSFileSystem::OpenFile(std::string filename, FileAccess access, const char *devicename) {1028if (access != FILEACCESS_READ) {1029ERROR_LOG(Log::FileSystem, "VFSFileSystem only supports plain reading");1030return SCE_KERNEL_ERROR_ERRNO_INVALID_FLAG;1031}10321033std::string fullName = GetLocalPath(filename);1034const char *fullNameC = fullName.c_str();1035VERBOSE_LOG(Log::FileSystem, "VFSFileSystem actually opening %s (%s)", fullNameC, filename.c_str());10361037size_t size;1038u8 *data = g_VFS.ReadFile(fullNameC, &size);1039if (!data) {1040ERROR_LOG(Log::FileSystem, "VFSFileSystem failed to open %s", filename.c_str());1041return SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;1042}10431044OpenFileEntry entry;1045entry.fileData = data;1046entry.size = size;1047entry.seekPos = 0;1048u32 newHandle = hAlloc->GetNewHandle();1049entries[newHandle] = entry;1050return newHandle;1051}10521053PSPFileInfo VFSFileSystem::GetFileInfo(std::string filename) {1054PSPFileInfo x;1055x.name = filename;10561057std::string fullName = GetLocalPath(filename);1058File::FileInfo fo;1059if (g_VFS.GetFileInfo(fullName.c_str(), &fo)) {1060x.exists = fo.exists;1061if (x.exists) {1062x.size = fo.size;1063x.type = fo.isDirectory ? FILETYPE_DIRECTORY : FILETYPE_NORMAL;1064x.access = fo.isWritable ? 0666 : 0444;1065}1066} else {1067x.exists = false;1068}1069return x;1070}10711072PSPFileInfo VFSFileSystem::GetFileInfoByHandle(u32 handle) {1073return PSPFileInfo();1074}107510761077void VFSFileSystem::CloseFile(u32 handle) {1078EntryMap::iterator iter = entries.find(handle);1079if (iter != entries.end()) {1080delete [] iter->second.fileData;1081entries.erase(iter);1082} else {1083//This shouldn't happen...1084ERROR_LOG(Log::FileSystem,"Cannot close file that hasn't been opened: %08x", handle);1085}1086}10871088bool VFSFileSystem::OwnsHandle(u32 handle) {1089EntryMap::iterator iter = entries.find(handle);1090return (iter != entries.end());1091}10921093int VFSFileSystem::Ioctl(u32 handle, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 outlen, int &usec) {1094return SCE_KERNEL_ERROR_ERRNO_FUNCTION_NOT_SUPPORTED;1095}10961097PSPDevType VFSFileSystem::DevType(u32 handle) {1098return PSPDevType::FILE;1099}11001101size_t VFSFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size) {1102int ignored;1103return ReadFile(handle, pointer, size, ignored);1104}11051106size_t VFSFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size, int &usec) {1107DEBUG_LOG(Log::FileSystem,"VFSFileSystem::ReadFile %08x %p %i", handle, pointer, (u32)size);1108EntryMap::iterator iter = entries.find(handle);1109if (iter != entries.end())1110{1111if(iter->second.seekPos + size > iter->second.size)1112size = iter->second.size - iter->second.seekPos;1113if(size < 0) size = 0;1114size_t bytesRead = size;1115memcpy(pointer, iter->second.fileData + iter->second.seekPos, size);1116iter->second.seekPos += size;1117return bytesRead;1118} else {1119ERROR_LOG(Log::FileSystem,"Cannot read file that hasn't been opened: %08x", handle);1120return 0;1121}1122}11231124size_t VFSFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size) {1125int ignored;1126return WriteFile(handle, pointer, size, ignored);1127}11281129size_t VFSFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size, int &usec) {1130// NOT SUPPORTED - READ ONLY1131return 0;1132}11331134size_t VFSFileSystem::SeekFile(u32 handle, s32 position, FileMove type) {1135EntryMap::iterator iter = entries.find(handle);1136if (iter != entries.end()) {1137switch (type) {1138case FILEMOVE_BEGIN: iter->second.seekPos = position; break;1139case FILEMOVE_CURRENT: iter->second.seekPos += position; break;1140case FILEMOVE_END: iter->second.seekPos = iter->second.size + position; break;1141}1142return iter->second.seekPos;1143} else {1144//This shouldn't happen...1145ERROR_LOG(Log::FileSystem,"Cannot seek in file that hasn't been opened: %08x", handle);1146return 0;1147}1148}11491150std::vector<PSPFileInfo> VFSFileSystem::GetDirListing(const std::string &path, bool *exists) {1151std::vector<PSPFileInfo> myVector;1152// TODO1153if (exists)1154*exists = false;1155return myVector;1156}11571158void VFSFileSystem::DoState(PointerWrap &p) {1159// Note: used interchangeably with DirectoryFileSystem for flash0:, so we use the same name.1160auto s = p.Section("DirectoryFileSystem", 0, 2);1161if (!s)1162return;11631164u32 num = (u32) entries.size();1165Do(p, num);11661167if (num != 0) {1168p.SetError(p.ERROR_WARNING);1169ERROR_LOG(Log::FileSystem, "FIXME: Open files during savestate, could go badly.");1170}1171}117211731174