refactor: phases 5 & 6 — TransferService, Result<T>, RAII

Phase 5 — TransferService extraction:
- New nxst::TransferService owns all sender/receiver state, threads, and atomics
  (previously 11 file-statics across transfer_sender.cpp + transfer_receiver.cpp)
- startReceive() calls io::restore internally; UI never touches infra/fs or infra/net
- TitlesLayout routes all transfer ops through mainApp->transfer
- g_currentUId removed from global scope; TitlesLayout::InitTitles(AccountUid)
- MainApplication gains transfer member; layout refs renamed to snake_case
- userAppExit() updated to cancel via service

Phase 6 — Result<T> + RAII:
- include/nxst/domain/result.hpp: 85-line tagged-union Result<T,E> + Result<void,E>
- include/nxst/infra/fs/handles.hpp: FsFileSystemHandle (auto fsFsClose),
  FileHandle (auto fclose) — eliminates manual close on every error path
- io::backup / io::restore return nxst::Result<std::string> (was tuple<bool,Result,string>)
- new u8[] + malloc in copyFile/restore replaced with std::vector<u8>
- NACP save-creation extracted to createSaveIfNeeded() helper in io.cpp

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-27 01:27:16 +03:00
parent 895fee6235
commit dc65a4c8a9
6 changed files with 270 additions and 152 deletions
+133 -136
View File
@@ -25,8 +25,10 @@
*/
#include <nxst/infra/fs/io.hpp>
#include <nxst/infra/fs/handles.hpp>
#include <nxst/app/main.hpp>
#include <nxst/infra/sys/logger.hpp>
#include <vector>
bool io::fileExists(const std::string& path)
{
@@ -38,42 +40,43 @@ void io::copyFile(const std::string& srcPath, const std::string& dstPath)
{
g_isTransferringFile = true;
FILE* src = fopen(srcPath.c_str(), "rb");
if (src == NULL) {
Logger::getInstance().log(Logger::ERROR, "Failed to open source file " + srcPath + " during copy with errno %d. Skipping...", errno);
nxst::FileHandle src(fopen(srcPath.c_str(), "rb"));
if (!src) {
nxst::log::error("Failed to open source file %s during copy with errno %d. Skipping...",
srcPath.c_str(), errno);
g_isTransferringFile = false;
return;
}
FILE* dst = fopen(dstPath.c_str(), "wb");
if (dst == NULL) {
Logger::getInstance().log(Logger::ERROR, "Failed to open destination file " + dstPath + " during copy with errno " + std::to_string(errno) + ". Skipping...");
fclose(src);
nxst::FileHandle dst(fopen(dstPath.c_str(), "wb"));
if (!dst) {
nxst::log::error("Failed to open destination file %s during copy with errno %d. Skipping...",
dstPath.c_str(), errno);
g_isTransferringFile = false;
return;
}
fseek(src, 0, SEEK_END);
u64 sz = ftell(src);
rewind(src);
fseek(src.get(), 0, SEEK_END);
u64 sz = (u64)ftell(src.get());
rewind(src.get());
u8* buf = new u8[BUFFER_SIZE];
std::vector<u8> buf(BUFFER_SIZE);
u64 offset = 0;
size_t slashpos = srcPath.rfind("/");
size_t slashpos = srcPath.rfind('/');
g_currentFile = srcPath.substr(slashpos + 1, srcPath.length() - slashpos - 1);
while (offset < sz) {
u32 count = fread((char*)buf, 1, BUFFER_SIZE, src);
u32 count = (u32)fread(buf.data(), 1, BUFFER_SIZE, src.get());
if (count == 0) {
Logger::getInstance().log(Logger::ERROR, "fread returned 0 for file {} at offset {}/{} with errno {}. Aborting copy.", srcPath, offset, sz, errno);
nxst::log::error("fread returned 0 for %s at offset %llu/%llu (errno %d). Aborting.",
srcPath.c_str(), (unsigned long long)offset,
(unsigned long long)sz, errno);
break;
}
fwrite((char*)buf, 1, count, dst);
fwrite(buf.data(), 1, count, dst.get());
offset += count;
}
delete[] buf;
fclose(src);
fclose(dst);
if (dstPath.rfind("save:/", 0) == 0) {
fsdevCommitDevice("save");
}
@@ -150,154 +153,150 @@ Result io::deleteFolderRecursively(const std::string& path)
return 0;
}
std::tuple<bool, Result, std::string> io::backup(size_t index, AccountUid uid)
nxst::Result<std::string> io::backup(size_t index, AccountUid uid)
{
Result res = 0;
std::tuple<bool, Result, std::string> ret = std::make_tuple(false, -1, "");
Title title;
getTitle(title, uid, index);
Logger::getInstance().log(Logger::INFO, "Started backup of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(), title.id(),
title.userId().uid[1], title.userId().uid[0]);
nxst::log::info("Started backup of %s. Title id: 0x%016lX; User id: 0x%lX%lX.",
title.name().c_str(), title.id(),
title.userId().uid[1], title.userId().uid[0]);
FsFileSystem fileSystem;
res = FileSystem::mount(&fileSystem, title.id(), title.userId());
if (R_SUCCEEDED(res)) {
int rc = FileSystem::mount(fileSystem);
if (rc == -1) {
fsFsClose(&fileSystem);
FileSystem::unmount();
Logger::getInstance().log(Logger::ERROR, "Failed to mount filesystem during backup. Title id: 0x%016lX; User id: 0x%lX%lX.", title.id(),
title.userId().uid[1], title.userId().uid[0]);
return std::make_tuple(false, -2, "Failed to mount save.");
}
nxst::FsFileSystemHandle fsHandle;
Result res = FileSystem::mount(fsHandle.get(), title.id(), title.userId());
if (R_FAILED(res)) {
nxst::log::error("Failed to mount filesystem during backup with result 0x%08lX. "
"Title id: 0x%016lX; User id: 0x%lX%lX.",
res, title.id(), title.userId().uid[1], title.userId().uid[0]);
return nxst::Result<std::string>::failure("Failed to mount save.");
}
else {
Logger::getInstance().log(Logger::ERROR,
"Failed to mount filesystem during backup with result 0x%08lX. Title id: 0x%016lX; User id: 0x%lX%lX.", res, title.id(),
title.userId().uid[1], title.userId().uid[0]);
return std::make_tuple(false, res, "Failed to mount save.");
fsHandle.valid = true;
if (FileSystem::mount(*fsHandle.get()) == -1) {
nxst::log::error("Failed to mount devfs during backup. Title id: 0x%016lX; User id: 0x%lX%lX.",
title.id(), title.userId().uid[1], title.userId().uid[0]);
FileSystem::unmount();
return nxst::Result<std::string>::failure("Failed to mount save.");
}
fsHandle.release(); // devfs now owns the kernel handle
std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(title.userId())));
io::createDirectory(title.path());
std::string dstPath = title.path() + "/" + suggestion;
std::string dst_path = title.path() + "/" + suggestion;
std::string tmp_path = dst_path + ".tmp";
// Write to a temp dir first; rename on success so the existing backup
// is never destroyed if the copy is interrupted mid-way.
std::string tmpPath = dstPath + ".tmp";
if (io::directoryExists(tmpPath)) {
io::deleteFolderRecursively((tmpPath + "/").c_str());
if (io::directoryExists(tmp_path)) {
io::deleteFolderRecursively(tmp_path + "/");
}
io::createDirectory(tmpPath);
res = io::copyDirectory("save:/", tmpPath + "/");
io::createDirectory(tmp_path);
res = io::copyDirectory("save:/", tmp_path + "/");
if (R_FAILED(res)) {
FileSystem::unmount();
io::deleteFolderRecursively((tmpPath + "/").c_str());
Logger::getInstance().log(Logger::ERROR, "Failed to copy directory to " + tmpPath + " with result 0x%08lX.", res);
return std::make_tuple(false, res, "Failed to backup save.");
io::deleteFolderRecursively(tmp_path + "/");
nxst::log::error("Failed to copy directory to %s with result 0x%08lX.", tmp_path.c_str(), res);
return nxst::Result<std::string>::failure("Failed to backup save.");
}
// Swap: delete old backup only after new one is fully written.
if (io::directoryExists(dstPath)) {
io::deleteFolderRecursively((dstPath + "/").c_str());
if (io::directoryExists(dst_path)) {
io::deleteFolderRecursively(dst_path + "/");
}
if (rename(tmpPath.c_str(), dstPath.c_str()) != 0) {
if (rename(tmp_path.c_str(), dst_path.c_str()) != 0) {
FileSystem::unmount();
Logger::getInstance().log(Logger::ERROR, "Failed to rename temp backup to " + dstPath);
return std::make_tuple(false, (Result)-1, "Failed to finalise backup.");
nxst::log::error("Failed to rename temp backup to %s.", dst_path.c_str());
return nxst::Result<std::string>::failure("Failed to finalise backup.");
}
refreshDirectories(title.id());
FileSystem::unmount();
ret = std::make_tuple(true, 0, dstPath);
Logger::getInstance().log(Logger::INFO, "Backup succeeded.");
return ret;
nxst::log::info("Backup succeeded.");
return nxst::Result<std::string>::success(dst_path);
}
std::tuple<bool, Result, std::string> io::restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell)
// Creates the save data filesystem for a title if it doesn't exist yet.
static void createSaveIfNeeded(u64 title_id, AccountUid uid)
{
Result res = 0;
std::tuple<bool, Result, std::string> ret = std::make_tuple(false, -1, "");
std::vector<u8> nsacd_buf(sizeof(NsApplicationControlData), 0);
auto* nsacd = reinterpret_cast<NsApplicationControlData*>(nsacd_buf.data());
size_t outsize = 0;
if (!R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage,
title_id, nsacd,
sizeof(NsApplicationControlData), &outsize))) {
return;
}
static const FsSaveDataMetaInfo meta = {.size = 0x40060, .type = FsSaveDataMetaType_Thumbnail};
FsSaveDataAttribute attr = {};
attr.application_id = title_id;
attr.uid = uid;
attr.save_data_type = FsSaveDataType_Account;
attr.save_data_rank = FsSaveDataRank_Primary;
FsSaveDataCreationInfo create_info = {};
create_info.save_data_size = (s64)nsacd->nacp.user_account_save_data_size;
create_info.journal_size = (s64)nsacd->nacp.user_account_save_data_journal_size;
create_info.available_size = 0x4000;
create_info.owner_id = nsacd->nacp.save_data_owner_id;
create_info.save_data_space_id = FsSaveDataSpaceId_User;
fsCreateSaveDataFileSystem(&attr, &create_info, &meta);
}
nxst::Result<std::string> io::restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell)
{
(void)cellIndex;
Title title;
getTitle(title, uid, index);
Logger::getInstance().log(Logger::INFO, "Started restore of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(), title.id(),
title.userId().uid[1], title.userId().uid[0]);
nxst::log::info("Started restore of %s. Title id: 0x%016lX; User id: 0x%lX%lX.",
title.name().c_str(), title.id(),
title.userId().uid[1], title.userId().uid[0]);
// If save data does not yet exist (game was never launched), create it via NACP.
// fsCreateSaveDataFileSystem returns an error if the save already exists — this is expected.
{
NsApplicationControlData* nsacd = (NsApplicationControlData*)malloc(sizeof(NsApplicationControlData));
if (nsacd != NULL) {
memset(nsacd, 0, sizeof(NsApplicationControlData));
size_t outsize = 0;
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage, title.id(), nsacd, sizeof(NsApplicationControlData), &outsize))) {
static const FsSaveDataMetaInfo meta = {.size = 0x40060, .type = FsSaveDataMetaType_Thumbnail};
createSaveIfNeeded(title.id(), uid);
FsSaveDataAttribute attr = {};
attr.application_id = title.id();
attr.uid = uid;
attr.save_data_type = FsSaveDataType_Account;
attr.save_data_rank = FsSaveDataRank_Primary;
FsSaveDataCreationInfo createInfo = {};
createInfo.save_data_size = (s64)nsacd->nacp.user_account_save_data_size;
createInfo.journal_size = (s64)nsacd->nacp.user_account_save_data_journal_size;
createInfo.available_size = 0x4000;
createInfo.owner_id = nsacd->nacp.save_data_owner_id;
createInfo.save_data_space_id = FsSaveDataSpaceId_User;
fsCreateSaveDataFileSystem(&attr, &createInfo, &meta);
}
free(nsacd);
}
nxst::FsFileSystemHandle fsHandle;
Result res = FileSystem::mount(fsHandle.get(), title.id(), uid);
if (R_FAILED(res)) {
nxst::log::error("Failed to mount filesystem during restore with result 0x%08lX. "
"Title id: 0x%016lX; User id: 0x%lX%lX.",
res, title.id(), uid.uid[1], uid.uid[0]);
return nxst::Result<std::string>::failure("Failed to mount save.");
}
fsHandle.valid = true;
FsFileSystem fileSystem;
res = FileSystem::mount(&fileSystem, title.id(), uid);
if (R_SUCCEEDED(res)) {
int rc = FileSystem::mount(fileSystem);
if (rc == -1) {
fsFsClose(&fileSystem);
FileSystem::unmount();
Logger::getInstance().log(Logger::ERROR, "Failed to mount filesystem during restore. Title id: 0x%016lX; User id: 0x%lX%lX.", title.id(),
uid.uid[1], uid.uid[0]);
return std::make_tuple(false, -2, "Failed to mount save.");
}
}
else {
Logger::getInstance().log(Logger::ERROR,
"Failed to mount filesystem during restore with result 0x%08lX. Title id: 0x%016lX; User id: 0x%lX%lX.", res, title.id(),
uid.uid[1], uid.uid[0]);
return std::make_tuple(false, res, "Failed to mount save.");
if (FileSystem::mount(*fsHandle.get()) == -1) {
nxst::log::error("Failed to mount devfs during restore. Title id: 0x%016lX; User id: 0x%lX%lX.",
title.id(), uid.uid[1], uid.uid[0]);
FileSystem::unmount();
return nxst::Result<std::string>::failure("Failed to mount save.");
}
fsHandle.release(); // devfs now owns the kernel handle
std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(uid)));
std::string srcPath = title.path() + "/" + suggestion + "/";
std::string dstPath = "save:/";
std::string src_path = title.path() + "/" + suggestion + "/";
const std::string dst_path = "save:/";
// Validate source exists and is non-empty before touching live save data.
{
Directory srcCheck(srcPath);
if (!srcCheck.good() || srcCheck.size() == 0) {
Directory src_check(src_path);
if (!src_check.good() || src_check.size() == 0) {
FileSystem::unmount();
Logger::getInstance().log(Logger::ERROR, "Restore source is empty or missing: " + srcPath);
return std::make_tuple(false, (Result)-1, "Restore source is empty or missing.");
nxst::log::error("Restore source is empty or missing: %s", src_path.c_str());
return nxst::Result<std::string>::failure("Restore source is empty or missing.");
}
}
{
Directory saveRoot(dstPath);
for (size_t i = 0, sz = saveRoot.size(); i < sz; i++) {
if (saveRoot.folder(i)) {
io::deleteFolderRecursively((dstPath + saveRoot.entry(i) + "/").c_str());
rmdir((dstPath + saveRoot.entry(i)).c_str());
Directory save_root(dst_path);
for (size_t i = 0, sz = save_root.size(); i < sz; i++) {
if (save_root.folder(i)) {
io::deleteFolderRecursively(dst_path + save_root.entry(i) + "/");
rmdir((dst_path + save_root.entry(i)).c_str());
} else {
std::remove((dstPath + saveRoot.entry(i)).c_str());
std::remove((dst_path + save_root.entry(i)).c_str());
}
}
}
@@ -305,29 +304,27 @@ std::tuple<bool, Result, std::string> io::restore(size_t index, AccountUid uid,
res = fsdevCommitDevice("save");
if (R_FAILED(res)) {
FileSystem::unmount();
Logger::getInstance().log(Logger::ERROR, "Failed to commit save after clearing with result 0x%08lX.", res);
return std::make_tuple(false, res, "Failed to commit save after delete.");
nxst::log::error("Failed to commit save after clearing with result 0x%08lX.", res);
return nxst::Result<std::string>::failure("Failed to commit save after delete.");
}
res = io::copyDirectory(srcPath, dstPath);
res = io::copyDirectory(src_path, dst_path);
if (R_FAILED(res)) {
FileSystem::unmount();
Logger::getInstance().log(Logger::ERROR, "Failed to copy directory " + srcPath + " to " + dstPath + " with result 0x%08lX. Skipping...", res);
return std::make_tuple(false, res, "Failed to restore save.");
nxst::log::error("Failed to copy %s to save:/ with result 0x%08lX.", src_path.c_str(), res);
return nxst::Result<std::string>::failure("Failed to restore save.");
}
res = fsdevCommitDevice("save");
if (R_FAILED(res)) {
Logger::getInstance().log(Logger::ERROR, "Failed to commit save with result 0x%08lX.", res);
return std::make_tuple(false, res, "Failed to commit to save device.");
}
else {
blinkLed(4);
ret = std::make_tuple(true, 0, nameFromCell + "\nhas been restored successfully.");
FileSystem::unmount();
nxst::log::error("Failed to commit save with result 0x%08lX.", res);
return nxst::Result<std::string>::failure("Failed to commit to save device.");
}
blinkLed(4);
FileSystem::unmount();
Logger::getInstance().log(Logger::INFO, "Restore succeeded.");
return ret;
}
nxst::log::info("Restore succeeded.");
return nxst::Result<std::string>::success(nameFromCell + "\nhas been restored successfully.");
}
+5 -5
View File
@@ -205,11 +205,11 @@ void TransferService::runSender(size_t title_index, AccountUid uid) {
sender_state.setStatus("Creating backup...");
#ifdef __SWITCH__
auto backup_result = io::backup(title_index, uid);
if (!std::get<0>(backup_result)) {
failSend("Failed to create backup:\n" + std::get<2>(backup_result));
if (!backup_result.isOk()) {
failSend("Failed to create backup:\n" + backup_result.error());
return finish();
}
fs::path directory = std::get<2>(backup_result);
fs::path directory = backup_result.value();
#else
fs::path directory = ".";
(void)title_index; (void)uid;
@@ -414,8 +414,8 @@ void TransferService::runAccept(int server_fd) {
#ifdef __SWITCH__
receiver_state.setStatus("Restoring...");
auto result = io::restore(restore_title_index, restore_uid, 0, restore_title_name);
restore_ok = std::get<0>(result);
restore_error = restore_ok ? "" : std::get<2>(result);
restore_ok = result.isOk();
restore_error = result.isOk() ? "" : result.error();
#else
restore_ok = true;
#endif