fs: add support for mounting nsp an xci files in the filebrowser.

This commit is contained in:
ITotalJustice
2025-08-11 07:01:52 +01:00
parent 25f2cfbff2
commit cb2fa1abfc
15 changed files with 801 additions and 137 deletions

View File

@@ -21,19 +21,21 @@ set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# enable LTO (only in release builds)
if (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
include(CheckIPOSupported)
check_ipo_supported(RESULT ipo_supported OUTPUT ipo_error)
if (ipo_supported)
message(STATUS "IPO / LTO enabled for ALL targets")
cmake_policy(SET CMP0069 NEW)
set(CMAKE_POLICY_DEFAULT_CMP0069 NEW)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
if (LTO)
if (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel")
include(CheckIPOSupported)
check_ipo_supported(RESULT ipo_supported OUTPUT ipo_error)
if (ipo_supported)
message(STATUS "IPO / LTO enabled for ALL targets")
cmake_policy(SET CMP0069 NEW)
set(CMAKE_POLICY_DEFAULT_CMP0069 NEW)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
else()
message(STATUS "IPO / LTO not supported: <${ipo_error}>")
endif()
else()
message(STATUS "IPO / LTO not supported: <${ipo_error}>")
message(STATUS "IPO / LTO not enabled in debug build")
endif()
else()
message(STATUS "IPO / LTO not enabled in debug build")
endif()
function(dkp_fatal_if_not_found var package)

View File

@@ -18,7 +18,8 @@
"inherits":["core"],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "MinSizeRel",
"ENABLE_NETWORK_INSTALL": false
"ENABLE_NETWORK_INSTALL": false,
"LTO": true
}
},
{
@@ -27,14 +28,19 @@
"inherits":["core"],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "MinSizeRel",
"ENABLE_NETWORK_INSTALL": true
"ENABLE_NETWORK_INSTALL": true,
"LTO": true
}
},
{
"name": "Debug",
"displayName": "Debug",
"name": "Dev",
"displayName": "Dev",
"inherits":["core"],
"cacheVariables": { "CMAKE_BUILD_TYPE":"Debug" }
"cacheVariables": {
"CMAKE_BUILD_TYPE": "MinSizeRel",
"ENABLE_NETWORK_INSTALL": true,
"LTO": false
}
}
],
"buildPresets": [
@@ -49,8 +55,8 @@
"jobs": 16
},
{
"name": "Debug",
"configurePreset": "Debug",
"name": "Dev",
"configurePreset": "Dev",
"jobs": 16
}
]

View File

