diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index 35beda0..972b5ef 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -128,6 +128,7 @@ add_executable(sphaira source/utils/devoptab_bfsar.cpp source/utils/devoptab_vfs.cpp source/utils/devoptab_fatfs.cpp + source/utils/devoptab_game.cpp source/utils/devoptab.cpp source/usb/base.cpp @@ -335,7 +336,7 @@ endif() if (ENABLE_FTPSRV) FetchContent_Declare(ftpsrv GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git - GIT_TAG 85b3cf0 + GIT_TAG a7c2283 SOURCE_SUBDIR NONE ) diff --git a/sphaira/include/title_info.hpp b/sphaira/include/title_info.hpp index c14e5ac..def5766 100644 --- a/sphaira/include/title_info.hpp +++ b/sphaira/include/title_info.hpp @@ -59,6 +59,7 @@ void Clear(); // adds new entry to queue. void PushAsync(u64 app_id); +void PushAsync(const std::span app_ids); // gets entry without removing it from the queue. auto GetAsync(u64 app_id) -> ThreadResultData*; // single threaded title info fetch. diff --git a/sphaira/include/utils/devoptab.hpp b/sphaira/include/utils/devoptab.hpp index 7797137..30110e0 100644 --- a/sphaira/include/utils/devoptab.hpp +++ b/sphaira/include/utils/devoptab.hpp @@ -27,6 +27,7 @@ Result MountSftpAll(); Result MountNfsAll(); Result MountSmb2All(); Result MountFatfsAll(); +Result MountGameAll(); Result GetNetworkDevices(location::StdioEntries& out); void UmountAllNeworkDevices(); diff --git a/sphaira/source/app.cpp b/sphaira/source/app.cpp index 3b161db..397534c 100644 --- a/sphaira/source/app.cpp +++ b/sphaira/source/app.cpp @@ -1664,6 +1664,11 @@ App::App(const char* argv0) { } #endif // ENABLE_DEVOPTAB_SMB2 + { + SCOPED_TIMESTAMP("game init"); + devoptab::MountGameAll(); + } + { SCOPED_TIMESTAMP("fatfs init"); devoptab::MountFatfsAll(); diff --git a/sphaira/source/ftpsrv_helper.cpp b/sphaira/source/ftpsrv_helper.cpp index c8875a1..58a8d5b 100644 --- a/sphaira/source/ftpsrv_helper.cpp +++ b/sphaira/source/ftpsrv_helper.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include namespace sphaira::ftpsrv { namespace { @@ -36,6 +38,7 @@ Thread g_thread; Mutex g_mutex{}; void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) { + log_write("[FTPSRV] %s\n", msg); sphaira::App::NotifyFlashLed(); } @@ -274,6 +277,183 @@ FtpVfs g_vfs_install = { .rename = vfs_install_rename, }; +struct FtpVfsFile { + int fd; + int valid; +}; + +struct FtpVfsDir { + DIR* fd; +}; + +struct FtpVfsDirEntry { + struct dirent* buf; +}; + +auto vfs_stdio_fix_path(const char* str) -> fs::FsPath { + while (*str == '/') { + str++; + } + + fs::FsPath out = str; + if (out.ends_with(":")) { + out += '/'; + } + + return out; +} + +int vfs_stdio_open(void* user, const char* _path, enum FtpVfsOpenMode mode) { + auto f = static_cast(user); + const auto path = vfs_stdio_fix_path(_path); + + int flags = 0, args = 0; + switch (mode) { + case FtpVfsOpenMode_READ: + flags = O_RDONLY; + args = 0; + break; + case FtpVfsOpenMode_WRITE: + flags = O_WRONLY | O_CREAT | O_TRUNC; + args = 0666; + break; + case FtpVfsOpenMode_APPEND: + flags = O_WRONLY | O_CREAT | O_APPEND; + args = 0666; + break; + } + + f->fd = open(path, flags, args); + if (f->fd >= 0) { + f->valid = 1; + } + + return f->fd; +} + +int vfs_stdio_read(void* user, void* buf, size_t size) { + auto f = static_cast(user); + return read(f->fd, buf, size); +} + +int vfs_stdio_write(void* user, const void* buf, size_t size) { + auto f = static_cast(user); + return write(f->fd, buf, size); +} + +int vfs_stdio_seek(void* user, const void* buf, size_t size, size_t off) { + auto f = static_cast(user); + const auto pos = lseek(f->fd, off, SEEK_SET); + if (pos < 0) { + return -1; + } + return 0; +} + +int vfs_stdio_isfile_open(void* user) { + auto f = static_cast(user); + return f->valid && f->fd >= 0; +} + +int vfs_stdio_close(void* user) { + auto f = static_cast(user); + int rc = 0; + if (vfs_stdio_isfile_open(f)) { + rc = close(f->fd); + f->fd = -1; + f->valid = 0; + } + return rc; +} + +int vfs_stdio_opendir(void* user, const char* _path) { + auto f = static_cast(user); + const auto path = vfs_stdio_fix_path(_path); + + f->fd = opendir(path); + if (!f->fd) { + return -1; + } + return 0; +} + +const char* vfs_stdio_readdir(void* user, void* user_entry) { + auto f = static_cast(user); + auto entry = static_cast(user_entry); + + entry->buf = readdir(f->fd); + if (!entry->buf) { + return NULL; + } + return entry->buf->d_name; +} + +int vfs_stdio_dirlstat(void* user, const void* user_entry, const char* _path, struct stat* st) { + const auto path = vfs_stdio_fix_path(_path); + return lstat(path, st); +} + +int vfs_stdio_isdir_open(void* user) { + auto f = static_cast(user); + return f->fd != NULL; +} + +int vfs_stdio_closedir(void* user) { + auto f = static_cast(user); + int rc = 0; + if (vfs_stdio_isdir_open(f)) { + rc = closedir(f->fd); + f->fd = NULL; + } + return rc; +} + +int vfs_stdio_stat(const char* _path, struct stat* st) { + const auto path = vfs_stdio_fix_path(_path); + return stat(path, st); +} + +int vfs_stdio_mkdir(const char* _path) { + const auto path = vfs_stdio_fix_path(_path); + return mkdir(path, 0777); +} + +int vfs_stdio_unlink(const char* _path) { + const auto path = vfs_stdio_fix_path(_path); + return unlink(path); +} + +int vfs_stdio_rmdir(const char* _path) { + const auto path = vfs_stdio_fix_path(_path); + return rmdir(path); +} + +int vfs_stdio_rename(const char* _src, const char* _dst) { + const auto src = vfs_stdio_fix_path(_src); + const auto dst = vfs_stdio_fix_path(_dst); + return rename(src, dst); +} + +FtpVfs g_vfs_stdio = { + .open = vfs_stdio_open, + .read = vfs_stdio_read, + .write = vfs_stdio_write, + .seek = vfs_stdio_seek, + .close = vfs_stdio_close, + .isfile_open = vfs_stdio_isfile_open, + .opendir = vfs_stdio_opendir, + .readdir = vfs_stdio_readdir, + .dirlstat = vfs_stdio_dirlstat, + .closedir = vfs_stdio_closedir, + .isdir_open = vfs_stdio_isdir_open, + .stat = vfs_stdio_stat, + .lstat = vfs_stdio_stat, + .mkdir = vfs_stdio_mkdir, + .unlink = vfs_stdio_unlink, + .rmdir = vfs_stdio_rmdir, + .rename = vfs_stdio_rename, +}; + void loop(void* arg) { log_write("[FTP] loop entered\n"); @@ -326,13 +506,20 @@ void loop(void* arg) { fsdev_wrapMountSdmc(); - const VfsNxCustomPath custom = { - .name = "install", - .user = NULL, - .func = &g_vfs_install, + static const VfsNxCustomPath custom_vfs[] = { + { + .name = "games", + .user = NULL, + .func = &g_vfs_stdio, + }, + { + .name = "install", + .user = NULL, + .func = &g_vfs_install, + }, }; - vfs_nx_init(&custom, mount_devices, save_writable, mount_bis, false); + vfs_nx_init(custom_vfs, std::size(custom_vfs), mount_devices, save_writable, mount_bis, false); } ON_SCOPE_EXIT( diff --git a/sphaira/source/haze_helper.cpp b/sphaira/source/haze_helper.cpp index afc2c4d..cbbf0b6 100644 --- a/sphaira/source/haze_helper.cpp +++ b/sphaira/source/haze_helper.cpp @@ -61,17 +61,18 @@ struct FsProxyBase : ::haze::FileSystemProxyImpl { } - auto FixPath(const char* path) const { + auto FixPath(const char* base, const char* path) const { fs::FsPath buf; const auto len = std::strlen(GetName()); if (len && !strncasecmp(path + 1, GetName(), len)) { - std::snprintf(buf, sizeof(buf), "/%s", path + 1 + len); + std::snprintf(buf, sizeof(buf), "%s/%s", base, path + 1 + len); } else { - std::strcpy(buf, path); + std::snprintf(buf, sizeof(buf), "%s/%s", base, path); + // std::strcpy(buf, path); } - // log_write("[FixPath] %s -> %s\n", path, buf.s); + log_write("[FixPath] %s -> %s\n", path, buf.s); return buf; } @@ -100,6 +101,10 @@ struct FsProxy final : FsProxyBase { } } + auto FixPath(const char* path) const { + return FsProxyBase::FixPath(m_fs->Root(), path); + } + // TODO: impl this for stdio Result GetTotalSpace(const char *path, s64 *out) override { if (m_fs->IsNative()) { @@ -243,6 +248,10 @@ struct FsProxyVfs : FsProxyBase { using FsProxyBase::FsProxyBase; virtual ~FsProxyVfs() = default; + auto FixPath(const char* path) const { + return FsProxyBase::FixPath("", path); + } + auto GetFileName(const char* s) -> const char* { const auto file_name = std::strrchr(s, '/'); if (!file_name || file_name[1] == '\0') { @@ -573,8 +582,8 @@ bool Init() { } g_fs_entries.emplace_back(std::make_shared(std::make_unique(), "", "microSD card")); - g_fs_entries.emplace_back(std::make_shared(std::make_unique(FsImageDirectoryId_Nand), "image_nand", "Image nand")); - g_fs_entries.emplace_back(std::make_shared(std::make_unique(FsImageDirectoryId_Sd), "image_sd", "Image sd")); + g_fs_entries.emplace_back(std::make_shared(std::make_unique(FsImageDirectoryId_Sd), "Album", "Album (Image SD)")); + g_fs_entries.emplace_back(std::make_shared(std::make_unique(true, "games:/"), "Games", "Games")); g_fs_entries.emplace_back(std::make_shared("DevNull", "DevNull (Speed Test)")); g_fs_entries.emplace_back(std::make_shared("install", "Install (NSP, XCI, NSZ, XCZ)")); diff --git a/sphaira/source/title_info.cpp b/sphaira/source/title_info.cpp index b816f17..c3289b5 100644 --- a/sphaira/source/title_info.cpp +++ b/sphaira/source/title_info.cpp @@ -28,6 +28,7 @@ struct ThreadData { void Clear(); void PushAsync(u64 id); + void PushAsync(const std::span app_ids); auto GetAsync(u64 app_id) -> ThreadResultData*; auto Get(u64 app_id, bool* cached = nullptr) -> ThreadResultData*; @@ -208,6 +209,30 @@ void ThreadData::PushAsync(u64 id) { } } +void ThreadData::PushAsync(const std::span app_ids) { + SCOPED_MUTEX(&m_mutex_id); + SCOPED_MUTEX(&m_mutex_result); + bool added_at_least_one = false; + + for (auto& record : app_ids) { + const auto id = record.application_id; + + const auto it_id = std::ranges::find(m_ids, id); + const auto it_result = std::ranges::find_if(m_result, [id](auto& e){ + return id == e->id; + }); + + if (it_id == m_ids.end() && it_result == m_result.end()) { + m_ids.emplace_back(id); + added_at_least_one = true; + } + } + + if (added_at_least_one) { + ueventSignal(&m_uevent); + } +} + auto ThreadData::GetAsync(u64 app_id) -> ThreadResultData* { SCOPED_MUTEX(&m_mutex_result); @@ -428,6 +453,13 @@ void PushAsync(u64 app_id) { } } +void PushAsync(const std::span app_ids) { + SCOPED_MUTEX(&g_mutex); + if (g_thread_data) { + g_thread_data->PushAsync(app_ids); + } +} + auto GetAsync(u64 app_id) -> ThreadResultData* { SCOPED_MUTEX(&g_mutex); if (g_thread_data) { diff --git a/sphaira/source/utils/devoptab_common.cpp b/sphaira/source/utils/devoptab_common.cpp index ed63809..231a0b1 100644 --- a/sphaira/source/utils/devoptab_common.cpp +++ b/sphaira/source/utils/devoptab_common.cpp @@ -710,7 +710,7 @@ Result LruBufferedData::Read(void *_buffer, s64 file_off, s64 read_size, u64* by } bool fix_path(const char* str, char* out, bool strip_leading_slash) { - str = std::strrchr(str, ':'); + str = std::strchr(str, ':'); if (!str) { return false; } diff --git a/sphaira/source/utils/devoptab_ftp.cpp b/sphaira/source/utils/devoptab_ftp.cpp index 0ae8c7e..1964e45 100644 --- a/sphaira/source/utils/devoptab_ftp.cpp +++ b/sphaira/source/utils/devoptab_ftp.cpp @@ -1,6 +1,7 @@ #include "utils/devoptab_common.hpp" #include "utils/profile.hpp" +#include "fs.hpp" #include "log.hpp" #include "defines.hpp" #include @@ -130,10 +131,10 @@ bool Device::ftp_parse_mlst_line(std::string_view line, struct stat* st, std::st const auto key = fact.substr(0, eq); const auto val = fact.substr(eq + 1); - if (key == "type") { - if (val == "file") { + if (fs::FsPath::path_equal(key, "type")) { + if (fs::FsPath::path_equal(val, "file")) { st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; - } else if (val == "dir") { + } else if (fs::FsPath::path_equal(val, "dir")) { st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; } else { log_write("[FTP] Unknown type fact value: %.*s\n", (int)val.size(), val.data()); @@ -142,9 +143,9 @@ bool Device::ftp_parse_mlst_line(std::string_view line, struct stat* st, std::st found_type = true; } else if (!type_only) { - if (key == "size") { + if (fs::FsPath::path_equal(key, "size")) { st->st_size = std::stoull(std::string(val)); - } else if (key == "modify") { + } else if (fs::FsPath::path_equal(key, "modify")) { if (val.size() >= 14) { struct tm tm{}; tm.tm_year = std::stoi(std::string(val.substr(0, 4))) - 1900; diff --git a/sphaira/source/utils/devoptab_game.cpp b/sphaira/source/utils/devoptab_game.cpp new file mode 100644 index 0000000..3ba7af7 --- /dev/null +++ b/sphaira/source/utils/devoptab_game.cpp @@ -0,0 +1,491 @@ + +#include "utils/devoptab.hpp" +#include "utils/devoptab_common.hpp" + +#include "defines.hpp" +#include "log.hpp" +#include "title_info.hpp" + +#include "ui/menus/game_menu.hpp" + +#include "yati/nx/es.hpp" +#include "yati/nx/ns.hpp" + +#include +#include +#include +#include + +namespace sphaira::devoptab { +namespace { + +namespace game = ui::menu::game; + +struct ContentEntry { + NsApplicationContentMetaStatus status{}; + std::unique_ptr nsp{}; +}; + +struct Entry final : game::Entry { + std::string name{}; + std::vector contents{}; +}; + +struct File { + game::NspEntry* nsp; + size_t off; +}; + +struct Dir { + Entry* entry; + u32 index; +}; + +void ParseId(std::string_view path, u64& id_out) { + id_out = 0; + + const auto start = path.find_first_of('['); + const auto end = path.find_first_of(']', start); + if (start != std::string_view::npos && end != std::string_view::npos && end > start + 1) { + // doesn't alloc because of SSO which is 32 bytes. + const std::string hex_str{path.substr(start + 1, end - start - 1)}; + id_out = std::stoull(hex_str, nullptr, 16); + } +} + +void ParseIds(std::string_view path, u64& app_id, u64& id) { + app_id = 0; + id = 0; + + // strip leading slashes (should only be one anyway). + while (path.starts_with('/')) { + path.remove_prefix(1); + } + + // find dir/path.nsp seperator. + const auto dir = path.find('/'); + if (dir != std::string_view::npos) { + const auto folder = path.substr(0, dir); + const auto file = path.substr(dir + 1); + ParseId(folder, app_id); + ParseId(file, id); + } else { + ParseId(path, app_id); + } +} + +struct Device final : common::MountDevice { + Device(const common::MountConfig& _config) + : MountDevice{_config} { + + } + + ~Device(); + +private: + bool Mount() override; + int devoptab_open(void *fileStruct, const char *path, int flags, int mode) override; + int devoptab_close(void *fd) override; + ssize_t devoptab_read(void *fd, char *ptr, size_t len) override; + ssize_t devoptab_seek(void *fd, off_t pos, int dir) override; + int devoptab_fstat(void *fd, struct stat *st) override; + int devoptab_diropen(void* fd, const char *path) override; + int devoptab_dirreset(void* fd) override; + int devoptab_dirnext(void* fd, char *filename, struct stat *filestat) override; + int devoptab_dirclose(void* fd) override; + int devoptab_lstat(const char *path, struct stat *st) override; + + game::NspEntry* FindNspFromEntry(Entry& entry, u64 id) const; + Entry* FindEntry(u64 app_id); + Result LoadMetaEntries(Entry& entry) const; + +private: + std::vector m_entries{}; + keys::Keys m_keys{}; + bool m_title_init{}; + bool m_es_init{}; + bool m_ns_init{}; + bool m_keys_init{}; + bool m_mounted{}; +}; + +Device::~Device() { + if (m_title_init) { + title::Exit(); + } + + if (m_es_init) { + es::Exit(); + } + + if (m_ns_init) { + ns::Exit(); + } +} + +Result Device::LoadMetaEntries(Entry& entry) const { + // check if we have already loaded the meta entries. + if (!entry.contents.empty()) { + R_SUCCEED(); + } + + title::MetaEntries entry_status{}; + R_TRY(title::GetMetaEntries(entry.app_id, entry_status, title::ContentFlag_All)); + + for (const auto& status : entry_status) { + entry.contents.emplace_back(status); + } + + R_SUCCEED(); +} + +game::NspEntry* Device::FindNspFromEntry(Entry& entry, u64 id) const { + // load all meta entries if not yet loaded. + if (R_FAILED(LoadMetaEntries(entry))) { + log_write("[GAME] failed to load meta entries for app id: %016lx\n", entry.app_id); + return nullptr; + } + + // try and find the matching nsp entry. + for (auto& content : entry.contents) { + if (content.status.application_id == id) { + // build nsp entry if not yet built. + if (!content.nsp) { + game::ContentInfoEntry info; + if (R_FAILED(game::BuildContentEntry(content.status, info))) { + log_write("[GAME] failed to build content info for app id: %016lx\n", entry.app_id); + return nullptr; + } + + content.nsp = std::make_unique(); + if (R_FAILED(game::BuildNspEntry(entry, info, m_keys, *content.nsp))) { + log_write("[GAME] failed to build nsp entry for app id: %016lx\n", entry.app_id); + content.nsp.reset(); + return nullptr; + } + + // update path to strip the folder, if it has one. + const auto slash = std::strchr(content.nsp->path, '/'); + if (slash) { + std::memmove(content.nsp->path, slash + 1, std::strlen(slash)); + } + } + + return content.nsp.get(); + } + } + + log_write("[GAME] failed to find content for id: %016lx\n", id); + return nullptr; +} + +Entry* Device::FindEntry(u64 app_id) { + for (auto& entry : m_entries) { + if (entry.app_id == app_id) { + // the error doesn't matter here, the fs will just report an empty dir. + LoadMetaEntries(entry); + return &entry; + } + } + + log_write("[GAME] failed to find entry for app id: %016lx\n", app_id); + return nullptr; +} + +bool Device::Mount() { + if (m_mounted) { + return true; + } + + log_write("[GAME] Mounting...\n"); + + if (!m_title_init) { + if (R_FAILED(title::Init())) { + log_write("[GAME] Failed to init title info\n"); + return false; + } + m_title_init = true; + } + + if (!m_es_init) { + if (R_FAILED(es::Initialize())) { + log_write("[GAME] Failed to init es\n"); + return false; + } + m_es_init = true; + } + + if (!m_ns_init) { + if (R_FAILED(ns::Initialize())) { + log_write("[GAME] Failed to init ns\n"); + return false; + } + m_ns_init = true; + } + + if (!m_keys_init) { + keys::parse_keys(m_keys, true); + } + + if (m_entries.empty()) { + m_entries.reserve(1000); + std::vector record_list(1000); + s32 offset{}; + + while (true) { + s32 record_count{}; + if (R_FAILED(nsListApplicationRecord(record_list.data(), record_list.size(), offset, &record_count))) { + log_write("failed to list application records at offset: %d\n", offset); + } + + // finished parsing all entries. + if (!record_count) { + break; + } + + title::PushAsync(std::span(record_list.data(), record_count)); + + for (s32 i = 0; i < record_count; i++) { + const auto& e = record_list[i]; + m_entries.emplace_back(game::Entry{e.application_id, e.last_event}); + } + + offset += record_count; + } + } + + log_write("[GAME] mounted with %zu entries\n", m_entries.size()); + m_mounted = true; + return true; +} + +int Device::devoptab_open(void *fileStruct, const char *path, int flags, int mode) { + auto file = static_cast(fileStruct); + + u64 app_id{}, id{}; + ParseIds(path, app_id, id); + + if (!app_id || !id) { + log_write("[GAME] invalid path %s\n", path); + return -ENOENT; + } + + auto entry = FindEntry(app_id); + if (!entry) { + log_write("[GAME] failed to find entry for app id: %016lx\n", app_id); + return -ENOENT; + } + + // try and find the matching nsp entry. + auto nsp = FindNspFromEntry(*entry, id); + if (!nsp) { + log_write("[GAME] failed to find nsp for content id: %016lx\n", id); + return -ENOENT; + } + + file->nsp = nsp; + return 0; +} + +int Device::devoptab_close(void *fd) { + auto file = static_cast(fd); + std::memset(file, 0, sizeof(*file)); + + return 0; +} + +ssize_t Device::devoptab_read(void *fd, char *ptr, size_t len) { + auto file = static_cast(fd); + + const auto& nsp = file->nsp; + len = std::min(len, nsp->nsp_size - file->off); + + u64 bytes_read; + if (R_FAILED(nsp->Read(ptr, file->off, len, &bytes_read))) { + return -EIO; + } + + file->off += bytes_read; + return bytes_read; +} + +ssize_t Device::devoptab_seek(void *fd, off_t pos, int dir) { + auto file = static_cast(fd); + const auto& nsp = file->nsp; + + if (dir == SEEK_CUR) { + pos += file->off; + } else if (dir == SEEK_END) { + pos = nsp->nsp_size; + } + + return file->off = std::clamp(pos, 0, nsp->nsp_size); +} + +int Device::devoptab_fstat(void *fd, struct stat *st) { + auto file = static_cast(fd); + const auto& nsp = file->nsp; + + st->st_nlink = 1; + st->st_size = nsp->nsp_size; + st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + return 0; +} + +int Device::devoptab_diropen(void* fd, const char *path) { + auto dir = static_cast(fd); + + if (!std::strcmp(path, "/")) { + return 0; + } else { + u64 app_id{}, id{}; + ParseIds(path, app_id, id); + + if (!app_id || id) { + log_write("[GAME] invalid folder path %s\n", path); + return -ENOENT; + } + + auto entry = FindEntry(app_id); + if (!entry) { + log_write("[GAME] failed to find entry for app id: %016lx\n", app_id); + return -ENOENT; + } + + dir->entry = entry; + return 0; + } +} + +int Device::devoptab_dirreset(void* fd) { + auto dir = static_cast(fd); + + dir->index = 0; + return 0; +} + +int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) { + auto dir = static_cast(fd); + + if (!dir->entry) { + if (dir->index >= m_entries.size()) { + log_write("[GAME] dirnext: no more entries\n"); + return -ENOENT; + } + + auto& entry = m_entries[dir->index]; + if (entry.status == title::NacpLoadStatus::None) { + // this will never be null as it blocks until a valid entry is loaded. + auto result = title::Get(entry.app_id); + entry.lang = result->lang; + entry.status = result->status; + + char name[NAME_MAX]{}; + if (result->status == title::NacpLoadStatus::Loaded) { + fs::FsPath name_buf = result->lang.name; + title::utilsReplaceIllegalCharacters(name_buf, true); + + const int name_max = sizeof(name) - 33; + std::snprintf(name, sizeof(name), "%.*s [%016lX]", name_max, name_buf.s, entry.app_id); + } else { + std::snprintf(name, sizeof(name), "[%016lX]", entry.app_id); + log_write("[GAME] failed to get title info for %s\n", name); + } + + entry.name = name; + } + + filestat->st_nlink = 1; + filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; + std::strcpy(filename, entry.name.c_str()); + } else { + auto& entry = dir->entry; + if (dir->index >= entry->contents.size()) { + log_write("[GAME] dirnext: no more entries\n"); + return -ENOENT; + } + + const auto& content = entry->contents[dir->index]; + if (!content.nsp) { + if (!FindNspFromEntry(*entry, content.status.application_id)) { + log_write("[GAME] failed to find nsp for content id: %016lx\n", content.status.application_id); + return -ENOENT; + } + } + + filestat->st_nlink = 1; + filestat->st_size = content.nsp->nsp_size; + filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + std::snprintf(filename, NAME_MAX, "%s", content.nsp->path.s); + } + + dir->index++; + return 0; +} + +int Device::devoptab_dirclose(void* fd) { + auto dir = static_cast(fd); + std::memset(dir, 0, sizeof(*dir)); + + return 0; +} + +int Device::devoptab_lstat(const char *path, struct stat *st) { + st->st_nlink = 1; + + if (!std::strcmp(path, "/")) { + st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; + return 0; + } else { + u64 app_id{}, id{}; + ParseIds(path, app_id, id); + if (!app_id) { + log_write("[GAME] invalid path %s\n", path); + return -ENOENT; + } + + auto entry = FindEntry(app_id); + if (!entry) { + log_write("[GAME] failed to find entry for app id: %016lx\n", app_id); + return -ENOENT; + } + + if (!id) { + st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; + return 0; + } + + auto nsp = FindNspFromEntry(*entry, id); + if (!nsp) { + log_write("[GAME] failed to find nsp for content id: %016lx\n", id); + return -ENOENT; + } + + st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + st->st_size = nsp->nsp_size; + return 0; + } +} + +} // namespace + +Result MountGameAll() { + common::MountConfig config{}; + config.read_only = true; + config.dump_hidden = true; + config.no_stat_file = false;; + + if (!common::MountNetworkDevice2( + std::make_unique(config), + config, + sizeof(File), sizeof(Dir), + "games", "games:/" + )) { + log_write("[GAME] Failed to mount GAME\n"); + R_THROW(0x1); + } + + R_SUCCEED(); +} + +} // namespace sphaira::devoptab