DVD file overlay support (#216)

* DVD file overlay support

Fixes #163

Basically allows games to "overlay" a set of file paths over the DVD. These are provided as a flat list of absolute paths, and when an overlayed file is read, the game gets the ability to provide its own read/seek callbacks.

The current implementation rebuilds the FST. This means EntryNums will not be consistent with the original disc, but it does avoid rewriting a significant chunk of the existing DVD code that relies on the FST.

* Comments and a guard

* EntryNum handling rewrite

Game now assigns entrynums for added files, and original disc entrynums are preserved.

* Split FST logic into another file in dvd lib

* Fix overlay userdata 0

Dusklight was using integer indexes for the userdata of overlay files. This meant file 0 was a null void*, and that meant the DVD code didn't treat it as an overlaid file.

Add an extra bool to avoid getting confused like this.

* Make entryNum only validated if not replacing file

* Make FST rebuilds thread safe with concurrent readers

Since I kinda would like to eventually allow mods to be loaded without game restart (where practical)

* Doc updates

* Remove debug logs

* Aurora-allocated entry nums & dir support

* Add warning for resetting cwd

---------

Co-authored-by: Luke Street <luke@street.dev>
This commit is contained in:
Pieter-Jan Briers
2026-05-30 21:16:42 +02:00
committed by GitHub
parent 7ba0d010cc
commit 7dd107e1a4
5 changed files with 697 additions and 133 deletions
+169 -130
View File
@@ -1,5 +1,7 @@
#include <aurora/dvd.h>
#include <dolphin/dvd.h>
#include <algorithm>
#include <dolphin/os.h>
#include <nod.h>
#include <SDL3/SDL_iostream.h>
@@ -8,36 +10,81 @@
#include <cstdint>
#include <cstring>
#include <limits>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "dvd.hpp"
#include "../../internal.hpp"
using namespace aurora::dvd::impl;
namespace aurora::dvd::impl {
NodHandle* s_partition = nullptr;
std::vector<FSTEntry> 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<FstIndex> 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;
};
struct IterateContext {
std::vector<FSTEntry>* entries = nullptr;
std::vector<std::pair<u32, u32>> dirStack;
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);
}
};
NodHandle* s_disc = nullptr;
NodHandle* s_partition = nullptr;
std::vector<FSTEntry> 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;
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);
}
};
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<size_t>(entry) < s_fstEntries.size(); }
bool isValidEntryNum(s32 entry) {
return entry >= 0 && static_cast<size_t>(entry) < s_entryNumToFstIndex.size() &&
s_entryNumToFstIndex[entry] != k_invalidFstEntry;
}
bool isValidFstIndex(FstIndex entry) {
return entry >= 0 && static_cast<size_t>(entry) < s_fstEntries.size();
}
bool isAligned(const void* addr, uintptr_t align) {
return (reinterpret_cast<uintptr_t>(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<IterateContext*>(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<u32>(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<char>(lc - 'a' + 'A');
}
if (rc >= 'a' && rc <= 'z') {
rc = static_cast<char>(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<u32>(dirEntry) + 1;
while (i < childEnd && i < s_fstEntries.size()) {
if (nameEqualsIgnoreCase(s_fstEntries[i].name, name, nameLen)) {
return static_cast<s32>(i);
return static_cast<FstIndex>(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<std::string> 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<s32>(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<size_t>(remaining));
const int64_t read = handle->read(writePtr + totalRead, static_cast<size_t>(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<NodHandle*>(block->userData);
return static_cast<CommandDataBase*>(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<u32>(offset), callback);
NodHandle* handle = getCommandHandle(block);
const int64_t seek = handle != nullptr ? nod_seek(handle, static_cast<int64_t>(offset), 0) : -1;
auto handle = getCommandHandle(block);
const int64_t seek = handle != nullptr ? handle->seek(static_cast<int64_t>(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<s32>(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<u32>(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<NodHandle*>(fileInfo->cb.userData));
delete static_cast<CommandDataBase*>(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;
}
s_currentDir = entry;
s_currentPath = buildDirPath(entry);
const auto fstIndex = s_entryNumToFstIndex[entry];
if (!s_fstEntries[fstIndex].isDir) {
return FALSE;
}
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<u32>(entrynum);
dir->location = static_cast<u32>(entrynum) + 1;
dir->next = s_fstEntries[entrynum].nextOrLength;
dir->location = static_cast<u32>(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<u32>(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<s32>(dir->entryNum);
if (!isValidEntryNum(entryNum)) {
return;
}
const auto fstIndex = s_entryNumToFstIndex[entryNum];
dir->location = static_cast<u32>(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<int64_t>(offset), 0) : -1;
const int64_t seek = s_disc != nullptr ? s_disc->seek(static_cast<int64_t>(offset), 0) : -1;
if (callback != nullptr) {
callback(static_cast<u32>((seek >= 0) ? DVD_RESULT_GOOD : DVD_RESULT_FATAL_ERROR));
}
+74
View File
@@ -0,0 +1,74 @@
#pragma once
#include <string>
#include <memory>
#include <mutex>
#include <nod.h>
#include <dolphin/os.h>
#include <aurora/dvd.h>
#include <dolphin/dvd.h>
#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<std::shared_ptr<IterateNode>> 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<IterateNode> root;
std::vector<std::pair<std::shared_ptr<IterateNode>, u32>> dirStack;
};
extern NodHandle* s_partition;
extern std::vector<FSTEntry> 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<FstIndex> 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);
}
+353
View File
@@ -0,0 +1,353 @@
#include "dvd.hpp"
#include <algorithm>
#include <limits>
#include <unordered_map>
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<OverlayFileEntry> s_overlayFiles;
std::unordered_map<std::string, s32> 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<char>(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<IterateContext*>(userData);
while (index >= ctx->dirStack.back().second) {
ctx->dirStack.pop_back();
}
const auto newEntry = std::make_shared<IterateNode>(
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<IterateNode>(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<IterateNode>(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<FstIndex>(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<FstIndex>(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<u32>(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<u32>::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<size_t>(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<IterateNode>(""s, true, 0, 0);
ctx.dirStack.emplace_back(ctx.root, std::numeric_limits<u32>::max());
nod_partition_iterate_fst(s_partition, fstCallback, &ctx);
s_baseEntryCount = calcEntryCount(*ctx.root);
syncOverlayEntryAllocator();
mergeOverlayFilesIntoContext(ctx);
makeFstFromContext(ctx);
if (currentDirEntryNum >= 0 && static_cast<size_t>(currentDirEntryNum) < s_entryNumToFstIndex.size()) {
const FstIndex currentDir = s_entryNumToFstIndex[currentDirEntryNum];
if (currentDir >= 0 && static_cast<size_t>(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<char>(lc - 'a' + 'A');
}
if (rc >= 'a' && rc <= 'z') {
rc = static_cast<char>(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<u32>(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;
}