@@ -91,6 +91,8 @@ add_executable(sphaira
source/fatfs.cpp
source/utils/devoptab_save.cpp
source/utils/devoptab_nsp.cpp
source/utils/devoptab_xci.cpp
# todo:
# source/utils/devoptab_zip.cpp
@@ -143,6 +145,8 @@ target_compile_options(sphaira PRIVATE
# disabled as it warns for strcat 2 paths together, but it will never
# overflow due to fs enforcing a max path len anyway.
-Wno-format-truncation
# many false positives when LTO is not enabled.
-Wno-suggest-final-types
# the below are taken from my gba emulator, they've served me well ;)
-Wformat-overflow=2
@@ -159,9 +163,7 @@ target_compile_options(sphaira PRIVATE
-Wcast-qual
-Wcast-align
-Wimplicit-fallthrough=5
-Wsuggest-final-types
-Wuninitialized
-fdiagnostics-all-candidates
)
include(FetchContent)

View File

@@ -305,6 +305,10 @@ struct FsView final : Widget {
void DisplayOptions();
void DisplayAdvancedOptions();
void MountNspFs();
void MountXciFs();
void MountZipFs();
// private:
Base* m_menu{};
ViewSide m_side{};
@@ -476,4 +480,22 @@ auto IsSamePath(std::string_view a, std::string_view b) -> bool;
auto IsExtension(std::string_view ext1, std::string_view ext2) -> bool;
auto IsExtension(std::string_view ext, std::span<const std::string_view> list) -> bool;
struct FsStdioWrapper final : fs::FsStdio {
using OnExit = std::function<void(void)>;
FsStdioWrapper(const fs::FsPath& root, const OnExit& on_exit) : fs::FsStdio{true, root}, m_on_exit{on_exit} {
}
~FsStdioWrapper() {
if (m_on_exit) {
m_on_exit();
}
}
const OnExit m_on_exit;
};
void MountFsHelper(const std::shared_ptr<fs::Fs>& fs, const fs::FsPath& name);
} // namespace sphaira::ui::menu::filebrowser

View File

@@ -10,15 +10,15 @@ Result MountFromSavePath(u64 id, fs::FsPath& out_path);
void UnmountSave(u64 id);
// todo:
void MountZip(fs::Fs* fs, const fs::FsPath& mount, fs::FsPath& out_path);
Result MountZip(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
void UmountZip(const fs::FsPath& mount);
// todo:
void MountNsp(fs::Fs* fs, const fs::FsPath& mount, fs::FsPath& out_path);
Result MountNsp(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
void UmountNsp(const fs::FsPath& mount);
// todo:
void MountXci(fs::Fs* fs, const fs::FsPath& mount, fs::FsPath& out_path);
Result MountXci(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path);
void UmountXci(const fs::FsPath& mount);
bool fix_path(const char* str, char* out);
} // namespace sphaira::devoptab

View File

@@ -8,8 +8,19 @@
namespace sphaira::yati::container {
struct Xci final : Base {
struct Partition {
// name of the partition.
std::string name;
// all the collections for this partition, may be empty.
Collections collections;
};
using Partitions = std::vector<Partition>;
using Base::Base;
Result GetCollections(Collections& out) override;
Result GetPartitions(Partitions& out);
};
} // namespace sphaira::yati::container

View File

@@ -7,6 +7,8 @@
#include "ui/error_box.hpp"
#include "ui/menus/file_viewer.hpp"
#include "utils/devoptab.hpp"
#include "log.hpp"
#include "app.hpp"
#include "ui/nvg_util.hpp"
@@ -90,6 +92,12 @@ constexpr std::string_view IMAGE_EXTENSIONS[] = {
constexpr std::string_view INSTALL_EXTENSIONS[] = {
"nsp", "xci", "nsz", "xcz",
};
constexpr std::string_view NSP_EXTENSIONS[] = {
"nsp", "nsz",
};
constexpr std::string_view XCI_EXTENSIONS[] = {
"xci", "xcz",
};
// these are files that are already compressed or encrypted and should
// be stored raw in a zip file.
constexpr std::string_view COMPRESSED_EXTENSIONS[] = {
@@ -678,6 +686,10 @@ void FsView::OnClick() {
nro_launch(GetNewPathCurrent());
}
});
} else if (IsExtension(entry.GetExtension(), NSP_EXTENSIONS)) {
MountNspFs();
} else if (IsExtension(entry.GetExtension(), XCI_EXTENSIONS)) {
MountXciFs();
} else if (IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) {
InstallFiles();
} else if (IsSd()) {
@@ -1950,6 +1962,38 @@ void FsView::DisplayAdvancedOptions() {
});
}
void FsView::MountNspFs() {
fs::FsPath mount;
const auto rc = devoptab::MountNsp(GetFs(), GetNewPathCurrent(), mount);
App::PushErrorBox(rc, "Failed to mount NSP."_i18n);
if (R_SUCCEEDED(rc)) {
auto fs = std::make_shared<FsStdioWrapper>(mount, [mount](){
devoptab::UmountNsp(mount);
});
MountFsHelper(fs, GetEntry().GetName());
}
}
void FsView::MountXciFs() {
fs::FsPath mount;
const auto rc = devoptab::MountXci(GetFs(), GetNewPathCurrent(), mount);
App::PushErrorBox(rc, "Failed to mount XCI."_i18n);
if (R_SUCCEEDED(rc)) {
auto fs = std::make_shared<FsStdioWrapper>(mount, [mount](){
devoptab::UmountXci(mount);
});
MountFsHelper(fs, GetEntry().GetName());
}
}
void FsView::MountZipFs() {
//todo:
}
Base::Base(u32 flags, u32 options)
: Base{CreateFs(FS_ENTRY_DEFAULT), FS_ENTRY_DEFAULT, {}, false, flags, options} {
}
@@ -2299,4 +2343,15 @@ auto Base::CreateFs(const FsEntry& fs_entry) -> std::shared_ptr<fs::Fs> {
std::unreachable();
}
void MountFsHelper(const std::shared_ptr<fs::Fs>& fs, const fs::FsPath& name) {
const filebrowser::FsEntry fs_entry{
.name = name,
.root = fs->Root(),
.type = filebrowser::FsType::Custom,
.flags = filebrowser::FsEntryFlag_ReadOnly,
};
App::Push<filebrowser::Menu>(fs, fs_entry, fs->Root());
}
} // namespace sphaira::ui::menu::filebrowser

