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:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -27,6 +27,7 @@ Result MountSftpAll();
|
||||
Result MountNfsAll();
|
||||
Result MountSmb2All();
|
||||
Result MountFatfsAll();
|
||||
Result MountGameAll();
|
||||
|
||||
Result GetNetworkDevices(location::StdioEntries& out);
|
||||
void UmountAllNeworkDevices();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = {
|
||||
.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(
|
||||
|
||||
@@ -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)"));
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
491
sphaira/source/utils/devoptab_game.cpp
Normal file
491
sphaira/source/utils/devoptab_game.cpp
Normal 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
|
||||
Reference in New Issue
Block a user