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_bfsar.cpp
|
||||||
source/utils/devoptab_vfs.cpp
|
source/utils/devoptab_vfs.cpp
|
||||||
source/utils/devoptab_fatfs.cpp
|
source/utils/devoptab_fatfs.cpp
|
||||||
|
source/utils/devoptab_game.cpp
|
||||||
source/utils/devoptab.cpp
|
source/utils/devoptab.cpp
|
||||||
|
|
||||||
source/usb/base.cpp
|
source/usb/base.cpp
|
||||||
@@ -335,7 +336,7 @@ endif()
|
|||||||
if (ENABLE_FTPSRV)
|
if (ENABLE_FTPSRV)
|
||||||
FetchContent_Declare(ftpsrv
|
FetchContent_Declare(ftpsrv
|
||||||
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
|
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
|
||||||
GIT_TAG 85b3cf0
|
GIT_TAG a7c2283
|
||||||
SOURCE_SUBDIR NONE
|
SOURCE_SUBDIR NONE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ void Clear();
|
|||||||
|
|
||||||
// adds new entry to queue.
|
// adds new entry to queue.
|
||||||
void PushAsync(u64 app_id);
|
void PushAsync(u64 app_id);
|
||||||
|
void PushAsync(const std::span<const NsApplicationRecord> app_ids);
|
||||||
// gets entry without removing it from the queue.
|
// gets entry without removing it from the queue.
|
||||||
auto GetAsync(u64 app_id) -> ThreadResultData*;
|
auto GetAsync(u64 app_id) -> ThreadResultData*;
|
||||||
// single threaded title info fetch.
|
// single threaded title info fetch.
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ Result MountSftpAll();
|
|||||||
Result MountNfsAll();
|
Result MountNfsAll();
|
||||||
Result MountSmb2All();
|
Result MountSmb2All();
|
||||||
Result MountFatfsAll();
|
Result MountFatfsAll();
|
||||||
|
Result MountGameAll();
|
||||||
|
|
||||||
Result GetNetworkDevices(location::StdioEntries& out);
|
Result GetNetworkDevices(location::StdioEntries& out);
|
||||||
void UmountAllNeworkDevices();
|
void UmountAllNeworkDevices();
|
||||||
|
|||||||
@@ -1664,6 +1664,11 @@ App::App(const char* argv0) {
|
|||||||
}
|
}
|
||||||
#endif // ENABLE_DEVOPTAB_SMB2
|
#endif // ENABLE_DEVOPTAB_SMB2
|
||||||
|
|
||||||
|
{
|
||||||
|
SCOPED_TIMESTAMP("game init");
|
||||||
|
devoptab::MountGameAll();
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
SCOPED_TIMESTAMP("fatfs init");
|
SCOPED_TIMESTAMP("fatfs init");
|
||||||
devoptab::MountFatfsAll();
|
devoptab::MountFatfsAll();
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
#include <ftpsrv_vfs.h>
|
#include <ftpsrv_vfs.h>
|
||||||
#include <nx/vfs_nx.h>
|
#include <nx/vfs_nx.h>
|
||||||
#include <nx/utils.h>
|
#include <nx/utils.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
|
||||||
namespace sphaira::ftpsrv {
|
namespace sphaira::ftpsrv {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -36,6 +38,7 @@ Thread g_thread;
|
|||||||
Mutex g_mutex{};
|
Mutex g_mutex{};
|
||||||
|
|
||||||
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
|
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
|
||||||
|
log_write("[FTPSRV] %s\n", msg);
|
||||||
sphaira::App::NotifyFlashLed();
|
sphaira::App::NotifyFlashLed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +277,183 @@ FtpVfs g_vfs_install = {
|
|||||||
.rename = vfs_install_rename,
|
.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) {
|
void loop(void* arg) {
|
||||||
log_write("[FTP] loop entered\n");
|
log_write("[FTP] loop entered\n");
|
||||||
|
|
||||||
@@ -326,13 +506,20 @@ void loop(void* arg) {
|
|||||||
|
|
||||||
fsdev_wrapMountSdmc();
|
fsdev_wrapMountSdmc();
|
||||||
|
|
||||||
const VfsNxCustomPath custom = {
|
static const VfsNxCustomPath custom_vfs[] = {
|
||||||
.name = "install",
|
{
|
||||||
.user = NULL,
|
.name = "games",
|
||||||
.func = &g_vfs_install,
|
.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(
|
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;
|
fs::FsPath buf;
|
||||||
const auto len = std::strlen(GetName());
|
const auto len = std::strlen(GetName());
|
||||||
|
|
||||||
if (len && !strncasecmp(path + 1, GetName(), len)) {
|
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 {
|
} 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;
|
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
|
// TODO: impl this for stdio
|
||||||
Result GetTotalSpace(const char *path, s64 *out) override {
|
Result GetTotalSpace(const char *path, s64 *out) override {
|
||||||
if (m_fs->IsNative()) {
|
if (m_fs->IsNative()) {
|
||||||
@@ -243,6 +248,10 @@ struct FsProxyVfs : FsProxyBase {
|
|||||||
using FsProxyBase::FsProxyBase;
|
using FsProxyBase::FsProxyBase;
|
||||||
virtual ~FsProxyVfs() = default;
|
virtual ~FsProxyVfs() = default;
|
||||||
|
|
||||||
|
auto FixPath(const char* path) const {
|
||||||
|
return FsProxyBase::FixPath("", path);
|
||||||
|
}
|
||||||
|
|
||||||
auto GetFileName(const char* s) -> const char* {
|
auto GetFileName(const char* s) -> const char* {
|
||||||
const auto file_name = std::strrchr(s, '/');
|
const auto file_name = std::strrchr(s, '/');
|
||||||
if (!file_name || file_name[1] == '\0') {
|
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::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), "Album", "Album (Image SD)"));
|
||||||
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::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<FsDevNullProxy>("DevNull", "DevNull (Speed Test)"));
|
||||||
g_fs_entries.emplace_back(std::make_shared<FsInstallProxy>("install", "Install (NSP, XCI, NSZ, XCZ)"));
|
g_fs_entries.emplace_back(std::make_shared<FsInstallProxy>("install", "Install (NSP, XCI, NSZ, XCZ)"));
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ struct ThreadData {
|
|||||||
void Clear();
|
void Clear();
|
||||||
|
|
||||||
void PushAsync(u64 id);
|
void PushAsync(u64 id);
|
||||||
|
void PushAsync(const std::span<const NsApplicationRecord> app_ids);
|
||||||
auto GetAsync(u64 app_id) -> ThreadResultData*;
|
auto GetAsync(u64 app_id) -> ThreadResultData*;
|
||||||
auto Get(u64 app_id, bool* cached = nullptr) -> 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* {
|
auto ThreadData::GetAsync(u64 app_id) -> ThreadResultData* {
|
||||||
SCOPED_MUTEX(&m_mutex_result);
|
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* {
|
auto GetAsync(u64 app_id) -> ThreadResultData* {
|
||||||
SCOPED_MUTEX(&g_mutex);
|
SCOPED_MUTEX(&g_mutex);
|
||||||
if (g_thread_data) {
|
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) {
|
bool fix_path(const char* str, char* out, bool strip_leading_slash) {
|
||||||
str = std::strrchr(str, ':');
|
str = std::strchr(str, ':');
|
||||||
if (!str) {
|
if (!str) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "utils/devoptab_common.hpp"
|
#include "utils/devoptab_common.hpp"
|
||||||
#include "utils/profile.hpp"
|
#include "utils/profile.hpp"
|
||||||
|
|
||||||
|
#include "fs.hpp"
|
||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
#include "defines.hpp"
|
#include "defines.hpp"
|
||||||
#include <fcntl.h>
|
#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 key = fact.substr(0, eq);
|
||||||
const auto val = fact.substr(eq + 1);
|
const auto val = fact.substr(eq + 1);
|
||||||
|
|
||||||
if (key == "type") {
|
if (fs::FsPath::path_equal(key, "type")) {
|
||||||
if (val == "file") {
|
if (fs::FsPath::path_equal(val, "file")) {
|
||||||
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
|
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;
|
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
|
||||||
} else {
|
} else {
|
||||||
log_write("[FTP] Unknown type fact value: %.*s\n", (int)val.size(), val.data());
|
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;
|
found_type = true;
|
||||||
} else if (!type_only) {
|
} else if (!type_only) {
|
||||||
if (key == "size") {
|
if (fs::FsPath::path_equal(key, "size")) {
|
||||||
st->st_size = std::stoull(std::string(val));
|
st->st_size = std::stoull(std::string(val));
|
||||||
} else if (key == "modify") {
|
} else if (fs::FsPath::path_equal(key, "modify")) {
|
||||||
if (val.size() >= 14) {
|
if (val.size() >= 14) {
|
||||||
struct tm tm{};
|
struct tm tm{};
|
||||||
tm.tm_year = std::stoi(std::string(val.substr(0, 4))) - 1900;
|
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