View File

@@ -411,14 +411,7 @@ Result Menu::MountNcaFs() {
auto fs = std::make_shared<fs::FsNativeId>(program_id, type, path);
R_TRY(fs->GetFsOpenResult());
const filebrowser::FsEntry fs_entry{
.name = "NCA",
.root = "/",
.type = filebrowser::FsType::Custom,
.flags = filebrowser::FsEntryFlag_ReadOnly,
};
App::Push<filebrowser::Menu>(fs, fs_entry, "/");
filebrowser::MountFsHelper(fs, hexIdToStr(e.content_id).str);
R_SUCCEED();
}

View File

@@ -523,8 +523,8 @@ struct NroRomFS final : fs::FsStdio {
};
Result Menu::MountRomfsFs() {
const char* name = "nro_romfs";
const char* root = "nro_romfs:/";
static const char* name = "nro_romfs";
static const char* root = "nro_romfs:/";
const auto& e = GetEntry();
// todo: add errors for when nro doesn't have romfs.
@@ -540,16 +540,11 @@ Result Menu::MountRomfsFs() {
R_THROW(rc);
}
auto fs = std::make_shared<NroRomFS>(name, root);
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [](){
romfsUnmount(name);
});
const filebrowser::FsEntry fs_entry{
.name = e.GetName(),
.root = root,
.type = filebrowser::FsType::Custom,
.flags = filebrowser::FsEntryFlag_ReadOnly,
};
App::Push<filebrowser::Menu>(fs, fs_entry, root);
filebrowser::MountFsHelper(fs, e.GetName());
R_SUCCEED();
}

View File

