Files
NXST/src/infra/fs/io.cpp
T
DragonSpirit dc65a4c8a9 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>
2026-04-27 01:27:16 +03:00

331 lines
12 KiB
C++

/*
* This file is part of Checkpoint
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
* * Requiring preservation of specified reasonable legal notices or
* author attributions in that material or in the Appropriate Legal
* Notices displayed by works containing it.
* * Prohibiting misrepresentation of the origin of that material,
* or requiring that modified versions of such material be marked in
* reasonable ways as different from the original version.
*/
#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)
{
struct stat buffer;
return (stat(path.c_str(), &buffer) == 0);
}
void io::copyFile(const std::string& srcPath, const std::string& dstPath)
{
g_isTransferringFile = true;
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;
}
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.get(), 0, SEEK_END);
u64 sz = (u64)ftell(src.get());
rewind(src.get());
std::vector<u8> buf(BUFFER_SIZE);
u64 offset = 0;
size_t slashpos = srcPath.rfind('/');
g_currentFile = srcPath.substr(slashpos + 1, srcPath.length() - slashpos - 1);
while (offset < sz) {
u32 count = (u32)fread(buf.data(), 1, BUFFER_SIZE, src.get());
if (count == 0) {
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(buf.data(), 1, count, dst.get());
offset += count;
}
if (dstPath.rfind("save:/", 0) == 0) {
fsdevCommitDevice("save");
}
g_isTransferringFile = false;
}
Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath)
{
Result res = 0;
bool quit = false;
Directory items(srcPath);
if (!items.good()) {
return items.error();
}
for (size_t i = 0, sz = items.size(); i < sz && !quit; i++) {
std::string newsrc = srcPath + items.entry(i);
std::string newdst = dstPath + items.entry(i);
if (items.folder(i)) {
res = io::createDirectory(newdst);
if (R_SUCCEEDED(res)) {
newsrc += "/";
newdst += "/";
res = io::copyDirectory(newsrc, newdst);
}
else {
quit = true;
}
}
else {
io::copyFile(newsrc, newdst);
}
}
return 0;
}
Result io::createDirectory(const std::string& path)
{
mkdir(path.c_str(), 0777);
return 0;
}
bool io::directoryExists(const std::string& path)
{
struct stat sb;
return (stat(path.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode));
}
Result io::deleteFolderRecursively(const std::string& path)
{
Directory dir(path);
if (!dir.good()) {
return dir.error();
}
for (size_t i = 0, sz = dir.size(); i < sz; i++) {
if (dir.folder(i)) {
std::string newpath = path + "/" + dir.entry(i) + "/";
deleteFolderRecursively(newpath);
newpath = path + dir.entry(i);
rmdir(newpath.c_str());
}
else {
std::string newpath = path + dir.entry(i);
std::remove(newpath.c_str());
}
}
rmdir(path.c_str());
return 0;
}
nxst::Result<std::string> io::backup(size_t index, AccountUid uid)
{
Title title;
getTitle(title, uid, index);
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]);
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.");
}
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 dst_path = title.path() + "/" + suggestion;
std::string tmp_path = dst_path + ".tmp";
if (io::directoryExists(tmp_path)) {
io::deleteFolderRecursively(tmp_path + "/");
}
io::createDirectory(tmp_path);
res = io::copyDirectory("save:/", tmp_path + "/");
if (R_FAILED(res)) {
FileSystem::unmount();
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.");
}
if (io::directoryExists(dst_path)) {
io::deleteFolderRecursively(dst_path + "/");
}
if (rename(tmp_path.c_str(), dst_path.c_str()) != 0) {
FileSystem::unmount();
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();
nxst::log::info("Backup succeeded.");
return nxst::Result<std::string>::success(dst_path);
}
// Creates the save data filesystem for a title if it doesn't exist yet.
static void createSaveIfNeeded(u64 title_id, AccountUid uid)
{
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);
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]);
createSaveIfNeeded(title.id(), uid);
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;
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 src_path = title.path() + "/" + suggestion + "/";
const std::string dst_path = "save:/";
{
Directory src_check(src_path);
if (!src_check.good() || src_check.size() == 0) {
FileSystem::unmount();
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 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((dst_path + save_root.entry(i)).c_str());
}
}
}
res = fsdevCommitDevice("save");
if (R_FAILED(res)) {
FileSystem::unmount();
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(src_path, dst_path);
if (R_FAILED(res)) {
FileSystem::unmount();
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)) {
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();
nxst::log::info("Restore succeeded.");
return nxst::Result<std::string>::success(nameFromCell + "\nhas been restored successfully.");
}