diff --git a/cmake/aurora_dvd.cmake b/cmake/aurora_dvd.cmake index c95f72e5fe..c7e8eacf94 100644 --- a/cmake/aurora_dvd.cmake +++ b/cmake/aurora_dvd.cmake @@ -1,10 +1,9 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/AuroraNodProvider.cmake) -add_library(aurora_dvd STATIC lib/dolphin/dvd/dvd.cpp) +add_library(aurora_dvd STATIC lib/dolphin/dvd/dvd.cpp lib/dolphin/dvd/dvd.hpp lib/dolphin/dvd/fst.cpp) add_library(aurora::dvd ALIAS aurora_dvd) set_target_properties(aurora_dvd PROPERTIES FOLDER "aurora") target_compile_definitions(aurora_dvd PUBLIC AURORA TARGET_PC) target_include_directories(aurora_dvd PUBLIC include) -target_link_libraries(aurora_dvd PUBLIC nod::nod ${AURORA_SDL3_TARGET}) -target_link_libraries(aurora_dvd PRIVATE fmt::fmt) +target_link_libraries(aurora_dvd PUBLIC nod::nod fmt::fmt ${AURORA_SDL3_TARGET}) diff --git a/include/aurora/dvd.h b/include/aurora/dvd.h index 4a7f53b834..e6ae8620ce 100644 --- a/include/aurora/dvd.h +++ b/include/aurora/dvd.h @@ -20,6 +20,105 @@ bool aurora_dvd_open(const char* disc_path); */ void aurora_dvd_close(void); +/** + * OVERLAY FILES! + * + * Overlay files allow you to replace and add ("overlay") files that are present in the loaded DVD. + * The way this works is pretty simple: you provide some callbacks and a list of files. + * When an overlaid file gets read, your callbacks get called instead of pulling from the underlying DVD. + * + * Original disc EntryNums are not touched by the overlay system. New files and directories + * are assigned stable EntryNums by Aurora. + */ + +/** + * \brief A single file to be overlaid over the DVD files. + * + * You do not need to concern yourself with providing entries for directories. They are automatically merged + * and created where necessary. + */ +typedef struct AuroraOverlayFile { + /** + * \brief Absolute file path of this file. + * + * Must be in the form "/foo/bar/baz.txt", note the leading slash. + */ + const char* fileName; + + /** + * \brief Userdata pointer that will be passed to the callback when this file is opened. + */ + void* userData; + + /** + * \brief Size of this file, in bytes. + * + * While this is of type size_t, file sizes larger than u32 are not currently supported. + */ + size_t size; + +} AuroraOverlayFile; + +/** + * \brief Callbacks to implement overlay files. + * + * Callbacks may be ran from any thread at any time. Make sure they're thread safe! + */ +typedef struct AuroraOverlayCallbacks { + /** + * Called when a new file has been opened. + * + * Returns an opaque handle that will be passed to the remaining callbacks. Receives the userdata specified in + * the AuroraOverlayFile. + */ + void* (*open)(void* userdata); + + /** + * Close a file handle previously returned from the open callback. + */ + void (*close)(void* handle); + + /** + * Read data from a file handle. + * + * Returns the amount of data read, or -1 on error. + */ + int64_t (*read)(void* handle, uint8_t* buf, size_t len); + + /** + * Seek to a position in a file handle. + * + * Returns the resulting position, or -1 on error. + */ + int64_t (*seek)(void* handle, int64_t offset, int32_t whence); +} AuroraOverlayCallbacks; + +/** + * \brief Specify callbacks for overlaid files. + */ +void aurora_dvd_overlay_callbacks(const AuroraOverlayCallbacks* callbacks); + +/** + * \brief Specify a set of overlay files to be used by the DVD layer. + * + * Calling this function immediately applies the new files and rebuilds the FST. This is not thread safe. + * It is best you only call this once on startup, before the game's code has started. + * + * This function must be called *after* aurora_dvd_overlay_callbacks. + * + * @param files Array of AuroraOverlayFiles, one for every file being overlaid. + * @param nFiles Amount of files in the array. + * @param outEntryNums Optional output array receiving one EntryNum per input file. Unaccepted files receive -1. + */ +void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles, s32* outEntryNums); + +/** + * \brief Gets the amount of FST entries present on the loaded game disc. + * + * This does not take overlay files into account. + */ +s32 aurora_dvd_base_entry_count(); + #ifdef __cplusplus } #endif diff --git a/lib/dolphin/dvd/dvd.cpp b/lib/dolphin/dvd/dvd.cpp index 9a054b5778..863ecb854b 100644 --- a/lib/dolphin/dvd/dvd.cpp +++ b/lib/dolphin/dvd/dvd.cpp @@ -1,5 +1,7 @@ #include #include + +#include #include #include #include @@ -8,36 +10,81 @@ #include #include #include +#include #include #include #include +#include "dvd.hpp" + #include "../../internal.hpp" +using namespace aurora::dvd::impl; + +namespace aurora::dvd::impl { + NodHandle* s_partition = nullptr; + std::vector s_fstEntries; + // Map from public FST entryNums (matching base disc, Aurora-assigned for new overlay entries) + // To the current FST indexes (that we use for navigating the tree). + // Unfilled spots are given the k_invalidFstEntry value. + std::vector s_entryNumToFstIndex; + s32 s_baseEntryCount = 0; + FstIndex s_currentDir = 0; + std::string s_currentPath = "/"; + BOOL s_autoInvalidation = FALSE; + BOOL s_autoFatalMessaging = FALSE; + DVDDiskID s_diskID = {}; + DVDLowCallback s_resetCoverCallback = nullptr; + bool s_initialized = false; + bool s_overlayCallbacksSet = false; + AuroraOverlayCallbacks s_overlayCallbacks; + std::mutex s_fstLock; +} + namespace { -struct FSTEntry { - std::string name; - bool isDir = false; - u32 parent = 0; - u32 nextOrLength = 0; +class CommandDataBase { +public: + virtual ~CommandDataBase() = default; + virtual int64_t read(uint8_t *buf, size_t len) = 0; + virtual int64_t seek(int64_t offset, int32_t whence) = 0; +}; + +class CommandDataNod final : public CommandDataBase { +public: + NodHandle* handle; + explicit CommandDataNod(NodHandle* nod_handle) : handle(nod_handle) { } + ~CommandDataNod() override { + nod_free(handle); + } + + int64_t read(uint8_t* buf, size_t len) override { + return nod_read(handle, buf, len); + } + + int64_t seek(int64_t offset, int32_t whence) override { + return nod_seek(handle, offset, whence); + } }; -struct IterateContext { - std::vector* entries = nullptr; - std::vector> dirStack; +class CommandDataOverlay final : public CommandDataBase { +public: + void* handle; + explicit CommandDataOverlay(void* handle) : handle(handle) { } + ~CommandDataOverlay() override { + s_overlayCallbacks.close(handle); + } + + int64_t read(uint8_t* buf, size_t len) override { + return s_overlayCallbacks.read(handle, buf, len); + } + + int64_t seek(int64_t offset, int32_t whence) override { + return s_overlayCallbacks.seek(handle, offset, whence); + } }; -NodHandle* s_disc = nullptr; -NodHandle* s_partition = nullptr; -std::vector s_fstEntries; -s32 s_currentDir = 0; -std::string s_currentPath = "/"; -BOOL s_autoInvalidation = FALSE; -BOOL s_autoFatalMessaging = FALSE; -DVDDiskID s_diskID = {}; -DVDLowCallback s_resetCoverCallback = nullptr; -bool s_initialized = false; +CommandDataNod* s_disc; void clearState() { if (s_partition != nullptr) { @@ -45,17 +92,26 @@ void clearState() { s_partition = nullptr; } if (s_disc != nullptr) { - nod_free(s_disc); + delete s_disc; s_disc = nullptr; } s_fstEntries.clear(); + s_entryNumToFstIndex.clear(); + s_baseEntryCount = 0; s_currentDir = 0; s_currentPath = "/"; s_diskID = {}; s_initialized = false; } -bool isValidEntryIndex(s32 entry) { return entry >= 0 && static_cast(entry) < s_fstEntries.size(); } +bool isValidEntryNum(s32 entry) { + return entry >= 0 && static_cast(entry) < s_entryNumToFstIndex.size() && + s_entryNumToFstIndex[entry] != k_invalidFstEntry; +} + +bool isValidFstIndex(FstIndex entry) { + return entry >= 0 && static_cast(entry) < s_fstEntries.size(); +} bool isAligned(const void* addr, uintptr_t align) { return (reinterpret_cast(addr) & (align - 1)) == 0; @@ -102,80 +158,12 @@ void sdlStreamClose(void* userData) { SDL_CloseIO(io); } -u32 fstCallback(u32 index, NodNodeKind kind, const char* name, u32 size, void* userData) { - auto* ctx = static_cast(userData); - - while (!ctx->dirStack.empty() && index >= ctx->dirStack.back().second) { - ctx->dirStack.pop_back(); - } - - if (ctx->entries->size() <= index) { - ctx->entries->resize(index + 1); - } - - FSTEntry& entry = (*ctx->entries)[index]; - entry.name = (name != nullptr) ? name : ""; - entry.isDir = (kind == NOD_NODE_KIND_DIRECTORY); - entry.parent = ctx->dirStack.empty() ? 0 : ctx->dirStack.back().first; - entry.nextOrLength = size; - - if (entry.isDir) { - ctx->dirStack.emplace_back(index, size); - } - - return index + 1; -} - -bool rebuildFST() { - if (s_partition == nullptr) { - return false; - } - - s_fstEntries.clear(); - IterateContext ctx{}; - ctx.entries = &s_fstEntries; - nod_partition_iterate_fst(s_partition, fstCallback, &ctx); - - if (s_fstEntries.empty()) { - FSTEntry root; - root.name = ""; - root.isDir = true; - root.parent = 0; - root.nextOrLength = 1; - s_fstEntries.push_back(std::move(root)); - } - - s_fstEntries[0].name.clear(); - s_fstEntries[0].isDir = true; - s_fstEntries[0].parent = 0; - if (s_fstEntries[0].nextOrLength < 1 || s_fstEntries[0].nextOrLength > s_fstEntries.size()) { - s_fstEntries[0].nextOrLength = static_cast(s_fstEntries.size()); - } - return true; -} - bool nameEqualsIgnoreCase(const std::string& lhs, const char* rhs, size_t rhsLen) { - if (lhs.size() != rhsLen) { - return false; - } - for (size_t i = 0; i < rhsLen; ++i) { - char lc = lhs[i]; - char rc = rhs[i]; - if (lc >= 'a' && lc <= 'z') { - lc = static_cast(lc - 'a' + 'A'); - } - if (rc >= 'a' && rc <= 'z') { - rc = static_cast(rc - 'a' + 'A'); - } - if (lc != rc) { - return false; - } - } - return true; + return aurora::dvd::impl::nameEqualsIgnoreCase(lhs, std::string_view(rhs, rhsLen)); } -s32 findInDir(s32 dirEntry, const char* name, size_t nameLen) { - if (!isValidEntryIndex(dirEntry) || !s_fstEntries[dirEntry].isDir) { +FstIndex findInDir(FstIndex dirEntry, const char* name, size_t nameLen) { + if (!isValidFstIndex(dirEntry) || !s_fstEntries[dirEntry].isDir) { return -1; } @@ -183,7 +171,7 @@ s32 findInDir(s32 dirEntry, const char* name, size_t nameLen) { u32 i = static_cast(dirEntry) + 1; while (i < childEnd && i < s_fstEntries.size()) { if (nameEqualsIgnoreCase(s_fstEntries[i].name, name, nameLen)) { - return static_cast(i); + return static_cast(i); } if (s_fstEntries[i].isDir) { @@ -196,16 +184,16 @@ s32 findInDir(s32 dirEntry, const char* name, size_t nameLen) { return -1; } -std::string buildDirPath(s32 entryNum) { - if (entryNum <= 0 || !isValidEntryIndex(entryNum)) { +std::string buildDirPath(FstIndex entryNum) { + if (entryNum <= 0 || !isValidFstIndex(entryNum)) { return "/"; } std::vector parts; - s32 cur = entryNum; - while (cur > 0 && isValidEntryIndex(cur)) { + FstIndex cur = entryNum; + while (cur > 0 && isValidFstIndex(cur)) { parts.push_back(s_fstEntries[cur].name); - s32 parent = static_cast(s_fstEntries[cur].parent); + auto parent = s_fstEntries[cur].parent; if (parent == cur) { break; } @@ -220,7 +208,7 @@ std::string buildDirPath(s32 entryNum) { return out; } -s32 readFromHandle(NodHandle* handle, void* out, s32 length, s32 offset, u32* transferredOut) { +s32 readFromHandle(CommandDataBase* handle, void* out, s32 length, s32 offset, u32* transferredOut) { if (transferredOut != nullptr) { *transferredOut = 0; } @@ -230,7 +218,7 @@ s32 readFromHandle(NodHandle* handle, void* out, s32 length, s32 offset, u32* tr if (length == 0) { return 0; } - if (nod_seek(handle, offset, 0) < 0) { + if (handle->seek(offset, 0) < 0) { return DVD_RESULT_FATAL_ERROR; } @@ -238,7 +226,7 @@ s32 readFromHandle(NodHandle* handle, void* out, s32 length, s32 offset, u32* tr s32 totalRead = 0; s32 remaining = length; while (remaining > 0) { - const int64_t read = nod_read(handle, writePtr + totalRead, static_cast(remaining)); + const int64_t read = handle->read(writePtr + totalRead, static_cast(remaining)); if (read < 0) { return DVD_RESULT_FATAL_ERROR; } @@ -277,9 +265,9 @@ bool isCommandBlockIdle(const DVDCommandBlock* block) { return block != nullptr && block->state != DVD_STATE_BUSY && block->state != DVD_STATE_WAITING; } -NodHandle* getCommandHandle(DVDCommandBlock* block) { +CommandDataBase* getCommandHandle(DVDCommandBlock* block) { if (block != nullptr && block->userData != nullptr) { - return static_cast(block->userData); + return static_cast(block->userData); } return s_disc; } @@ -360,20 +348,23 @@ bool aurora_dvd_open(const char* disc_path) { .preloader_threads = 1, }; - NodResult result = nod_disc_open_stream(&stream, &options, &s_disc); - if (result != NOD_RESULT_OK || s_disc == nullptr) { + NodHandle* discHandle; + NodResult result = nod_disc_open_stream(&stream, &options, &discHandle); + if (result != NOD_RESULT_OK || discHandle == nullptr) { clearState(); return false; } - result = nod_disc_open_partition_kind(s_disc, NOD_PARTITION_KIND_DATA, nullptr, &s_partition); + s_disc = new CommandDataNod(discHandle); + + result = nod_disc_open_partition_kind(s_disc->handle, NOD_PARTITION_KIND_DATA, nullptr, &s_partition); if (result != NOD_RESULT_OK || s_partition == nullptr) { clearState(); return false; } NodDiscHeader header{}; - if (nod_disc_header(s_disc, &header) == NOD_RESULT_OK) { + if (nod_disc_header(s_disc->handle, &header) == NOD_RESULT_OK) { std::memcpy(s_diskID.gameName, header.game_id, sizeof(s_diskID.gameName)); std::memcpy(s_diskID.company, header.game_id + sizeof(s_diskID.gameName), sizeof(s_diskID.company)); s_diskID.diskNumber = header.disc_num; @@ -443,8 +434,8 @@ int DVDSeekAbsAsyncPrio(DVDCommandBlock* block, s32 offset, DVDCBCallback callba ASSERTMSGLINE(0x7AC, !(offset & (4 - 1)), "DVDSeekAbs(): offset must be a multiple of 4."); beginCommand(block, DVD_COMMAND_SEEK, nullptr, 0, static_cast(offset), callback); - NodHandle* handle = getCommandHandle(block); - const int64_t seek = handle != nullptr ? nod_seek(handle, static_cast(offset), 0) : -1; + auto handle = getCommandHandle(block); + const int64_t seek = handle != nullptr ? handle->seek(static_cast(offset), 0) : -1; const s32 result = (seek < 0) ? DVD_RESULT_FATAL_ERROR : DVD_RESULT_GOOD; finishCommand(block, result, 0); if (callback != nullptr) { @@ -685,11 +676,13 @@ int DVDSetAutoFatalMessaging(BOOL enable) { } s32 DVDConvertPathToEntrynum(const char* pathPtr) { + std::lock_guard lock(s_fstLock); + if (!s_initialized || pathPtr == nullptr || s_fstEntries.empty()) { return -1; } - s32 current = 0; + FstIndex current = 0; const char* p = pathPtr; if (*p == '/') { ++p; @@ -705,7 +698,7 @@ s32 DVDConvertPathToEntrynum(const char* pathPtr) { break; } - if (!isValidEntryIndex(current) || !s_fstEntries[current].isDir) { + if (!isValidFstIndex(current) || !s_fstEntries[current].isDir) { return -1; } @@ -720,7 +713,7 @@ s32 DVDConvertPathToEntrynum(const char* pathPtr) { } else if (compLen == 2 && p[0] == '.' && p[1] == '.') { current = static_cast(s_fstEntries[current].parent); } else { - const s32 found = findInDir(current, p, compLen); + const FstIndex found = findInDir(current, p, compLen); if (found < 0) { return -1; } @@ -729,28 +722,46 @@ s32 DVDConvertPathToEntrynum(const char* pathPtr) { p = compEnd; } - return current; + assert(isValidFstIndex(current)); + return s_fstEntries[current].origEntryNum; } BOOL DVDFastOpen(s32 entrynum, DVDFileInfo* fileInfo) { - if (!s_initialized || fileInfo == nullptr || !isValidEntryIndex(entrynum) || s_partition == nullptr) { + std::lock_guard lock(s_fstLock); + + if (!s_initialized || fileInfo == nullptr || !isValidEntryNum(entrynum) || s_partition == nullptr) { return FALSE; } - if (s_fstEntries[entrynum].isDir) { + + const auto fstIndex = s_entryNumToFstIndex[entrynum]; + assert(fstIndex >= 0); + + const auto& entry = s_fstEntries[fstIndex]; + if (entry.isDir) { return FALSE; } std::memset(fileInfo, 0, sizeof(*fileInfo)); fileInfo->startAddr = 0; - fileInfo->length = s_fstEntries[entrynum].nextOrLength; + fileInfo->length = entry.nextOrLength; - NodHandle* handle = nullptr; - NodResult result = nod_partition_open_file(s_partition, static_cast(entrynum), &handle); - if (result != NOD_RESULT_OK || handle == nullptr) { - return FALSE; + if (entry.isOverlay) { + const auto handle = s_overlayCallbacks.open(entry.overlayData); + if (!handle) { + return FALSE; + } + + fileInfo->cb.userData = new CommandDataOverlay(handle); + } else { + NodHandle* handle = nullptr; + NodResult result = nod_partition_open_file(s_partition, entry.origEntryNum, &handle); + if (result != NOD_RESULT_OK || handle == nullptr) { + return FALSE; + } + + fileInfo->cb.userData = new CommandDataNod(handle); } - fileInfo->cb.userData = handle; fileInfo->cb.state = DVD_STATE_END; return TRUE; } @@ -768,7 +779,7 @@ BOOL DVDClose(DVDFileInfo* fileInfo) { return FALSE; } if (fileInfo->cb.userData != nullptr) { - nod_free(static_cast(fileInfo->cb.userData)); + delete static_cast(fileInfo->cb.userData); fileInfo->cb.userData = nullptr; } fileInfo->cb.state = DVD_STATE_END; @@ -788,11 +799,20 @@ BOOL DVDGetCurrentDir(char* path, u32 maxlen) { BOOL DVDChangeDir(const char* dirName) { s32 entry = DVDConvertPathToEntrynum(dirName); - if (!isValidEntryIndex(entry) || !s_fstEntries[entry].isDir) { + + std::lock_guard lock(s_fstLock); + + if (!isValidEntryNum(entry)) { + return FALSE; + } + + const auto fstIndex = s_entryNumToFstIndex[entry]; + if (!s_fstEntries[fstIndex].isDir) { return FALSE; } - s_currentDir = entry; - s_currentPath = buildDirPath(entry); + + s_currentDir = fstIndex; + s_currentPath = buildDirPath(fstIndex); return TRUE; } @@ -856,12 +876,20 @@ s32 DVDGetFileInfoStatus(const DVDFileInfo* fileInfo) { } BOOL DVDFastOpenDir(s32 entrynum, DVDDir* dir) { - if (!isValidEntryIndex(entrynum) || dir == nullptr || !s_fstEntries[entrynum].isDir) { + std::lock_guard lock(s_fstLock); + + if (!isValidEntryNum(entrynum) || dir == nullptr) { return FALSE; } + + const auto fstIndex = s_entryNumToFstIndex[entrynum]; + if (!s_fstEntries[fstIndex].isDir) { + return FALSE; + } + dir->entryNum = static_cast(entrynum); - dir->location = static_cast(entrynum) + 1; - dir->next = s_fstEntries[entrynum].nextOrLength; + dir->location = static_cast(fstIndex) + 1; + dir->next = s_fstEntries[fstIndex].nextOrLength; return TRUE; } @@ -877,13 +905,16 @@ int DVDReadDir(DVDDir* dir, DVDDirEntry* dirent) { if (dir == nullptr || dirent == nullptr) { return FALSE; } + + std::lock_guard lock(s_fstLock); + if (dir->location >= dir->next || dir->location >= s_fstEntries.size()) { return FALSE; } const u32 index = dir->location; FSTEntry& entry = s_fstEntries[index]; - dirent->entryNum = index; + dirent->entryNum = static_cast(entry.origEntryNum); dirent->isDir = entry.isDir ? TRUE : FALSE; dirent->name = entry.name.empty() ? nullptr : entry.name.data(); @@ -905,7 +936,15 @@ void DVDRewindDir(DVDDir* dir) { if (dir == nullptr) { return; } - dir->location = dir->entryNum + 1; + + std::lock_guard lock(s_fstLock); + const s32 entryNum = static_cast(dir->entryNum); + if (!isValidEntryNum(entryNum)) { + return; + } + + const auto fstIndex = s_entryNumToFstIndex[entryNum]; + dir->location = static_cast(fstIndex) + 1; } void* DVDGetFSTLocation(void) { @@ -998,7 +1037,7 @@ BOOL DVDLowRead(void* addr, u32 length, u32 offset, DVDLowCallback callback) { } BOOL DVDLowSeek(u32 offset, DVDLowCallback callback) { - const int64_t seek = s_disc != nullptr ? nod_seek(s_disc, static_cast(offset), 0) : -1; + const int64_t seek = s_disc != nullptr ? s_disc->seek(static_cast(offset), 0) : -1; if (callback != nullptr) { callback(static_cast((seek >= 0) ? DVD_RESULT_GOOD : DVD_RESULT_FATAL_ERROR)); } diff --git a/lib/dolphin/dvd/dvd.hpp b/lib/dolphin/dvd/dvd.hpp new file mode 100644 index 0000000000..13cb73accf --- /dev/null +++ b/lib/dolphin/dvd/dvd.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include + +#include "../../internal.hpp" + +namespace aurora::dvd::impl { + +inline Module Log("aurora::dvd"); + +using FstIndex = s32; +constexpr s32 k_invalidFstEntry = -1; + +struct FSTEntry { + std::string name; + bool isDir = false; + FstIndex parent = 0; + u32 nextOrLength = 0; + void* overlayData = nullptr; + bool isOverlay = false; + s32 origEntryNum = 0; +}; + +struct IterateNode { + std::string name; + bool isDir; + s32 originalEntryNum; + u32 size; + void* overlayData; + bool isOverlay; + std::vector> children; + + IterateNode(std::string name, bool isDir, u32 size, s32 originalEntryNum, void* overlayData) + : name(std::move(name)), isDir(isDir), size(size), originalEntryNum(originalEntryNum), overlayData(overlayData), isOverlay(true) {} + + IterateNode(std::string name, bool isDir, u32 size, s32 originalEntryNum) + : name(std::move(name)), isDir(isDir), size(size), originalEntryNum(originalEntryNum), overlayData(nullptr), isOverlay(false) {} +}; + +struct IterateContext { + std::shared_ptr root; + std::vector, u32>> dirStack; +}; + +extern NodHandle* s_partition; +extern std::vector s_fstEntries; +// Map from public FST entryNums (matching base disc, Aurora-assigned for new overlay files) +// To the current FST indexes (that we use for navigating the tree). +// Unfilled spots are given the k_invalidFstEntry value. +extern std::vector s_entryNumToFstIndex; +extern s32 s_baseEntryCount; +extern FstIndex s_currentDir; +extern std::string s_currentPath; +extern BOOL s_autoInvalidation; +extern BOOL s_autoFatalMessaging; +extern DVDDiskID s_diskID; +extern DVDLowCallback s_resetCoverCallback; +extern bool s_initialized; +extern bool s_overlayCallbacksSet; +extern AuroraOverlayCallbacks s_overlayCallbacks; +extern std::mutex s_fstLock; + +bool rebuildFST(); +bool nameEqualsIgnoreCase(std::string_view lhs, std::string_view rhs); + +} diff --git a/lib/dolphin/dvd/fst.cpp b/lib/dolphin/dvd/fst.cpp new file mode 100644 index 0000000000..c416f8a1dc --- /dev/null +++ b/lib/dolphin/dvd/fst.cpp @@ -0,0 +1,353 @@ +#include "dvd.hpp" + +#include +#include +#include + +using namespace aurora::dvd::impl; + +namespace { + +struct OverlayFileEntry { + std::string fileName; + void* userData; + u32 size; + s32 entryNum = k_invalidFstEntry; + size_t sourceIndex = 0; +}; + +std::vector s_overlayFiles; +std::unordered_map s_overlayEntryNums; +s32 s_nextOverlayEntryNum = 0; +s32 s_overlayEntryNumBase = 0; + +std::string normalizeOverlayPath(std::string_view path) { + std::string normalized; + normalized.reserve(path.size()); + bool lastWasSlash = false; + for (char ch : path) { + if (ch == '\\') { + ch = '/'; + } + if (ch == '/') { + if (lastWasSlash) { + continue; + } + lastWasSlash = true; + normalized.push_back('/'); + continue; + } + lastWasSlash = false; + if (ch >= 'A' && ch <= 'Z') { + ch = static_cast(ch - 'A' + 'a'); + } + normalized.push_back(ch); + } + if (normalized.size() > 1 && normalized.back() == '/') { + normalized.pop_back(); + } + return normalized; +} + +void syncOverlayEntryAllocator() { + if (s_overlayEntryNumBase == s_baseEntryCount) { + return; + } + + s_overlayEntryNums.clear(); + s_overlayEntryNumBase = s_baseEntryCount; + s_nextOverlayEntryNum = s_baseEntryCount; +} + +s32 allocateOverlayEntryNum(std::string_view path) { + syncOverlayEntryAllocator(); + + std::string normalized = normalizeOverlayPath(path); + auto it = s_overlayEntryNums.find(normalized); + if (it != s_overlayEntryNums.end()) { + return it->second; + } + + const s32 entryNum = s_nextOverlayEntryNum++; + s_overlayEntryNums.emplace(std::move(normalized), entryNum); + return entryNum; +} + + +u32 fstCallback(u32 index, NodNodeKind kind, const char* name, u32 size, void* userData) { + auto* ctx = static_cast(userData); + + while (index >= ctx->dirStack.back().second) { + ctx->dirStack.pop_back(); + } + + const auto newEntry = std::make_shared( + name, + (kind == NOD_NODE_KIND_DIRECTORY), + size, + index); + + const auto& curDir = ctx->dirStack.back().first; + curDir->children.push_back(newEntry); + + if (newEntry->isDir) { + ctx->dirStack.emplace_back(newEntry, size); + } + + return index + 1; +} + +IterateNode* findNode(const IterateNode& node, const std::string_view name) { + for (const auto& child : node.children) { + if (nameEqualsIgnoreCase(child->name, name)) { + return child.get(); + } + } + + return nullptr; +} + +void mergeOverlayFileIntoContext(const IterateContext& context, OverlayFileEntry& overlayFile) { + IterateNode* node = context.root.get(); + std::string_view filePath = overlayFile.fileName; + std::string currentPath; + + assert(filePath.starts_with('/')); + filePath = filePath.substr(1); + while (true) { + const auto nextDelim = filePath.find('/'); + if (nextDelim == std::string_view::npos) { + break; + } + + const auto segment = filePath.substr(0, nextDelim); + filePath = filePath.substr(nextDelim + 1); + currentPath += '/'; + currentPath.append(segment); + + const auto existingNode = findNode(*node, segment); + if (existingNode) { + if (!existingNode->isDir) { + Log.error("Overlay file {} needs directory that's already a file!", overlayFile.fileName); + return; + } + + node = existingNode; + } else { + const s32 entryNum = allocateOverlayEntryNum(currentPath); + const auto newNode = std::make_shared(std::string(segment), true, 0, entryNum); + node->children.push_back(newNode); + node = newNode.get(); + } + } + + // Remainder of fileName is the actual file name, and node is the directory we're in. + + std::string fullFilePath = currentPath; + fullFilePath += '/'; + fullFilePath.append(filePath); + + auto newNode = IterateNode(std::string(filePath), false, overlayFile.size, k_invalidFstEntry, overlayFile.userData); + const auto existingNode = findNode(*node, filePath); + if (existingNode) { + if (existingNode->isDir) { + Log.error("Overlay file {} overlaps directory with same name!", overlayFile.fileName); + return; + } + + newNode.originalEntryNum = existingNode->originalEntryNum; + overlayFile.entryNum = newNode.originalEntryNum; + + // Replace existing disc entry. + *existingNode = std::move(newNode); + } else { + // Add new entry. + newNode.originalEntryNum = allocateOverlayEntryNum(fullFilePath); + overlayFile.entryNum = newNode.originalEntryNum; + + node->children.emplace_back(std::make_shared(std::move(newNode))); + } +} + +void mergeOverlayFilesIntoContext(const IterateContext& context) { + for (auto& overlayFile : s_overlayFiles) { + mergeOverlayFileIntoContext(context, overlayFile); + } +} + +void makeFstRecursive(IterateNode& node, FstIndex parent) { + if (node.originalEntryNum != k_invalidFstEntry) { + if (s_entryNumToFstIndex.size() <= node.originalEntryNum) { + s_entryNumToFstIndex.resize(node.originalEntryNum + 1, k_invalidFstEntry); + } + + auto& map = s_entryNumToFstIndex[node.originalEntryNum]; + if (map != k_invalidFstEntry) { + Log.error("File {} with entry num {} already exists in map!", node.name, node.originalEntryNum); + return; + } + + map = static_cast(s_fstEntries.size()); + } + + if (!node.isDir) { + assert(node.children.empty()); + assert(node.originalEntryNum != k_invalidFstEntry); + + s_fstEntries.emplace_back(node.name, false, parent, node.size, node.overlayData, node.isOverlay, node.originalEntryNum); + return; + } + + std::ranges::sort(node.children, [](const auto& a, const auto& b) { return a->name < b->name; }); + + const FstIndex ourIndex = static_cast(s_fstEntries.size()); + s_fstEntries.emplace_back(node.name, true, parent, 0, node.overlayData, node.isOverlay, node.originalEntryNum); + + for (const auto& child : node.children) { + makeFstRecursive(*child, ourIndex); + } + + s_fstEntries[ourIndex].nextOrLength = static_cast(s_fstEntries.size()); +} + +void makeFstFromContext(const IterateContext& context) { + makeFstRecursive(*context.root, 0); +} + +s32 calcEntryCount(const IterateNode& node) { + s32 counter = 1; + + for (const auto& child : node.children) { + counter += calcEntryCount(*child); + } + + return counter; +} + +bool validateOverlayFile(const AuroraOverlayFile& file) { + const std::string_view name(file.fileName); + + if (!name.starts_with('/')) { + Log.error("Overlay path {} does not start with /", name); + return false; + } + + if (file.size > std::numeric_limits::max()) { + Log.error("Overlay file sizes above 4 GiB are not supported: {}", name); + return false; + } + + return true; +} + +} + +namespace aurora::dvd::impl { + +bool rebuildFST() { + using namespace std::string_literals; + + if (s_partition == nullptr) { + return false; + } + + std::lock_guard lock(s_fstLock); + + s32 currentDirEntryNum = k_invalidFstEntry; + const std::string currentPath = s_currentPath; + if (s_currentDir >= 0 && static_cast(s_currentDir) < s_fstEntries.size() && s_fstEntries[s_currentDir].isDir) { + currentDirEntryNum = s_fstEntries[s_currentDir].origEntryNum; + } + + s_fstEntries.clear(); + s_entryNumToFstIndex.clear(); + IterateContext ctx; + ctx.root = std::make_shared(""s, true, 0, 0); + ctx.dirStack.emplace_back(ctx.root, std::numeric_limits::max()); + + nod_partition_iterate_fst(s_partition, fstCallback, &ctx); + s_baseEntryCount = calcEntryCount(*ctx.root); + syncOverlayEntryAllocator(); + mergeOverlayFilesIntoContext(ctx); + makeFstFromContext(ctx); + + if (currentDirEntryNum >= 0 && static_cast(currentDirEntryNum) < s_entryNumToFstIndex.size()) { + const FstIndex currentDir = s_entryNumToFstIndex[currentDirEntryNum]; + if (currentDir >= 0 && static_cast(currentDir) < s_fstEntries.size() && s_fstEntries[currentDir].isDir) { + s_currentDir = currentDir; + s_currentPath = currentPath; + return true; + } + } + + if (currentDirEntryNum != k_invalidFstEntry) { + Log.warn("Current DVD directory {} with entryNum {} was lost during FST rebuild; resetting to root", + currentPath, currentDirEntryNum); + } + + s_currentDir = 0; + s_currentPath = "/"; + return true; +} + +bool nameEqualsIgnoreCase(const std::string_view lhs, const std::string_view rhs) { + if (lhs.size() != rhs.size()) { + return false; + } + for (size_t i = 0; i < rhs.size(); ++i) { + char lc = lhs[i]; + char rc = rhs[i]; + if (lc >= 'a' && lc <= 'z') { + lc = static_cast(lc - 'a' + 'A'); + } + if (rc >= 'a' && rc <= 'z') { + rc = static_cast(rc - 'a' + 'A'); + } + if (lc != rc) { + return false; + } + } + return true; +} + +} + +s32 aurora_dvd_base_entry_count() { + return s_baseEntryCount; +} + +void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles, s32* outEntryNums) { + if (!s_overlayCallbacksSet) { + Log.fatal("aurora_dvd_overlay_callbacks not called before aurora_dvd_overlay_files!"); + } + + s_overlayFiles.clear(); + if (outEntryNums != nullptr) { + std::fill_n(outEntryNums, nFiles, k_invalidFstEntry); + } + + for (size_t i = 0; i < nFiles; i++) { + const auto& file = files[i]; + + if (!validateOverlayFile(file)) { + continue; + } + + s_overlayFiles.emplace_back(file.fileName, file.userData, static_cast(file.size), k_invalidFstEntry, i); + } + + rebuildFST(); + + if (outEntryNums != nullptr) { + for (const auto& file : s_overlayFiles) { + if (file.entryNum != k_invalidFstEntry) { + outEntryNums[file.sourceIndex] = file.entryNum; + } + } + } +} + +void aurora_dvd_overlay_callbacks(const AuroraOverlayCallbacks* callbacks) { + s_overlayCallbacks = *callbacks; + s_overlayCallbacksSet = true; +}