@@ -42,18 +42,6 @@ constexpr const char* NX_SAVE_META_NAME = ".nx_save_meta.bin";
constinit UEvent g_change_uevent;
struct SystemSaveFs final : fs::FsStdio {
SystemSaveFs(u64 id, const fs::FsPath& root) : FsStdio{true, root}, m_id{id} {
}
~SystemSaveFs() {
devoptab::UnmountSave(m_id);
}
const u64 m_id;
};
// https://github.com/J-D-K/JKSV/issues/264#issuecomment-2618962807
struct NXSaveMeta {
u32 magic{}; // NX_SAVE_META_MAGIC
@@ -1118,21 +1106,16 @@ Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& loc
Result Menu::MountSaveFs() {
const auto& e = m_entries[m_index];
fs::FsPath root;
if (e.system_save_data_id) {
fs::FsPath root;
R_TRY(devoptab::MountFromSavePath(e.system_save_data_id, root));
auto fs = std::make_shared<SystemSaveFs>(e.system_save_data_id, root);
auto fs = std::make_shared<filebrowser::FsStdioWrapper>(root, [&e](){
devoptab::UnmountSave(e.system_save_data_id);
});
const filebrowser::FsEntry fs_entry{
.name = e.GetName(),
.root = root,
.type = filebrowser::FsType::Custom,
.flags = filebrowser::FsEntryFlag_ReadOnly,
};
App::Push<filebrowser::Menu>(fs, fs_entry, root);
filebrowser::MountFsHelper(fs, e.GetName());
} else {
const auto save_data_space_id = (FsSaveDataSpaceId)e.save_data_space_id;
@@ -1146,15 +1129,7 @@ Result Menu::MountSaveFs() {
auto fs = std::make_shared<fs::FsNativeSave>((FsSaveDataType)e.save_data_type, save_data_space_id, &attr, true);
R_TRY(fs->GetFsOpenResult());
const filebrowser::FsEntry fs_entry{
.name = e.GetName(),
.root = "/",
.type = filebrowser::FsType::Custom,
.flags = filebrowser::FsEntryFlag_ReadOnly,
};
App::Push<filebrowser::Menu>(fs, fs_entry, "/");
filebrowser::MountFsHelper(fs, e.GetName());
}
R_SUCCEED();

View File

@@ -0,0 +1,285 @@
#include "utils/devoptab.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "yati/container/nsp.hpp"
#include "yati/container/xci.hpp"
#include "yati/source/file.hpp"
#include <cstring>
#include <cstdio>
#include <cerrno>
#include <algorithm>
#include <sys/iosupport.h>
namespace sphaira::devoptab {
namespace {
struct Device {
std::unique_ptr<yati::source::Base> source;
yati::container::Collections collections;
};
struct File {
Device* device;
const yati::container::CollectionEntry* collection;
size_t off;
};
struct Dir {
Device* device;
u32 index;
};
int set_errno(struct _reent *r, int err) {
r->_errno = err;
return -1;
}
int devoptab_open(struct _reent *r, void *fileStruct, const char *_path, int flags, int mode) {
auto device = (Device*)r->deviceData;
auto file = static_cast<File*>(fileStruct);
std::memset(file, 0, sizeof(*file));
char path[FS_MAX_PATH];
if (!fix_path(_path, path)) {
return set_errno(r, ENOENT);
}
for (const auto& collection : device->collections) {
if (path == "/" + collection.name) {
file->device = device;
file->collection = &collection;
return r->_errno = 0;
}
}
return set_errno(r, ENOENT);
}
int devoptab_close(struct _reent *r, void *fd) {
auto file = static_cast<File*>(fd);
std::memset(file, 0, sizeof(*file));
return r->_errno = 0;
}
ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
const auto& collection = file->collection;
len = std::min(len, collection->size - file->off);
u64 bytes_read;
if (R_FAILED(file->device->source->Read(ptr, collection->offset + file->off, len, &bytes_read))) {
return set_errno(r, ENOENT);
}
file->off += bytes_read;
return bytes_read;
}
off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
const auto& collection = file->collection;
if (dir == SEEK_CUR) {
pos += file->off;
} else if (dir == SEEK_END) {
pos = collection->size;
}
r->_errno = 0;
return file->off = std::clamp<u64>(pos, 0, collection->size);
}
int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
const auto& collection = file->collection;
std::memset(st, 0, sizeof(*st));
st->st_nlink = 1;
st->st_size = collection->size;
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
return r->_errno = 0;
}
DIR_ITER* devoptab_diropen(struct _reent *r, DIR_ITER *dirState, const char *_path) {
auto device = (Device*)r->deviceData;
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(dir, 0, sizeof(*dir));
char path[FS_MAX_PATH];
if (!fix_path(_path, path)) {
set_errno(r, ENOENT);
return NULL;
}
if (!std::strcmp(path, "/")) {
dir->device = device;
} else {
set_errno(r, ENOENT);
return NULL;
}
r->_errno = 0;
return dirState;
}
int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
dir->index = 0;
return r->_errno = 0;
}
int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(filestat, 0, sizeof(*filestat));
if (dir->index >= dir->device->collections.size()) {
return set_errno(r, ENOENT);
}
const auto& collection = dir->device->collections[dir->index];
filestat->st_nlink = 1;
filestat->st_size = collection.size;
filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
std::strcpy(filename, collection.name.c_str());
dir->index++;
return r->_errno = 0;
}
int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(dir, 0, sizeof(*dir));
return r->_errno = 0;
}
int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) {
auto device = (Device*)r->deviceData;
log_write("[\t\tDEV] lstat\n");
char path[FS_MAX_PATH];
if (!fix_path(_path, path)) {
return set_errno(r, ENOENT);
}
std::memset(st, 0, sizeof(*st));
if (!std::strcmp(path, "/")) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else {
const auto it = std::ranges::find_if(device->collections, [path](auto& e){
return path == "/" + e.name;
});
if (it == device->collections.end()) {
return set_errno(r, ENOENT);
}
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
st->st_size = it->size;
}
st->st_nlink = 1;
return r->_errno = 0;
}
constexpr devoptab_t DEVOPTAB = {
.structSize = sizeof(File),
.open_r = devoptab_open,
.close_r = devoptab_close,
.read_r = devoptab_read,
.seek_r = devoptab_seek,
.fstat_r = devoptab_fstat,
.stat_r = devoptab_lstat,
.dirStateSize = sizeof(Dir),
.diropen_r = devoptab_diropen,
.dirreset_r = devoptab_dirreset,
.dirnext_r = devoptab_dirnext,
.dirclose_r = devoptab_dirclose,
.lstat_r = devoptab_lstat,
};
struct Entry {
Device device;
devoptab_t devoptab;
fs::FsPath path;
fs::FsPath mount;
char name[32];
s32 ref_count;
};
Mutex g_mutex;
std::vector<Entry> g_entries;
u32 g_mount_idx;
} // namespace
Result MountNsp(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) {
SCOPED_MUTEX(&g_mutex);
// check if we already have the save mounted.
for (auto& e : g_entries) {
if (e.path == path) {
e.ref_count++;
out_path = e.mount;
R_SUCCEED();
}
}
// create new entry.
auto& entry = g_entries.emplace_back();
auto source = std::make_unique<yati::source::File>(fs, path);
// R_TRY(yati::container::Xci(source.get()).ReadAll());
yati::container::Nsp nsp{source.get()};
R_TRY(nsp.GetCollections(entry.device.collections));
entry.path = path;
entry.devoptab = DEVOPTAB;
entry.devoptab.name = entry.name;
entry.devoptab.deviceData = &entry.device;
entry.device.source = std::move(source);
std::snprintf(entry.name, sizeof(entry.name), "nsp_%u", g_mount_idx);
std::snprintf(entry.mount, sizeof(entry.mount), "nsp_%u:/", g_mount_idx);
R_UNLESS(AddDevice(&entry.devoptab) >= 0, 0x1);
log_write("[NSP] DEVICE SUCCESS %s %s\n", path.s, entry.name);
out_path = entry.mount;
entry.ref_count++;
g_mount_idx++;
R_SUCCEED();
}
void UmountNsp(const fs::FsPath& mount) {
SCOPED_MUTEX(&g_mutex);
auto itr = std::ranges::find_if(g_entries, [&mount](auto& e){
return mount == e.mount;
});
if (itr == g_entries.end()) {
return;
}
if (itr->ref_count) {
itr->ref_count--;
}
if (!itr->ref_count) {
RemoveDevice(mount);
g_entries.erase(itr);
}
}
} // namespace sphaira::devoptab

