devoptab: add games. add MTP and FTP game exporting. update ftpsrv (see below). fix "fix_path()" parsing.

ftpsrv was updated to support MLST and MLSD, as well as fixing SIZE (was fixed to 32bit).
This commit is contained in:
ITotalJustice
2025-09-15 21:18:53 +01:00
parent 9fe0044a65
commit a94c6bb581
10 changed files with 746 additions and 18 deletions

View File

@@ -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
)

View File

@@ -59,6 +59,7 @@ void Clear();
// adds new entry to queue.
void PushAsync(u64 app_id);
void PushAsync(const std::span<const NsApplicationRecord> app_ids);
// gets entry without removing it from the queue.
auto GetAsync(u64 app_id) -> ThreadResultData*;
// single threaded title info fetch.

View File

@@ -27,6 +27,7 @@ Result MountSftpAll();
Result MountNfsAll();
Result MountSmb2All();
Result MountFatfsAll();
Result MountGameAll();
Result GetNetworkDevices(location::StdioEntries& out);
void UmountAllNeworkDevices();

View File

@@ -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();

View File

@@ -11,6 +11,8 @@
#include <ftpsrv_vfs.h>
#include <nx/vfs_nx.h>
#include <nx/utils.h>
#include <unistd.h>
#include <fcntl.h>
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<FtpVfsFile*>(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<FtpVfsFile*>(user);
return read(f->fd, buf, size);
}
int vfs_stdio_write(void* user, const void* buf, size_t size) {
auto f = static_cast<FtpVfsFile*>(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<FtpVfsFile*>(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<FtpVfsFile*>(user);
return f->valid && f->fd >= 0;
}
int vfs_stdio_close(void* user) {
auto f = static_cast<FtpVfsFile*>(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<FtpVfsDir*>(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<FtpVfsDir*>(user);
auto entry = static_cast<FtpVfsDirEntry*>(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<FtpVfsDir*>(user);
return f->fd != NULL;
}
int vfs_stdio_closedir(void* user) {
auto f = static_cast<FtpVfsDir*>(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 = {
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(

View File

@@ -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<FsProxy>(std::make_unique<fs::FsNativeSd>(), "", "microSD card"));
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Nand), "image_nand", "Image nand"));
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Sd), "image_sd", "Image sd"));
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Sd), "Album", "Album (Image SD)"));
g_fs_entries.emplace_back(std::make_shared<FsProxy>(std::make_unique<fs::FsStdio>(true, "games:/"), "Games", "Games"));
g_fs_entries.emplace_back(std::make_shared<FsDevNullProxy>("DevNull", "DevNull (Speed Test)"));
g_fs_entries.emplace_back(std::make_shared<FsInstallProxy>("install", "Install (NSP, XCI, NSZ, XCZ)"));

View File

@@ -28,6 +28,7 @@ struct ThreadData {
void Clear();
void PushAsync(u64 id);
void PushAsync(const std::span<const NsApplicationRecord> 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<const NsApplicationRecord> 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<const NsApplicationRecord> 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) {

View File

@@ -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;
}

View File

@@ -1,6 +1,7 @@
#include "utils/devoptab_common.hpp"
#include "utils/profile.hpp"
#include "fs.hpp"
#include "log.hpp"
#include "defines.hpp"
#include <fcntl.h>
@@ -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;

View File

@@ -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 <cstring>
#include <array>
#include <memory>
#include <algorithm>
namespace sphaira::devoptab {
namespace {
namespace game = ui::menu::game;
struct ContentEntry {
NsApplicationContentMetaStatus status{};
std::unique_ptr<game::NspEntry> nsp{};
};
struct Entry final : game::Entry {
std::string name{};
std::vector<ContentEntry> 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<Entry> 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<game::NspEntry>();
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<NsApplicationRecord> 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<File*>(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<File*>(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<File*>(fd);
const auto& nsp = file->nsp;
len = std::min<u64>(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<File*>(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<u64>(pos, 0, nsp->nsp_size);
}
int Device::devoptab_fstat(void *fd, struct stat *st) {
auto file = static_cast<File*>(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<Dir*>(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<Dir*>(fd);
dir->index = 0;
return 0;
}
int Device::devoptab_dirnext(void* fd, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(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<Dir*>(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<Device>(config),
config,
sizeof(File), sizeof(Dir),
"games", "games:/"
)) {
log_write("[GAME] Failed to mount GAME\n");
R_THROW(0x1);
}
R_SUCCEED();
}
} // namespace sphaira::devoptab