2022-10-26 00:25:46 +02:00

707 lines
18 KiB
C++

#include "Cafe/Filesystem/fsc.h"
#include "Cafe/Filesystem/FST/fstUtil.h"
struct FSCMountPathNode
{
std::string path;
std::vector<FSCMountPathNode*> subnodes;
FSCMountPathNode* parent;
// device target and path (if subnodes is empty)
fscDeviceC* device{ nullptr };
void* ctx{ nullptr };
std::string deviceTargetPath; // the destination base path for the device, utf8
// priority
sint32 priority{};
FSCMountPathNode(FSCMountPathNode* parent) : parent(parent)
{
}
~FSCMountPathNode()
{
for (auto& itr : subnodes)
delete itr;
subnodes.clear();
}
};
// compare two file or directory names using FSA rules
bool FSA_CompareNodeName(std::string_view a, std::string_view b)
{
if (a.size() != b.size())
return false;
for (size_t i = 0; i < a.size(); i++)
{
uint8 ac = (uint8)a[i];
uint8 bc = (uint8)b[i];
// lower case compare
if (ac >= (uint8)'A' && ac <= (uint8)'Z')
ac -= ((uint8)'A' - (uint8)'a');
if (bc >= (uint8)'A' && bc <= (uint8)'Z')
bc -= ((uint8)'A' - (uint8)'a');
if (ac != bc)
return false;
}
return true;
}
FSCMountPathNode* s_fscRootNodePerPrio[FSC_PRIORITY_COUNT]{};
std::recursive_mutex s_fscMutex;
#define fscEnter() s_fscMutex.lock();
#define fscLeave() s_fscMutex.unlock();
FSCMountPathNode* fsc_lookupPathVirtualNode(const char* path, sint32 priority = FSC_PRIORITY_BASE);
void fsc_reset()
{
// delete existing nodes
for (auto& itr : s_fscRootNodePerPrio)
{
delete itr;
itr = nullptr;
}
// init root node for each priority
for (sint32 i = 0; i < FSC_PRIORITY_COUNT; i++)
s_fscRootNodePerPrio[i] = new FSCMountPathNode(nullptr);
}
/*
* Creates a node chain for the given mount path. Returns the bottom node.
* If the path already exists for the given priority, NULL is returned (we can't mount two devices to the same path with the same priority)
* But we can map devices to subdirectories. Something like this is possible:
* /vol/content -> Map to WUD (includes all subdirectories except /data, which is handled by the entry below. This exclusion rule applies only if the priority of both mount entries is the same)
* /vol/content/data -> Map to HostFS
* If overlapping paths with different priority are created, then the higher priority one will be checked first
*/
FSCMountPathNode* fsc_createMountPath(const FSCPath& mountPath, sint32 priority)
{
cemu_assert(priority >= 0 && priority < FSC_PRIORITY_COUNT);
fscEnter();
FSCMountPathNode* nodeParent = s_fscRootNodePerPrio[priority];
for (size_t i=0; i< mountPath.GetNodeCount(); i++)
{
// search for subdirectory
FSCMountPathNode* nodeSub = nullptr; // set if we found a subnode with a matching name, else this is used to store the new nodes
for (auto& nodeItr : nodeParent->subnodes)
{
if (mountPath.MatchNodeName(i, nodeItr->path))
{
// subnode found
nodeSub = nodeItr;
break;
}
}
if (nodeSub)
{
// traverse subnode
nodeParent = nodeSub;
continue;
}
// no matching subnode, add new entry
nodeSub = new FSCMountPathNode(nodeParent);
nodeSub->path = mountPath.GetNodeName(i);
nodeSub->priority = priority;
nodeParent->subnodes.emplace_back(nodeSub);
if (i == (mountPath.GetNodeCount() - 1))
{
// last node
fscLeave();
return nodeSub;
}
// traverse subnode
nodeParent = nodeSub;
}
// path is empty or already mounted
fscLeave();
if (mountPath.GetNodeCount() == 0)
return nodeParent;
return nullptr;
}
// Map a virtual FSC directory to a device. targetPath points to the destination base directory within the device
sint32 fsc_mount(std::string_view mountPath, std::string_view targetPath, fscDeviceC* fscDevice, void* ctx, sint32 priority)
{
cemu_assert(fscDevice);
std::string mountPathTmp(mountPath);
// make sure the target path ends with a slash
std::string targetPathWithSlash(targetPath);
if (!targetPathWithSlash.empty() && (targetPathWithSlash.back() != '/' && targetPathWithSlash.back() != '\\'))
targetPathWithSlash.push_back('/');
FSCPath parsedMountPath(mountPathTmp);
// register path
fscEnter();
FSCMountPathNode* node = fsc_createMountPath(parsedMountPath, priority);
if( !node )
{
// path empty, invalid or already used
cemuLog_log(LogType::Force, "fsc_mount failed (virtual path: {})", mountPath);
fscLeave();
return FSC_STATUS_INVALID_PATH;
}
node->device = fscDevice;
node->ctx = ctx;
node->deviceTargetPath = std::move(targetPathWithSlash);
fscLeave();
return FSC_STATUS_OK;
}
bool fsc_unmount(std::string_view mountPath, sint32 priority)
{
std::string _tmp(mountPath);
fscEnter();
FSCMountPathNode* mountPathNode = fsc_lookupPathVirtualNode(_tmp.c_str(), priority);
if (!mountPathNode)
{
fscLeave();
return false;
}
cemu_assert(mountPathNode->priority == priority);
cemu_assert(mountPathNode->device);
// delete node
while (mountPathNode && mountPathNode->parent)
{
FSCMountPathNode* parent = mountPathNode->parent;
cemu_assert(!(!mountPathNode->subnodes.empty() && mountPathNode->device));
if (!mountPathNode->subnodes.empty())
break;
parent->subnodes.erase(std::find(parent->subnodes.begin(), parent->subnodes.end(), mountPathNode));
delete mountPathNode;
mountPathNode = parent;
}
fscLeave();
return true;
}
void fsc_unmountAll()
{
fscEnter();
fsc_reset();
fscLeave();
}
// lookup virtual path and find mounted device and relative device directory
bool fsc_lookupPath(const char* path, std::string& devicePathOut, fscDeviceC** fscDeviceOut, void** ctxOut, sint32 priority = FSC_PRIORITY_BASE)
{
FSCPath parsedPath(path);
FSCMountPathNode* nodeParent = s_fscRootNodePerPrio[priority];
size_t i;
fscEnter();
for (i = 0; i < parsedPath.GetNodeCount(); i++)
{
// search for subdirectory
FSCMountPathNode* nodeSub = nullptr;
for(auto& nodeItr : nodeParent->subnodes)
{
if (parsedPath.MatchNodeName(i, nodeItr->path))
{
nodeSub = nodeItr;
break;
}
}
if (nodeSub)
{
nodeParent = nodeSub;
continue;
}
// no matching subnode
break;
}
// if the found node is not a device mount point, then travel back towards the root until we find one
while (nodeParent)
{
if (nodeParent->device)
{
devicePathOut = nodeParent->deviceTargetPath;
for (size_t f = i; f < parsedPath.GetNodeCount(); f++)
{
auto nodeName = parsedPath.GetNodeName(f);
devicePathOut.append(nodeName);
if (f < (parsedPath.GetNodeCount() - 1))
devicePathOut.push_back('/');
}
*fscDeviceOut = nodeParent->device;
*ctxOut = nodeParent->ctx;
fscLeave();
return true;
}
nodeParent = nodeParent->parent;
i--;
}
fscLeave();
return false;
}
// lookup path and find virtual device node
FSCMountPathNode* fsc_lookupPathVirtualNode(const char* path, sint32 priority)
{
FSCPath parsedPath(path);
FSCMountPathNode* nodeCurrentDir = s_fscRootNodePerPrio[priority];
fscEnter();
for (size_t i = 0; i < parsedPath.GetNodeCount(); i++)
{
// search for subdirectory
FSCMountPathNode* nodeSub = nullptr;
for (auto& nodeItr : nodeCurrentDir->subnodes)
{
if (parsedPath.MatchNodeName(i, nodeItr->path))
{
nodeSub = nodeItr;
break;
}
}
if (nodeSub)
{
// traverse subdirectory
nodeCurrentDir = nodeSub;
continue;
}
fscLeave();
return nullptr;
}
fscLeave();
return nodeCurrentDir;
}
// this wraps multiple iterated directories from different devices into one unified virtual representation
class FSCVirtualFileDirectoryIterator : public FSCVirtualFile
{
public:
sint32 fscGetType() override
{
return FSC_TYPE_DIRECTORY;
}
FSCVirtualFileDirectoryIterator(std::string_view path, std::span<FSCVirtualFile*> mappedFolders)
: m_path(path), m_folders(mappedFolders.begin(), mappedFolders.end())
{
dirIterator = nullptr;
}
~FSCVirtualFileDirectoryIterator()
{
// dirIterator is deleted in base constructor
for (auto& itr : m_folders)
delete itr;
}
bool fscDirNext(FSCDirEntry* dirEntry) override
{
if (!dirIterator)
{
// lazily populate list only if directory is actually iterated
PopulateIterationList();
cemu_assert_debug(dirIterator);
}
if (dirIterator->index >= dirIterator->dirEntries.size())
return false;
*dirEntry = dirIterator->dirEntries[dirIterator->index];
dirIterator->index++;
return true;
}
void addUniqueDirEntry(const FSCDirEntry& dirEntry)
{
// skip if already in list
for (auto& itr : dirIterator->dirEntries)
{
if (FSA_CompareNodeName(dirEntry.path, itr.path))
return;
}
dirIterator->dirEntries.emplace_back(dirEntry);
}
private:
void PopulateIterationList()
{
cemu_assert_debug(!dirIterator);
dirIterator = new FSCVirtualFile::FSCDirIteratorState();
FSCDirEntry dirEntry;
fscEnter();
for (auto& itr : m_folders)
{
while (itr->fscDirNext(&dirEntry))
addUniqueDirEntry(dirEntry);
}
for (sint32 prio = FSC_PRIORITY_COUNT - 1; prio >= 0; prio--)
{
FSCMountPathNode* nodeVirtualPath = fsc_lookupPathVirtualNode(m_path.c_str(), prio);
if (nodeVirtualPath)
{
for (auto& itr : nodeVirtualPath->subnodes)
{
dirEntry = {};
dirEntry.isDirectory = true;
strncpy(dirEntry.path, itr->path.c_str(), sizeof(dirEntry.path) - 1);
dirEntry.path[sizeof(dirEntry.path) - 1] = '\0';
dirEntry.fileSize = 0;
addUniqueDirEntry(dirEntry);
}
}
}
fscLeave();
}
private:
std::string m_path;
std::vector<FSCVirtualFile*> m_folders; // list of all folders mapped to the same directory (at different priorities)
};
// Open file or directory from virtual file system
FSCVirtualFile* fsc_open(const char* path, FSC_ACCESS_FLAG accessFlags, sint32* fscStatus, sint32 maxPriority)
{
cemu_assert_debug(HAS_FLAG(accessFlags, FSC_ACCESS_FLAG::OPEN_FILE) || HAS_FLAG(accessFlags, FSC_ACCESS_FLAG::OPEN_DIR)); // must open either file or directory
FSCVirtualFile* dirList[FSC_PRIORITY_COUNT];
uint8 dirListCount = 0;
std::string devicePath;
fscDeviceC* fscDevice = NULL;
*fscStatus = FSC_STATUS_UNDEFINED;
void* ctx;
fscEnter();
for (sint32 prio = maxPriority; prio >= 0; prio--)
{
if (fsc_lookupPath(path, devicePath, &fscDevice, &ctx, prio))
{
FSCVirtualFile* fscVirtualFile = fscDevice->fscDeviceOpenByPath(devicePath, accessFlags, ctx, fscStatus);
if (fscVirtualFile)
{
if (fscVirtualFile->fscGetType() == FSC_TYPE_DIRECTORY)
{
cemu_assert_debug(HAS_FLAG(accessFlags, FSC_ACCESS_FLAG::OPEN_DIR));
// collect all folders
dirList[dirListCount] = fscVirtualFile;
dirListCount++;
}
else
{
// return first found file
cemu_assert_debug(HAS_FLAG(accessFlags, FSC_ACCESS_FLAG::OPEN_FILE));
fscLeave();
return fscVirtualFile;
}
}
}
}
// for directories we create a virtual representation of the enumerated files of all priorities as well as the FSC folder structure itself
if (HAS_FLAG(accessFlags, FSC_ACCESS_FLAG::OPEN_DIR))
{
// create a virtual directory VirtualFile that represents all the mounted folders as well as the virtual FSC folder structure
bool folderExists = dirListCount > 0;
for (sint32 prio = FSC_PRIORITY_COUNT - 1; prio >= 0; prio--)
{
if (folderExists)
break;
folderExists |= (fsc_lookupPathVirtualNode(path, prio) != 0);
}
if (folderExists)
{
FSCVirtualFileDirectoryIterator* dirIteratorFile = new FSCVirtualFileDirectoryIterator(path, { dirList, dirListCount});
*fscStatus = FSC_STATUS_OK;
fscLeave();
return dirIteratorFile;
}
}
else
{
cemu_assert_debug(dirListCount == 0);
}
fscLeave();
*fscStatus = FSC_STATUS_FILE_NOT_FOUND;
return nullptr;
}
/*
* Open file using virtual path
*/
FSCVirtualFile* fsc_openDirIterator(const char* path, sint32* fscStatus)
{
return fsc_open(path, FSC_ACCESS_FLAG::OPEN_DIR, fscStatus);
}
/*
* Iterate next node in directory
* Returns false if there is no node left
*/
bool fsc_nextDir(FSCVirtualFile* fscFile, FSCDirEntry* dirEntry)
{
fscEnter();
if (fscFile->fscGetType() != FSC_TYPE_DIRECTORY)
{
cemu_assert_suspicious();
fscLeave();
return false;
}
bool r = fscFile->fscDirNext(dirEntry);
fscLeave();
return r;
}
/*
* Create directory
*/
bool fsc_createDir(const char* path, sint32* fscStatus)
{
fscDeviceC* fscDevice = NULL;
*fscStatus = FSC_STATUS_UNDEFINED;
void* ctx;
std::string devicePath;
fscEnter();
if( fsc_lookupPath(path, devicePath, &fscDevice, &ctx) )
{
sint32 status = fscDevice->fscDeviceCreateDir(devicePath, ctx, fscStatus);
fscLeave();
return status;
}
fscLeave();
return false;
}
/*
* Rename file or directory
*/
bool fsc_rename(const char* srcPath, const char* dstPath, sint32* fscStatus)
{
std::string srcDevicePath;
std::string dstDevicePath;
void* srcCtx;
void* dstCtx;
fscDeviceC* fscSrcDevice = NULL;
fscDeviceC* fscDstDevice = NULL;
*fscStatus = FSC_STATUS_UNDEFINED;
if( fsc_lookupPath(srcPath, srcDevicePath, &fscSrcDevice, &srcCtx) && fsc_lookupPath(dstPath, dstDevicePath, &fscDstDevice, &dstCtx) )
{
if( fscSrcDevice == fscDstDevice )
return fscSrcDevice->fscDeviceRename(srcDevicePath, dstDevicePath, srcCtx, fscStatus);
}
return false;
}
/*
* Delete file or subdirectory
*/
bool fsc_remove(const char* path, sint32* fscStatus)
{
std::string devicePath;
fscDeviceC* fscDevice = NULL;
*fscStatus = FSC_STATUS_UNDEFINED;
void* ctx;
if( fsc_lookupPath(path, devicePath, &fscDevice, &ctx) )
{
return fscDevice->fscDeviceRemoveFileOrDir(devicePath, ctx, fscStatus);
}
return false;
}
/*
* Close file handle
*/
void fsc_close(FSCVirtualFile* fscFile)
{
fscEnter();
delete fscFile;
fscLeave();
}
/*
* Return size of file
*/
uint32 fsc_getFileSize(FSCVirtualFile* fscFile)
{
return (uint32)fscFile->fscQueryValueU64(FSC_QUERY_SIZE);
}
/*
* Return file position
*/
uint32 fsc_getFileSeek(FSCVirtualFile* fscFile)
{
return (uint32)fscFile->fscGetSeek();
}
/*
* Set file seek
* For writable files the seek pointer can be set past the end of the file
*/
void fsc_setFileSeek(FSCVirtualFile* fscFile, uint32 newSeek)
{
fscEnter();
uint32 fileSize = fsc_getFileSize(fscFile);
if (fsc_isWritable(fscFile) == false)
newSeek = std::min(newSeek, fileSize);
fscFile->fscSetSeek((uint64)newSeek);
fscLeave();
}
// set file length
void fsc_setFileLength(FSCVirtualFile* fscFile, uint32 newEndOffset)
{
fscEnter();
uint32 fileSize = fsc_getFileSize(fscFile);
if (!fsc_isWritable(fscFile))
{
cemuLog_force("TruncateFile called on read-only file");
}
else
{
fscFile->fscSetFileLength((uint64)newEndOffset);
}
fscLeave();
}
/*
* Returns true if the file object is a directory
*/
bool fsc_isDirectory(FSCVirtualFile* fscFile)
{
return fscFile->fscGetType() == FSC_TYPE_DIRECTORY;
}
/*
* Returns true if the file object is a file
*/
bool fsc_isFile(FSCVirtualFile* fscFile)
{
return fscFile->fscGetType() == FSC_TYPE_FILE;
}
/*
* Returns true if the file is writable
*/
bool fsc_isWritable(FSCVirtualFile* fscFile)
{
return fscFile->fscQueryValueU64(FSC_QUERY_WRITEABLE) != 0;
}
/*
* Read data from file
* Returns number of bytes successfully read
*/
uint32 fsc_readFile(FSCVirtualFile* fscFile, void* buffer, uint32 size)
{
fscEnter();
uint32 fscStatus = fscFile->fscReadData(buffer, size);
fscLeave();
return fscStatus;
}
/*
* Write data to file
* Returns number of bytes successfully written
*/
uint32 fsc_writeFile(FSCVirtualFile* fscFile, void* buffer, uint32 size)
{
fscEnter();
if (fsc_isWritable(fscFile) == false)
{
fscLeave();
return 0;
}
uint32 fscStatus = fscFile->fscWriteData(buffer, size);
fscLeave();
return fscStatus;
}
// helper function to load a file into memory
uint8* fsc_extractFile(const char* path, uint32* fileSize, sint32 maxPriority)
{
fscDeviceC* fscDevice = nullptr;
sint32 fscStatus = FSC_STATUS_UNDEFINED;
fscEnter();
FSCVirtualFile* fscFile = fsc_open(path, FSC_ACCESS_FLAG::OPEN_FILE | FSC_ACCESS_FLAG::READ_PERMISSION, &fscStatus, maxPriority);
if( !fscFile )
{
*fileSize = 0;
fscLeave();
return nullptr;
}
uint32 fscFileSize = fsc_getFileSize(fscFile);
*fileSize = fscFileSize;
uint8* fileMem = (uint8*)malloc(fscFileSize);
if( fsc_readFile(fscFile, fileMem, fscFileSize) != fscFileSize )
{
free(fileMem);
fsc_close(fscFile);
*fileSize = 0;
fscLeave();
return nullptr;
}
fsc_close(fscFile);
fscLeave();
return fileMem;
}
std::optional<std::vector<uint8>> fsc_extractFile(const char* path, sint32 maxPriority)
{
fscDeviceC* fscDevice = nullptr;
sint32 fscStatus = FSC_STATUS_UNDEFINED;
fscEnter();
FSCVirtualFile* fscFile = fsc_open(path, FSC_ACCESS_FLAG::OPEN_FILE | FSC_ACCESS_FLAG::READ_PERMISSION, &fscStatus, maxPriority);
if (!fscFile)
{
fscLeave();
return std::nullopt;
}
std::vector<uint8> fileData;
uint32 fscFileSize = fsc_getFileSize(fscFile);
fileData.resize(fscFileSize);
uint32 readOffset = 0;
while (readOffset < fscFileSize)
{
uint32 stepReadSize = std::min(fscFileSize - readOffset, (uint32)1024 * 1024 * 32);
uint32 numBytesRead = fsc_readFile(fscFile, fileData.data() + readOffset, stepReadSize);
if (numBytesRead != stepReadSize)
{
fsc_close(fscFile);
fscLeave();
return std::nullopt;
}
readOffset += stepReadSize;
}
fsc_close(fscFile);
fscLeave();
return fileData;
}
// helper function to check if a file exists
bool fsc_doesFileExist(const char* path, sint32 maxPriority)
{
fscDeviceC* fscDevice = nullptr;
sint32 fscStatus = FSC_STATUS_UNDEFINED;
fscEnter();
FSCVirtualFile* fscFile = fsc_open(path, FSC_ACCESS_FLAG::OPEN_FILE, &fscStatus, maxPriority);
if (!fscFile)
{
fscLeave();
return false;
}
fsc_close(fscFile);
fscLeave();
return true;
}
// helper function to check if a directory exists
bool fsc_doesDirectoryExist(const char* path, sint32 maxPriority)
{
fscDeviceC* fscDevice = nullptr;
sint32 fscStatus = FSC_STATUS_UNDEFINED;
fscEnter();
FSCVirtualFile* fscFile = fsc_open(path, FSC_ACCESS_FLAG::OPEN_DIR, &fscStatus, maxPriority);
if (!fscFile)
{
fscLeave();
return false;
}
fsc_close(fscFile);
fscLeave();
return true;
}
// initialize Cemu's virtual filesystem
void fsc_init()
{
fsc_reset();
}