View File

@@ -39,47 +39,7 @@ struct Dir {
u32 next_file;
};
bool fix_path(const char* str, char* out) {
// log_write("[SAVE] got path: %s\n", str);
str = std::strrchr(str, ':');
if (!str) {
return false;
}
// skip over ':'
str++;
size_t len = 0;
for (size_t i = 0; str[i]; i++) {
// skip multiple slashes.
if (i && str[i] == '/' && str[i - 1] == '/') {
continue;
}
// add leading slash.
if (!i && str[i] != '/') {
out[len++] = '/';
}
// save single char.
out[len++] = str[i];
}
// root path uses ""
if (len == 1 && out[0] == '/') {
// out[0] = '\0';
}
// null the end.
out[len] = '\0';
// log_write("[SAVE] end path: %s\n", out);
return true;
}
static int set_errno(struct _reent *r, int err) {
int set_errno(struct _reent *r, int err) {
r->_errno = err;
return -1;
}
@@ -242,8 +202,6 @@ int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) {
int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) {
auto device = (Device*)r->deviceData;
log_write("[\t\tDEV] lstat\n");
char path[FS_MAX_PATH];
if (!fix_path(_path, path)) {
return set_errno(r, ENOENT);
@@ -300,6 +258,47 @@ void MakeMountPath(u64 id, fs::FsPath& out_path) {
} // namespace
bool fix_path(const char* str, char* out) {
// log_write("[SAVE] got path: %s\n", str);
str = std::strrchr(str, ':');
if (!str) {
return false;
}
// skip over ':'
str++;
size_t len = 0;
// todo: hanle utf8 paths.
for (size_t i = 0; str[i]; i++) {
// skip multiple slashes.
if (i && str[i] == '/' && str[i - 1] == '/') {
continue;
}
// add leading slash.
if (!i && str[i] != '/') {
out[len++] = '/';
}
// save single char.
out[len++] = str[i];
}
// skip trailing slash.
if (len > 1 && out[len - 1] == '/') {
out[len - 1] = '\0';
}
// null the end.
out[len] = '\0';
// log_write("[SAVE] end path: %s\n", out);
return true;
}
Result MountFromSavePath(u64 id, fs::FsPath& out_path) {
SCOPED_MUTEX(&g_mutex);

View File

@@ -0,0 +1,304 @@
#include "utils/devoptab.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "yati/container/xci.hpp"
#include "yati/source/file.hpp"
#include <cstring>
#include <cstdio>
#include <cerrno>
#include <algorithm>
#include <sys/iosupport.h>
namespace sphaira::devoptab {
namespace {
struct Device {
std::unique_ptr<yati::source::Base> source;
yati::container::Xci::Partitions partitions;
};
struct File {
Device* device;
const yati::container::CollectionEntry* collection;
size_t off;
};
struct Dir {
Device* device;
const yati::container::Collections* collections;
u32 index;
};
int set_errno(struct _reent *r, int err) {
r->_errno = err;
return -1;
}
int devoptab_open(struct _reent *r, void *fileStruct, const char *_path, int flags, int mode) {
auto device = (Device*)r->deviceData;
auto file = static_cast<File*>(fileStruct);
std::memset(file, 0, sizeof(*file));
char path[FS_MAX_PATH];
if (!fix_path(_path, path)) {
return set_errno(r, ENOENT);
}
for (const auto& partition : device->partitions) {
for (const auto& collection : partition.collections) {
if (path == "/" + partition.name + "/" + collection.name) {
file->device = device;
file->collection = &collection;
return r->_errno = 0;
}
}
}
return set_errno(r, ENOENT);
}
int devoptab_close(struct _reent *r, void *fd) {
auto file = static_cast<File*>(fd);
std::memset(file, 0, sizeof(*file));
return r->_errno = 0;
}
ssize_t devoptab_read(struct _reent *r, void *fd, char *ptr, size_t len) {
auto file = static_cast<File*>(fd);
const auto& collection = file->collection;
len = std::min(len, collection->size - file->off);
u64 bytes_read;
if (R_FAILED(file->device->source->Read(ptr, collection->offset + file->off, len, &bytes_read))) {
return set_errno(r, ENOENT);
}
file->off += bytes_read;
return bytes_read;
}
off_t devoptab_seek(struct _reent *r, void *fd, off_t pos, int dir) {
auto file = static_cast<File*>(fd);
const auto& collection = file->collection;
if (dir == SEEK_CUR) {
pos += file->off;
} else if (dir == SEEK_END) {
pos = collection->size;
}
r->_errno = 0;
return file->off = std::clamp<u64>(pos, 0, collection->size);
}
int devoptab_fstat(struct _reent *r, void *fd, struct stat *st) {
auto file = static_cast<File*>(fd);
const auto& collection = file->collection;
std::memset(st, 0, sizeof(*st));
st->st_nlink = 1;
st->st_size = collection->size;
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
return r->_errno = 0;
}
DIR_ITER* devoptab_diropen(struct _reent *r, DIR_ITER *dirState, const char *_path) {
auto device = (Device*)r->deviceData;
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(dir, 0, sizeof(*dir));
char path[FS_MAX_PATH];
if (!fix_path(_path, path)) {
set_errno(r, ENOENT);
return NULL;
}
if (!std::strcmp(path, "/")) {
dir->device = device;
r->_errno = 0;
return dirState;
} else {
for (const auto& partition : device->partitions) {
if (path == "/" + partition.name) {
dir->collections = &partition.collections;
r->_errno = 0;
return dirState;
}
}
}
set_errno(r, ENOENT);
return NULL;
}
int devoptab_dirreset(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
dir->index = 0;
return r->_errno = 0;
}
int devoptab_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(filestat, 0, sizeof(*filestat));
if (!dir->collections) {
if (dir->index >= dir->device->partitions.size()) {
return set_errno(r, ENOENT);
}
filestat->st_nlink = 1;
filestat->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
std::strcpy(filename, dir->device->partitions[dir->index].name.c_str());
} else {
if (dir->index >= dir->collections->size()) {
return set_errno(r, ENOENT);
}
const auto& collection = (*dir->collections)[dir->index];
filestat->st_nlink = 1;
filestat->st_size = collection.size;
filestat->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
std::strcpy(filename, collection.name.c_str());
}
dir->index++;
return r->_errno = 0;
}
int devoptab_dirclose(struct _reent *r, DIR_ITER *dirState) {
auto dir = static_cast<Dir*>(dirState->dirStruct);
std::memset(dir, 0, sizeof(*dir));
return r->_errno = 0;
}
int devoptab_lstat(struct _reent *r, const char *_path, struct stat *st) {
auto device = (Device*)r->deviceData;
char path[FS_MAX_PATH];
if (!fix_path(_path, path)) {
return set_errno(r, ENOENT);
}
std::memset(st, 0, sizeof(*st));
st->st_nlink = 1;
if (!std::strcmp(path, "/")) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
} else {
for (const auto& partition : device->partitions) {
if (path == "/" + partition.name) {
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
return r->_errno = 0;
}
for (const auto& collection : partition.collections) {
if (path == "/" + partition.name + "/" + collection.name) {
st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
st->st_size = collection.size;
return r->_errno = 0;
}
}
}
}
return set_errno(r, ENOENT);
}
constexpr devoptab_t DEVOPTAB = {
.structSize = sizeof(File),
.open_r = devoptab_open,
.close_r = devoptab_close,
.read_r = devoptab_read,
.seek_r = devoptab_seek,
.fstat_r = devoptab_fstat,
.stat_r = devoptab_lstat,
.dirStateSize = sizeof(Dir),
.diropen_r = devoptab_diropen,
.dirreset_r = devoptab_dirreset,
.dirnext_r = devoptab_dirnext,
.dirclose_r = devoptab_dirclose,
.lstat_r = devoptab_lstat,
};
struct Entry {
Device device;
devoptab_t devoptab;
fs::FsPath path;
fs::FsPath mount;
char name[32];
s32 ref_count;
};
Mutex g_mutex;
std::vector<Entry> g_entries;
u32 g_mount_idx;
} // namespace
Result MountXci(fs::Fs* fs, const fs::FsPath& path, fs::FsPath& out_path) {
SCOPED_MUTEX(&g_mutex);
// check if we already have the save mounted.
for (auto& e : g_entries) {
if (e.path == path) {
e.ref_count++;
out_path = e.mount;
R_SUCCEED();
}
}
// create new entry.
auto& entry = g_entries.emplace_back();
auto source = std::make_unique<yati::source::File>(fs, path);
yati::container::Xci xci{source.get()};
R_TRY(xci.GetPartitions(entry.device.partitions));
entry.path = path;
entry.devoptab = DEVOPTAB;
entry.devoptab.name = entry.name;
entry.devoptab.deviceData = &entry.device;
entry.device.source = std::move(source);
std::snprintf(entry.name, sizeof(entry.name), "xci_%u", g_mount_idx);
std::snprintf(entry.mount, sizeof(entry.mount), "xci_%u:/", g_mount_idx);
R_UNLESS(AddDevice(&entry.devoptab) >= 0, 0x1);
log_write("[XCI] DEVICE SUCCESS %s %s\n", path.s, entry.name);
out_path = entry.mount;
entry.ref_count++;
g_mount_idx++;
R_SUCCEED();
}
void UmountXci(const fs::FsPath& mount) {
SCOPED_MUTEX(&g_mutex);
auto itr = std::ranges::find_if(g_entries, [&mount](auto& e){
return mount == e.mount;
});
if (itr == g_entries.end()) {
return;
}
if (itr->ref_count) {
itr->ref_count--;
}
if (!itr->ref_count) {
RemoveDevice(mount);
g_entries.erase(itr);
}
}
} // namespace sphaira::devoptab

View File

@@ -58,8 +58,40 @@ Result Hfs0GetPartition(source::Base* source, s64 off, Hfs0& out) {
R_SUCCEED();
}
Result ReadPartitionIntoCollection(source::Base* source, const Hfs0& root, u32 index, Collections& out) {
log_write("[XCI] fetching %s partition\n", root.string_table[index].c_str());
Hfs0 hfs0{};
R_TRY(Hfs0GetPartition(source, root.data_offset + root.file_table[index].data_offset, hfs0));
log_write("[XCI] got %s partition\n", root.string_table[index].c_str());
for (u32 i = 0; i < hfs0.header.total_files; i++) {
CollectionEntry entry;
entry.name = hfs0.string_table[i];
entry.offset = hfs0.data_offset + hfs0.file_table[i].data_offset;
entry.size = hfs0.file_table[i].data_size;
out.emplace_back(entry);
}
log_write("[XCI] read %s partition count: %zu\n", root.string_table[index].c_str(), out.size());
R_SUCCEED();
}
} // namespace
Result Xci::GetPartitions(Partitions& out) {
Hfs0 root{};
R_TRY(Hfs0GetPartition(m_source, HFS0_HEADER_OFFSET, root));
for (u32 i = 0; i < root.header.total_files; i++) {
Partition partition{root.string_table[i]};
R_TRY(ReadPartitionIntoCollection(m_source, root, i, partition.collections));
out.emplace_back(partition);
}
R_SUCCEED();
}
Result Xci::GetCollections(Collections& out) {
Hfs0 root{};
R_TRY(Hfs0GetPartition(m_source, HFS0_HEADER_OFFSET, root));
@@ -68,20 +100,7 @@ Result Xci::GetCollections(Collections& out) {
for (u32 i = 0; i < root.header.total_files; i++) {
if (root.string_table[i] == "secure") {
log_write("[XCI] found secure partition\n");
Hfs0 secure{};
R_TRY(Hfs0GetPartition(m_source, root.data_offset + root.file_table[i].data_offset, secure));
log_write("[XCI] got secure partition\n");
for (u32 i = 0; i < secure.header.total_files; i++) {
CollectionEntry entry;
entry.name = secure.string_table[i];
entry.offset = secure.data_offset + secure.file_table[i].data_offset;
entry.size = secure.file_table[i].data_size;
out.emplace_back(entry);
}
R_SUCCEED();
return ReadPartitionIntoCollection(m_source, root, i, out);
} else {
log_write("[XCI] skipping partition %u | %s\n", i, root.string_table[i].c_str());
}

View File

@@ -646,7 +646,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
if ((config.convert_to_standard_crypto && ticket) || config.lower_master_key) {
t->nca->modified = true;
u8 keak_generation;
u8 keak_generation = 0;
if (ticket) {
const auto key_gen = header.key_gen;
@@ -673,10 +673,6 @@ Result Yati::decompressFuncInternal(ThreadData* t) {
R_TRY(nca::DecryptKeak(keys, header));
}
if (config.lower_master_key) {
keak_generation = 0;
}
R_TRY(nca::EncryptKeak(keys, header, keak_generation));
std::memset(&header.rights_id, 0, sizeof(header.rights_id));
}