Path: blob/master/Core/FileSystems/DirectoryFileSystem.cpp
5657 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_view localPath) const {93if (localPath.empty())94return basePath;9596if (localPath[0] == '/')97localPath = localPath.substr(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_view internalPath) const {111if (internalPath.empty())112return basePath;113114if (internalPath[0] == '/')115internalPath = internalPath.substr(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#ifdef HAVE_LIBRETRO_VFS158int flags = 0;159if (access & FILEACCESS_READ && access & FILEACCESS_WRITE)160flags = RETRO_VFS_FILE_ACCESS_READ_WRITE;161else if (access & FILEACCESS_WRITE)162flags = RETRO_VFS_FILE_ACCESS_WRITE;163else if (access & FILEACCESS_READ && access & FILEACCESS_CREATE)164flags = RETRO_VFS_FILE_ACCESS_READ_WRITE;165else166flags = RETRO_VFS_FILE_ACCESS_READ;167bool success = false;168if (!(access & FILEACCESS_CREATE)) {169hFile = filestream_open(fullName.c_str(), flags & RETRO_VFS_FILE_ACCESS_WRITE ? flags | RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING : flags, RETRO_VFS_FILE_ACCESS_HINT_NONE);170success = hFile != nullptr;171} else if (!(access & FILEACCESS_EXCL)) {172hFile = filestream_open(fullName.c_str(), flags & RETRO_VFS_FILE_ACCESS_WRITE && File::Exists(fullName) ? flags | RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING : flags, RETRO_VFS_FILE_ACCESS_HINT_NONE);173success = hFile != nullptr;174} else if (!File::Exists(fullName)) {175hFile = filestream_open(fullName.c_str(), flags & RETRO_VFS_FILE_ACCESS_WRITE ? flags | RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING : flags, RETRO_VFS_FILE_ACCESS_HINT_NONE);176success = hFile != nullptr;177}178#elif PPSSPP_PLATFORM(WINDOWS)179// Convert parameters to Windows permissions and access180DWORD desired = 0;181DWORD sharemode = 0;182DWORD openmode = 0;183if (access & FILEACCESS_READ) {184desired |= GENERIC_READ;185sharemode |= FILE_SHARE_READ;186}187if (access & FILEACCESS_WRITE) {188desired |= GENERIC_WRITE;189sharemode |= FILE_SHARE_WRITE | FILE_SHARE_READ;190}191if (access & FILEACCESS_CREATE) {192if (access & FILEACCESS_EXCL) {193openmode = CREATE_NEW;194} else {195openmode = OPEN_ALWAYS;196}197} else {198openmode = OPEN_EXISTING;199}200201// Let's do it!202#if PPSSPP_PLATFORM(UWP)203hFile = CreateFile2FromAppW(fullName.ToWString().c_str(), desired, sharemode, openmode, nullptr);204#else205hFile = CreateFile(fullName.ToWString().c_str(), desired, sharemode, 0, openmode, 0, 0);206#endif207bool success = hFile != INVALID_HANDLE_VALUE;208if (!success) {209DWORD w32err = GetLastError();210211if (w32err == ERROR_SHARING_VIOLATION) {212// Sometimes, the file is locked for write, let's try again.213sharemode |= FILE_SHARE_WRITE;214#if PPSSPP_PLATFORM(UWP)215hFile = CreateFile2FromAppW(fullName.ToWString().c_str(), desired, sharemode, openmode, nullptr);216#else217hFile = CreateFile(fullName.ToWString().c_str(), desired, sharemode, 0, openmode, 0, 0);218#endif219success = hFile != INVALID_HANDLE_VALUE;220if (!success) {221w32err = GetLastError();222}223}224225if (w32err == ERROR_DISK_FULL || w32err == ERROR_NOT_ENOUGH_QUOTA) {226// This is returned when the disk is full.227auto err = GetI18NCategory(I18NCat::ERRORS);228g_OSD.Show(OSDType::MESSAGE_ERROR, err->T("Disk full while writing data"), 0.0f, "diskfull");229error = SCE_KERNEL_ERROR_ERRNO_NO_PERM;230} else if (!success) {231error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;232}233}234#else235if (fullName.Type() == PathType::CONTENT_URI) {236// Convert flags. Don't want to share this type, bad dependency.237u32 flags = File::OPEN_NONE;238if (access & FILEACCESS_READ)239flags |= File::OPEN_READ;240if (access & FILEACCESS_WRITE)241flags |= File::OPEN_WRITE;242if (access & FILEACCESS_APPEND)243flags |= File::OPEN_APPEND;244if (access & FILEACCESS_CREATE)245flags |= File::OPEN_CREATE;246// Important: don't pass TRUNCATE here, the PSP truncates weirdly. See #579.247// See above about truncate behavior. Just add READ to preserve data here.248if (access & FILEACCESS_TRUNCATE)249flags |= File::OPEN_READ;250251int fd = File::OpenFD(fullName, (File::OpenFlag)flags);252// Try to detect reads/writes to PSP/GAME to avoid them in replays.253if (fullName.FilePathContainsNoCase("PSP/GAME/")) {254inGameDir_ = true;255}256hFile = fd;257if (fd != -1) {258// Success259return true;260} else {261ERROR_LOG(Log::FileSystem, "File::OpenFD returned an error");262// TODO: Need better error codes from OpenFD so we can distinguish263// disk full. Just set not found for now.264error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;265return false;266}267}268269int flags = 0;270if (access & FILEACCESS_APPEND) {271flags |= O_APPEND;272}273if ((access & FILEACCESS_READ) && (access & FILEACCESS_WRITE)) {274flags |= O_RDWR;275} else if (access & FILEACCESS_READ) {276flags |= O_RDONLY;277} else if (access & FILEACCESS_WRITE) {278flags |= O_WRONLY;279}280if (access & FILEACCESS_CREATE) {281flags |= O_CREAT;282}283if (access & FILEACCESS_EXCL) {284flags |= O_EXCL;285}286287hFile = open(fullName.c_str(), flags, 0666);288bool success = hFile != -1;289#endif290291if (fileSystemFlags_ & FileSystemFlags::CASE_SENSITIVE) {292if (!success && !(access & FILEACCESS_CREATE)) {293if (!FixPathCase(basePath, fileName, FPC_PATH_MUST_EXIST)) {294error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;295return false;296}297fullName = GetLocalPath(basePath, fileName);298DEBUG_LOG(Log::FileSystem, "Case may have been incorrect, second try opening %s (%s)", fullName.c_str(), fileName.c_str());299300// And try again with the correct case this time301#ifdef HAVE_LIBRETRO_VFS302success = false;303if (!(access & FILEACCESS_CREATE)) {304hFile = filestream_open(fullName.c_str(), flags & RETRO_VFS_FILE_ACCESS_WRITE ? flags | RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING : flags, RETRO_VFS_FILE_ACCESS_HINT_NONE);305success = hFile != nullptr;306} else if (!(access & FILEACCESS_EXCL)) {307hFile = filestream_open(fullName.c_str(), flags & RETRO_VFS_FILE_ACCESS_WRITE && File::Exists(fullName) ? flags | RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING : flags, RETRO_VFS_FILE_ACCESS_HINT_NONE);308success = hFile != nullptr;309} else if (!File::Exists(fullName)) {310hFile = filestream_open(fullName.c_str(), flags & RETRO_VFS_FILE_ACCESS_WRITE ? flags | RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING : flags, RETRO_VFS_FILE_ACCESS_HINT_NONE);311success = hFile != nullptr;312}313#elif PPSSPP_PLATFORM(UWP)314// Should never get here.315#elif PPSSPP_PLATFORM(WINDOWS)316// Unlikely to get here, heh.317hFile = CreateFile(fullName.ToWString().c_str(), desired, sharemode, 0, openmode, 0, 0);318success = hFile != INVALID_HANDLE_VALUE;319#else320hFile = open(fullName.c_str(), flags, 0666);321success = hFile != -1;322#endif323}324}325326#if !PPSSPP_PLATFORM(WINDOWS) && !defined(HAVE_LIBRETRO_VFS)327if (success) {328// Reject directories, even if we succeed in opening them.329// TODO: Might want to do this stat first...330struct stat st;331if (fstat(hFile, &st) == 0 && S_ISDIR(st.st_mode)) {332close(hFile);333errno = EISDIR;334success = false;335}336} else if (errno == ENOSPC) {337// This is returned when the disk is full.338auto err = GetI18NCategory(I18NCat::ERRORS);339g_OSD.Show(OSDType::MESSAGE_ERROR, err->T("Disk full while writing data"), 0.0f, "diskfull");340error = SCE_KERNEL_ERROR_ERRNO_NO_PERM;341} else {342error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;343}344#endif345346// Try to detect reads/writes to PSP/GAME to avoid them in replays.347if (fullName.FilePathContainsNoCase("PSP/GAME/")) {348inGameDir_ = true;349}350if (access & (FILEACCESS_APPEND | FILEACCESS_CREATE | FILEACCESS_WRITE)) {351MemoryStick_NotifyWrite();352}353354return success;355}356357size_t DirectoryFileHandle::Read(u8* pointer, s64 size)358{359size_t bytesRead = 0;360if (needsTrunc_ != -1) {361// If the file was marked to be truncated, pretend there's nothing.362// On a PSP. it actually is truncated, but the data wasn't erased.363off_t off = (off_t)Seek(0, FILEMOVE_CURRENT);364if (needsTrunc_ <= off) {365return replay_ ? ReplayApplyDiskRead(pointer, 0, (uint32_t)size, inGameDir_, CoreTiming::GetGlobalTimeUs()) : 0;366}367if (needsTrunc_ < off + size) {368size = needsTrunc_ - off;369}370}371if (size > 0) {372#ifdef HAVE_LIBRETRO_VFS373bytesRead = fread(pointer, 1, size, hFile);374#elif defined(_WIN32)375::ReadFile(hFile, (LPVOID)pointer, (DWORD)size, (LPDWORD)&bytesRead, 0);376#else377bytesRead = read(hFile, pointer, size);378#endif379}380return replay_ ? ReplayApplyDiskRead(pointer, (uint32_t)bytesRead, (uint32_t)size, inGameDir_, CoreTiming::GetGlobalTimeUs()) : bytesRead;381}382383size_t DirectoryFileHandle::Write(const u8* pointer, s64 size)384{385size_t bytesWritten = 0;386bool diskFull = false;387388#ifdef HAVE_LIBRETRO_VFS389bytesWritten = fwrite(pointer, 1, size, hFile);390#elif defined(_WIN32)391BOOL success = ::WriteFile(hFile, (LPVOID)pointer, (DWORD)size, (LPDWORD)&bytesWritten, 0);392if (success == FALSE) {393DWORD err = GetLastError();394diskFull = err == ERROR_DISK_FULL || err == ERROR_NOT_ENOUGH_QUOTA;395}396#else397bytesWritten = write(hFile, pointer, size);398if (bytesWritten == (size_t)-1) {399diskFull = errno == ENOSPC;400}401#endif402if (needsTrunc_ != -1) {403off_t off = (off_t)Seek(0, FILEMOVE_CURRENT);404if (needsTrunc_ < off) {405needsTrunc_ = off;406}407}408409if (replay_) {410bytesWritten = ReplayApplyDiskWrite(pointer, (uint64_t)bytesWritten, (uint64_t)size, &diskFull, inGameDir_, CoreTiming::GetGlobalTimeUs());411}412413MemoryStick_NotifyWrite();414415if (diskFull) {416ERROR_LOG(Log::FileSystem, "Disk full");417auto err = GetI18NCategory(I18NCat::ERRORS);418g_OSD.Show(OSDType::MESSAGE_ERROR, err->T("Disk full while writing data"), 0.0f, "diskfull");419// We only return an error when the disk is actually full.420// When writing this would cause the disk to be full, so it wasn't written, we return 0.421Path saveFolder = GetSysDirectory(DIRECTORY_SAVEDATA);422int64_t space;423if (free_disk_space(saveFolder, space)) {424if (space < size) {425// Sign extend to a 64-bit value.426return (size_t)(s64)(s32)SCE_KERNEL_ERROR_ERRNO_DEVICE_NO_FREE_SPACE;427}428}429}430return bytesWritten;431}432433size_t DirectoryFileHandle::Seek(s32 position, FileMove type)434{435if (needsTrunc_ != -1) {436// If the file is "currently truncated" move to the end based on that position.437// The actual, underlying file hasn't been truncated (yet.)438if (type == FILEMOVE_END) {439type = FILEMOVE_BEGIN;440position = (s32)(needsTrunc_ + position);441}442}443444size_t result;445#ifdef HAVE_LIBRETRO_VFS446int moveMethod = 0;447switch (type) {448case FILEMOVE_BEGIN: moveMethod = SEEK_SET; break;449case FILEMOVE_CURRENT: moveMethod = SEEK_CUR; break;450case FILEMOVE_END: moveMethod = SEEK_END; break;451}452result = File::Fseektell(hFile, position, moveMethod);453#elif defined(_WIN32)454DWORD moveMethod = 0;455switch (type) {456case FILEMOVE_BEGIN: moveMethod = FILE_BEGIN; break;457case FILEMOVE_CURRENT: moveMethod = FILE_CURRENT; break;458case FILEMOVE_END: moveMethod = FILE_END; break;459}460461LARGE_INTEGER distance;462distance.QuadPart = position;463LARGE_INTEGER cursor;464SetFilePointerEx(hFile, distance, &cursor, moveMethod);465result = (size_t)cursor.QuadPart;466#else467int moveMethod = 0;468switch (type) {469case FILEMOVE_BEGIN: moveMethod = SEEK_SET; break;470case FILEMOVE_CURRENT: moveMethod = SEEK_CUR; break;471case FILEMOVE_END: moveMethod = SEEK_END; break;472}473result = lseek(hFile, position, moveMethod);474#endif475476return replay_ ? (size_t)ReplayApplyDisk64(ReplayAction::FILE_SEEK, result, CoreTiming::GetGlobalTimeUs()) : result;477}478479void DirectoryFileHandle::Close() {480if (needsTrunc_ != -1) {481#ifdef HAVE_LIBRETRO_VFS482if (filestream_truncate(hFile, needsTrunc_) != 0) {483ERROR_LOG(Log::FileSystem, "Failed to truncate file to %d bytes", (int)needsTrunc_);484}485#elif defined(_WIN32)486Seek((s32)needsTrunc_, FILEMOVE_BEGIN);487if (SetEndOfFile(hFile) == 0) {488ERROR_LOG(Log::FileSystem, "Failed to truncate file to %d bytes", (int)needsTrunc_);489}490#elif !PPSSPP_PLATFORM(SWITCH)491// Note: it's not great that Switch cannot truncate appropriately...492if (ftruncate(hFile, (off_t)needsTrunc_) != 0) {493ERROR_LOG(Log::FileSystem, "Failed to truncate file to %d bytes", (int)needsTrunc_);494}495#endif496}497498#ifdef HAVE_LIBRETRO_VFS499if (hFile != nullptr)500fclose(hFile);501#elif defined(_WIN32)502if (hFile != (HANDLE)-1)503CloseHandle(hFile);504#else505if (hFile != -1)506close(hFile);507#endif508}509510void DirectoryFileSystem::CloseAll() {511for (auto iter = entries.begin(); iter != entries.end(); ++iter) {512INFO_LOG(Log::FileSystem, "DirectoryFileSystem::CloseAll(): Force closing %d (%s)", (int)iter->first, iter->second.guestFilename.c_str());513iter->second.hFile.Close();514}515entries.clear();516}517518bool DirectoryFileSystem::MkDir(const std::string &dirname) {519bool result;520if (flags & FileSystemFlags::CASE_SENSITIVE) {521// Must fix case BEFORE attempting, because MkDir would create522// duplicate (different case) directories523std::string fixedCase = dirname;524if (!FixPathCase(basePath, fixedCase, FPC_PARTIAL_ALLOWED)) {525result = false;526} else {527result = File::CreateFullPath(GetLocalPath(fixedCase));528}529} else {530result = File::CreateFullPath(GetLocalPath(dirname));531}532MemoryStick_NotifyWrite();533return ReplayApplyDisk(ReplayAction::MKDIR, result, CoreTiming::GetGlobalTimeUs()) != 0;534}535536bool DirectoryFileSystem::RmDir(const std::string &dirname) {537Path fullName = GetLocalPath(dirname);538539if (flags & FileSystemFlags::CASE_SENSITIVE) {540// Maybe we're lucky?541if (File::DeleteDirRecursively(fullName)) {542MemoryStick_NotifyWrite();543return (bool)ReplayApplyDisk(ReplayAction::RMDIR, true, CoreTiming::GetGlobalTimeUs());544}545546// Nope, fix case and try again. Should we try again?547std::string fullPath = dirname;548if (!FixPathCase(basePath, fullPath, FPC_FILE_MUST_EXIST))549return (bool)ReplayApplyDisk(ReplayAction::RMDIR, false, CoreTiming::GetGlobalTimeUs());550551fullName = GetLocalPath(fullPath);552}553554bool result = File::DeleteDirRecursively(fullName);555MemoryStick_NotifyWrite();556return ReplayApplyDisk(ReplayAction::RMDIR, result, CoreTiming::GetGlobalTimeUs()) != 0;557}558559int DirectoryFileSystem::RenameFile(const std::string &from, const std::string &to) {560std::string fullTo = to;561562// Rename ignores the path (even if specified) on to.563size_t chop_at = to.find_last_of('/');564if (chop_at != to.npos)565fullTo = to.substr(chop_at + 1);566567// Now put it in the same directory as from.568size_t dirname_end = from.find_last_of('/');569if (dirname_end != from.npos)570fullTo = from.substr(0, dirname_end + 1) + fullTo;571572// At this point, we should check if the paths match and give an already exists error.573if (from == fullTo)574return ReplayApplyDisk(ReplayAction::FILE_RENAME, SCE_KERNEL_ERROR_ERRNO_FILE_ALREADY_EXISTS, CoreTiming::GetGlobalTimeUs());575576Path fullFrom = GetLocalPath(from);577578if (flags & FileSystemFlags::CASE_SENSITIVE) {579// In case TO should overwrite a file with different case. Check error code?580if (!FixPathCase(basePath, fullTo, FPC_PATH_MUST_EXIST))581return ReplayApplyDisk(ReplayAction::FILE_RENAME, -1, CoreTiming::GetGlobalTimeUs());582}583584Path fullToPath = GetLocalPath(fullTo);585586bool retValue = File::Rename(fullFrom, fullToPath);587588if (flags & FileSystemFlags::CASE_SENSITIVE) {589if (!retValue) {590// May have failed due to case sensitivity on FROM, so try again. Check error code?591std::string fullFromPath = from;592if (!FixPathCase(basePath, fullFromPath, FPC_FILE_MUST_EXIST))593return ReplayApplyDisk(ReplayAction::FILE_RENAME, -1, CoreTiming::GetGlobalTimeUs());594fullFrom = GetLocalPath(fullFromPath);595596retValue = File::Rename(fullFrom, fullToPath);597}598}599600// TODO: Better error codes.601int result = retValue ? 0 : (int)SCE_KERNEL_ERROR_ERRNO_FILE_ALREADY_EXISTS;602MemoryStick_NotifyWrite();603return ReplayApplyDisk(ReplayAction::FILE_RENAME, result, CoreTiming::GetGlobalTimeUs());604}605606bool DirectoryFileSystem::RemoveFile(const std::string &filename) {607Path localPath = GetLocalPath(filename);608609bool retValue = File::Delete(localPath);610611if (flags & FileSystemFlags::CASE_SENSITIVE) {612if (!retValue) {613// May have failed due to case sensitivity, so try again. Try even if it fails?614std::string fullNamePath = filename;615if (!FixPathCase(basePath, fullNamePath, FPC_FILE_MUST_EXIST))616return (bool)ReplayApplyDisk(ReplayAction::FILE_REMOVE, false, CoreTiming::GetGlobalTimeUs());617localPath = GetLocalPath(fullNamePath);618619retValue = File::Delete(localPath);620}621}622623MemoryStick_NotifyWrite();624return ReplayApplyDisk(ReplayAction::FILE_REMOVE, retValue, CoreTiming::GetGlobalTimeUs()) != 0;625}626627int DirectoryFileSystem::OpenFile(std::string filename, FileAccess access, const char *devicename) {628OpenFileEntry entry;629entry.hFile.fileSystemFlags_ = flags;630u32 err = 0;631bool success = entry.hFile.Open(basePath, filename, (FileAccess)(access & FILEACCESS_PSP_FLAGS), err);632if (err == 0 && !success) {633err = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;634}635636err = ReplayApplyDisk(ReplayAction::FILE_OPEN, err, CoreTiming::GetGlobalTimeUs());637if (err != 0) {638std::string errorString;639int logError;640#ifdef _WIN32641auto win32err = GetLastError();642logError = (int)win32err;643errorString = GetStringErrorMsg(win32err);644#else645logError = (int)errno;646#endif647if (!(access & FILEACCESS_PPSSPP_QUIET)) {648ERROR_LOG(Log::FileSystem, "DirectoryFileSystem::OpenFile('%s'): FAILED, %d - access = %d '%s'", filename.c_str(), logError, (int)(access & FILEACCESS_PSP_FLAGS), errorString.c_str());649}650return err;651} else {652#if defined(_WIN32) || defined(HAVE_LIBRETRO_VFS)653if (access & FILEACCESS_APPEND) {654entry.hFile.Seek(0, FILEMOVE_END);655}656#endif657658u32 newHandle = hAlloc->GetNewHandle();659660entry.guestFilename = filename;661entry.access = (FileAccess)(access & FILEACCESS_PSP_FLAGS);662663entries[newHandle] = entry;664665return newHandle;666}667}668669void DirectoryFileSystem::CloseFile(u32 handle) {670EntryMap::iterator iter = entries.find(handle);671if (iter != entries.end()) {672hAlloc->FreeHandle(handle);673iter->second.hFile.Close();674entries.erase(iter);675} else {676//This shouldn't happen...677ERROR_LOG(Log::FileSystem,"Cannot close file that hasn't been opened: %08x", handle);678}679}680681bool DirectoryFileSystem::OwnsHandle(u32 handle) {682EntryMap::iterator iter = entries.find(handle);683return (iter != entries.end());684}685686int DirectoryFileSystem::Ioctl(u32 handle, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 outlen, int &usec) {687return SCE_KERNEL_ERROR_ERRNO_FUNCTION_NOT_SUPPORTED;688}689690PSPDevType DirectoryFileSystem::DevType(u32 handle) {691return PSPDevType::FILE;692}693694size_t DirectoryFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size) {695int ignored;696return ReadFile(handle, pointer, size, ignored);697}698699size_t DirectoryFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size, int &usec) {700EntryMap::iterator iter = entries.find(handle);701if (iter != entries.end()) {702if (size < 0) {703ERROR_LOG(Log::FileSystem, "Invalid read for %lld bytes from disk %s", size, iter->second.guestFilename.c_str());704return 0;705}706707size_t bytesRead = iter->second.hFile.Read(pointer,size);708return bytesRead;709} else {710// This shouldn't happen...711ERROR_LOG(Log::FileSystem,"Cannot read file that hasn't been opened: %08x", handle);712return 0;713}714}715716size_t DirectoryFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size) {717int ignored;718return WriteFile(handle, pointer, size, ignored);719}720721size_t DirectoryFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size, int &usec) {722EntryMap::iterator iter = entries.find(handle);723if (iter != entries.end()) {724size_t bytesWritten = iter->second.hFile.Write(pointer,size);725return bytesWritten;726} else {727//This shouldn't happen...728ERROR_LOG(Log::FileSystem,"Cannot write to file that hasn't been opened: %08x", handle);729return 0;730}731}732733size_t DirectoryFileSystem::SeekFile(u32 handle, s32 position, FileMove type) {734EntryMap::iterator iter = entries.find(handle);735if (iter != entries.end()) {736return iter->second.hFile.Seek(position,type);737} else {738//This shouldn't happen...739ERROR_LOG(Log::FileSystem,"Cannot seek in file that hasn't been opened: %08x", handle);740return 0;741}742}743744PSPFileInfo DirectoryFileSystem::GetFileInfo(std::string filename) {745PSPFileInfo x;746x.name = filename;747748File::FileInfo info;749Path fullName = GetLocalPath(filename);750if (!File::GetFileInfo(fullName, &info)) {751if (flags & FileSystemFlags::CASE_SENSITIVE) {752if (!FixPathCase(basePath, filename, FPC_FILE_MUST_EXIST))753return ReplayApplyDiskFileInfo(x, CoreTiming::GetGlobalTimeUs());754fullName = GetLocalPath(filename);755756if (!File::GetFileInfo(fullName, &info))757return ReplayApplyDiskFileInfo(x, CoreTiming::GetGlobalTimeUs());758} else {759return ReplayApplyDiskFileInfo(x, CoreTiming::GetGlobalTimeUs());760}761}762763x.type = info.isDirectory ? FILETYPE_DIRECTORY : FILETYPE_NORMAL;764x.exists = true;765766if (x.type != FILETYPE_DIRECTORY) {767x.size = info.size;768}769x.access = info.access;770time_t atime = info.atime;771time_t ctime = info.ctime;772time_t mtime = info.mtime;773774localtime_r((time_t*)&atime, &x.atime);775localtime_r((time_t*)&ctime, &x.ctime);776localtime_r((time_t*)&mtime, &x.mtime);777778return ReplayApplyDiskFileInfo(x, CoreTiming::GetGlobalTimeUs());779}780781PSPFileInfo DirectoryFileSystem::GetFileInfoByHandle(u32 handle) {782WARN_LOG(Log::FileSystem, "GetFileInfoByHandle not yet implemented for DirectoryFileSystem");783return PSPFileInfo();784}785786#ifdef _WIN32787#define FILETIME_FROM_UNIX_EPOCH_US 11644473600000000ULL788789static void tmFromFiletime(tm &dest, const FILETIME &src) {790u64 from_1601_us = (((u64) src.dwHighDateTime << 32ULL) + (u64) src.dwLowDateTime) / 10ULL;791u64 from_1970_us = from_1601_us - FILETIME_FROM_UNIX_EPOCH_US;792793time_t t = (time_t) (from_1970_us / 1000000UL);794localtime_s(&dest, &t);795}796#endif797798// This simulates a bug in the PSP VFAT driver.799//800// Windows NT VFAT optimizes valid DOS filenames that are in lowercase.801// The PSP VFAT driver doesn't support this optimization, and behaves like Windows 98.802// Some homebrew depends on this bug in the PSP firmware.803//804// This essentially tries to simulate the "Windows 98 world view" on modern operating systems.805// Essentially all lowercase files are seen as UPPERCASE.806//807// Note: PSP-created files would stay lowercase, but this uppercases them too.808// Hopefully no PSP games read directories after they create files in them...809static std::string SimulateVFATBug(std::string filename) {810// These are the characters allowed in DOS filenames.811static const char * const FAT_UPPER_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&'(){}-_`~";812static const char * const FAT_LOWER_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&'(){}-_`~";813static const char * const LOWER_CHARS = "abcdefghijklmnopqrstuvwxyz";814815// To avoid logging/comparing, skip all this if it has no lowercase chars to begin with.816size_t lowerchar = filename.find_first_of(LOWER_CHARS);817if (lowerchar == filename.npos) {818return filename;819}820821bool apply_hack = false;822size_t dot_pos = filename.find('.');823if (dot_pos == filename.npos && filename.length() <= 8) {824size_t badchar = filename.find_first_not_of(FAT_LOWER_CHARS);825if (badchar == filename.npos) {826// It's all lowercase. Convert to upper.827apply_hack = true;828}829} else {830// There's a separate flag for each, so we compare separately.831// But they both have to either be all upper or lowercase.832std::string base = filename.substr(0, dot_pos);833std::string ext = filename.substr(dot_pos + 1);834835// The filename must be short enough to fit.836if (base.length() <= 8 && ext.length() <= 3) {837size_t base_non_lower = base.find_first_not_of(FAT_LOWER_CHARS);838size_t base_non_upper = base.find_first_not_of(FAT_UPPER_CHARS);839size_t ext_non_lower = ext.find_first_not_of(FAT_LOWER_CHARS);840size_t ext_non_upper = ext.find_first_not_of(FAT_UPPER_CHARS);841842// As long as neither is mixed, we apply the hack.843bool base_apply_hack = base_non_lower == base.npos || base_non_upper == base.npos;844bool ext_apply_hack = ext_non_lower == ext.npos || ext_non_upper == ext.npos;845apply_hack = base_apply_hack && ext_apply_hack;846}847}848849if (apply_hack) {850VERBOSE_LOG(Log::FileSystem, "Applying VFAT hack to filename: %s", filename.c_str());851// In this situation, NT would write UPPERCASE, and just set a flag to say "actually lowercase".852// That VFAT flag isn't read by the PSP firmware, so let's pretend to "not read it."853std::transform(filename.begin(), filename.end(), filename.begin(), toupper);854}855856return filename;857}858859bool DirectoryFileSystem::ComputeRecursiveDirSizeIfFast(const std::string &path, int64_t *size) {860Path localPath = GetLocalPath(path);861862int64_t sizeTemp = File::ComputeRecursiveDirectorySize(localPath);863if (sizeTemp >= 0) {864*size = sizeTemp;865return true;866} else {867return false;868}869}870871std::vector<PSPFileInfo> DirectoryFileSystem::GetDirListing(const std::string &path, bool *exists) {872std::vector<PSPFileInfo> myVector;873874std::vector<File::FileInfo> files;875Path localPath = GetLocalPath(path);876const int flags = File::GETFILES_GETHIDDEN | File::GETFILES_GET_NAVIGATION_ENTRIES;877bool success = File::GetFilesInDir(localPath, &files, nullptr, flags);878879if (this->flags & FileSystemFlags::CASE_SENSITIVE) {880if (!success) {881// TODO: Case sensitivity should be checked on a file system basis, right?882std::string fixedPath = path;883if (FixPathCase(basePath, fixedPath, FPC_FILE_MUST_EXIST)) {884// May have failed due to case sensitivity, try again885localPath = GetLocalPath(fixedPath);886success = File::GetFilesInDir(localPath, &files, nullptr, flags);887}888}889}890891if (!success) {892if (exists)893*exists = false;894return ReplayApplyDiskListing(myVector, CoreTiming::GetGlobalTimeUs());895}896897bool hideISOFiles = PSP_CoreParameter().compat.flags().HideISOFiles;898899// Then apply transforms to match PSP idiosynchrasies, as we convert the entries.900for (auto &file : files) {901PSPFileInfo entry;902if (Flags() & FileSystemFlags::SIMULATE_FAT32) {903entry.name = SimulateVFATBug(file.name);904} else {905entry.name = file.name;906}907if (hideISOFiles) {908if (endsWithNoCase(entry.name, ".cso") || endsWithNoCase(entry.name, ".iso") || endsWithNoCase(entry.name, ".chd")) { // chd not really necessary, but let's hide them too.909// Workaround for DJ Max Portable, see compat.ini.910continue;911} else if (file.isDirectory) {912if (endsWithNoCase(path, "SAVEDATA")) {913// Don't let it see savedata from other games, it can misinterpret stuff.914std::string gameID = g_paramSFO.GetDiscID();915if (entry.name.size() > 2 && !startsWithNoCase(entry.name, gameID)) {916continue;917}918} else if (file.name == "GAME" || file.name == "TEXTURES" || file.name == "PPSSPP_STATE" || file.name == "PLUGINS" || file.name == "SYSTEM" || equalsNoCase(file.name, "Cheats")) {919// The game scans these folders on startup which can take time. Skip them.920continue;921}922}923}924if (file.name == "..") {925entry.size = 4096;926} else {927entry.size = file.size;928}929if (file.isDirectory) {930entry.type = FILETYPE_DIRECTORY;931} else {932entry.type = FILETYPE_NORMAL;933}934entry.access = file.access;935entry.exists = file.exists;936937localtime_r((time_t*)&file.atime, &entry.atime);938localtime_r((time_t*)&file.ctime, &entry.ctime);939localtime_r((time_t*)&file.mtime, &entry.mtime);940941myVector.push_back(entry);942}943944if (this->flags & FileSystemFlags::STRIP_PSP) {945if (path == "/") {946// Artificially add the /PSP directory to the root listing.947PSPFileInfo pspInfo{};948pspInfo.name = "PSP";949pspInfo.type = FILETYPE_DIRECTORY;950pspInfo.size = 4096;951pspInfo.access = 0x777;952pspInfo.exists = true;953myVector.push_back(pspInfo);954}955}956957if (exists)958*exists = true;959return ReplayApplyDiskListing(myVector, CoreTiming::GetGlobalTimeUs());960}961962u64 DirectoryFileSystem::FreeDiskSpace(const std::string &path) {963int64_t result = 0;964if (free_disk_space(GetLocalPath(path), result)) {965return ReplayApplyDisk64(ReplayAction::FREESPACE, (uint64_t)result, CoreTiming::GetGlobalTimeUs());966}967968if (flags & FileSystemFlags::CASE_SENSITIVE) {969std::string fixedCase = path;970if (FixPathCase(basePath, fixedCase, FPC_FILE_MUST_EXIST)) {971// May have failed due to case sensitivity, try again.972if (free_disk_space(GetLocalPath(fixedCase), result)) {973return ReplayApplyDisk64(ReplayAction::FREESPACE, result, CoreTiming::GetGlobalTimeUs());974}975}976}977978// Just assume they're swimming in free disk space if we don't know otherwise.979return ReplayApplyDisk64(ReplayAction::FREESPACE, std::numeric_limits<u64>::max(), CoreTiming::GetGlobalTimeUs());980}981982void DirectoryFileSystem::DoState(PointerWrap &p) {983auto s = p.Section("DirectoryFileSystem", 0, 2);984if (!s)985return;986987// Savestate layout:988// u32: number of entries989// per-entry:990// u32: handle number991// std::string filename (in guest's terms, untranslated)992// enum FileAccess file access mode993// u32 seek position994// s64 current truncate position (v2+ only)995996u32 num = (u32) entries.size();997Do(p, num);998999if (p.mode == p.MODE_READ) {1000CloseAll();1001u32 key;1002OpenFileEntry entry;1003entry.hFile.fileSystemFlags_ = flags;1004for (u32 i = 0; i < num; i++) {1005Do(p, key);1006Do(p, entry.guestFilename);1007Do(p, entry.access);1008u32 err;1009bool brokenFile = false;1010if (!entry.hFile.Open(basePath,entry.guestFilename,entry.access, err)) {1011ERROR_LOG(Log::FileSystem, "Failed to reopen file while loading state: %s", entry.guestFilename.c_str());1012brokenFile = true;1013}1014u32 position;1015Do(p, position);1016if (position != entry.hFile.Seek(position, FILEMOVE_BEGIN)) {1017ERROR_LOG(Log::FileSystem, "Failed to restore seek position while loading state: %s", entry.guestFilename.c_str());1018brokenFile = true;1019}1020if (s >= 2) {1021Do(p, entry.hFile.needsTrunc_);1022}1023// Let's hope that things don't go that badly with the file mysteriously auto-closed.1024// Better than not loading the save state at all, hopefully.1025if (!brokenFile) {1026entries[key] = entry;1027}1028}1029} else {1030for (auto iter = entries.begin(); iter != entries.end(); ++iter) {1031u32 key = iter->first;1032Do(p, key);1033Do(p, iter->second.guestFilename);1034Do(p, iter->second.access);1035u32 position = (u32)iter->second.hFile.Seek(0, FILEMOVE_CURRENT);1036Do(p, position);1037Do(p, iter->second.hFile.needsTrunc_);1038}1039}1040}1041104210431044VFSFileSystem::VFSFileSystem(IHandleAllocator *_hAlloc, std::string _basePath) : basePath(_basePath) {1045hAlloc = _hAlloc;1046}10471048VFSFileSystem::~VFSFileSystem() {1049for (auto iter = entries.begin(); iter != entries.end(); ++iter) {1050delete [] iter->second.fileData;1051}1052entries.clear();1053}10541055std::string VFSFileSystem::GetLocalPath(std::string_view localPath) const {1056return join(basePath, localPath);1057}10581059bool VFSFileSystem::MkDir(const std::string &dirname) {1060// NOT SUPPORTED - READ ONLY1061return false;1062}10631064bool VFSFileSystem::RmDir(const std::string &dirname) {1065// NOT SUPPORTED - READ ONLY1066return false;1067}10681069int VFSFileSystem::RenameFile(const std::string &from, const std::string &to) {1070// NOT SUPPORTED - READ ONLY1071return -1;1072}10731074bool VFSFileSystem::RemoveFile(const std::string &filename) {1075// NOT SUPPORTED - READ ONLY1076return false;1077}10781079int VFSFileSystem::OpenFile(std::string filename, FileAccess access, const char *devicename) {1080if (access != FILEACCESS_READ) {1081ERROR_LOG(Log::FileSystem, "VFSFileSystem only supports plain reading");1082return SCE_KERNEL_ERROR_ERRNO_INVALID_FLAG;1083}10841085std::string fullName = GetLocalPath(filename);1086const char *fullNameC = fullName.c_str();1087VERBOSE_LOG(Log::FileSystem, "VFSFileSystem actually opening %s (%s)", fullNameC, filename.c_str());10881089size_t size;1090u8 *data = g_VFS.ReadFile(fullNameC, &size);1091if (!data) {1092ERROR_LOG(Log::FileSystem, "VFSFileSystem failed to open %s", filename.c_str());1093return SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;1094}10951096OpenFileEntry entry;1097entry.fileData = data;1098entry.size = size;1099entry.seekPos = 0;1100u32 newHandle = hAlloc->GetNewHandle();1101entries[newHandle] = entry;1102return newHandle;1103}11041105PSPFileInfo VFSFileSystem::GetFileInfo(std::string filename) {1106PSPFileInfo x;1107x.name = filename;11081109std::string fullName = GetLocalPath(filename);1110File::FileInfo fo;1111if (g_VFS.GetFileInfo(fullName.c_str(), &fo)) {1112x.exists = fo.exists;1113if (x.exists) {1114x.size = fo.size;1115x.type = fo.isDirectory ? FILETYPE_DIRECTORY : FILETYPE_NORMAL;1116x.access = fo.isWritable ? 0666 : 0444;1117}1118} else {1119x.exists = false;1120}1121return x;1122}11231124PSPFileInfo VFSFileSystem::GetFileInfoByHandle(u32 handle) {1125return PSPFileInfo();1126}112711281129void VFSFileSystem::CloseFile(u32 handle) {1130EntryMap::iterator iter = entries.find(handle);1131if (iter != entries.end()) {1132delete [] iter->second.fileData;1133entries.erase(iter);1134} else {1135//This shouldn't happen...1136ERROR_LOG(Log::FileSystem,"Cannot close file that hasn't been opened: %08x", handle);1137}1138}11391140bool VFSFileSystem::OwnsHandle(u32 handle) {1141EntryMap::iterator iter = entries.find(handle);1142return (iter != entries.end());1143}11441145int VFSFileSystem::Ioctl(u32 handle, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 outlen, int &usec) {1146return SCE_KERNEL_ERROR_ERRNO_FUNCTION_NOT_SUPPORTED;1147}11481149PSPDevType VFSFileSystem::DevType(u32 handle) {1150return PSPDevType::FILE;1151}11521153size_t VFSFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size) {1154int ignored;1155return ReadFile(handle, pointer, size, ignored);1156}11571158size_t VFSFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size, int &usec) {1159DEBUG_LOG(Log::FileSystem,"VFSFileSystem::ReadFile %08x %p %i", handle, pointer, (u32)size);1160EntryMap::iterator iter = entries.find(handle);1161if (iter != entries.end())1162{1163if(iter->second.seekPos + size > iter->second.size)1164size = iter->second.size - iter->second.seekPos;1165if(size < 0) size = 0;1166size_t bytesRead = size;1167memcpy(pointer, iter->second.fileData + iter->second.seekPos, size);1168iter->second.seekPos += size;1169return bytesRead;1170} else {1171ERROR_LOG(Log::FileSystem,"Cannot read file that hasn't been opened: %08x", handle);1172return 0;1173}1174}11751176size_t VFSFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size) {1177int ignored;1178return WriteFile(handle, pointer, size, ignored);1179}11801181size_t VFSFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size, int &usec) {1182// NOT SUPPORTED - READ ONLY1183return 0;1184}11851186size_t VFSFileSystem::SeekFile(u32 handle, s32 position, FileMove type) {1187EntryMap::iterator iter = entries.find(handle);1188if (iter != entries.end()) {1189switch (type) {1190case FILEMOVE_BEGIN: iter->second.seekPos = position; break;1191case FILEMOVE_CURRENT: iter->second.seekPos += position; break;1192case FILEMOVE_END: iter->second.seekPos = iter->second.size + position; break;1193}1194return iter->second.seekPos;1195} else {1196//This shouldn't happen...1197ERROR_LOG(Log::FileSystem,"Cannot seek in file that hasn't been opened: %08x", handle);1198return 0;1199}1200}12011202std::vector<PSPFileInfo> VFSFileSystem::GetDirListing(const std::string &path, bool *exists) {1203std::vector<PSPFileInfo> myVector;1204// TODO1205if (exists)1206*exists = false;1207return myVector;1208}12091210void VFSFileSystem::DoState(PointerWrap &p) {1211// Note: used interchangeably with DirectoryFileSystem for flash0:, so we use the same name.1212auto s = p.Section("DirectoryFileSystem", 0, 2);1213if (!s)1214return;12151216u32 num = (u32) entries.size();1217Do(p, num);12181219if (num != 0) {1220p.SetError(p.ERROR_WARNING);1221ERROR_LOG(Log::FileSystem, "FIXME: Open files during savestate, could go badly.");1222}1223}122412251226