/* * 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 . * * 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 "io.hpp" #include "main.hpp" #include 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; 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); 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); return; } fseek(src, 0, SEEK_END); u64 sz = ftell(src); rewind(src); u8* buf = new u8[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 = fread((char*)buf, 1, BUFFER_SIZE, src); fwrite((char*)buf, 1, count, dst); offset += count; } delete[] buf; fclose(src); fclose(dst); 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(), 777); 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; } std::tuple io::backup(size_t index, AccountUid uid) { Result res = 0; std::tuple 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]); FsFileSystem fileSystem; res = FileSystem::mount(&fileSystem, title.id(), title.userId()); if (R_SUCCEEDED(res)) { int rc = FileSystem::mount(fileSystem); if (rc == -1) { 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."); } } 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."); } std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(title.userId()))); std::string dstPath = title.path() + "/" + suggestion; // 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()); } io::createDirectory(tmpPath); res = io::copyDirectory("save:/", tmpPath + "/"); 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."); } // Swap: delete old backup only after new one is fully written. if (io::directoryExists(dstPath)) { io::deleteFolderRecursively((dstPath + "/").c_str()); } if (rename(tmpPath.c_str(), dstPath.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."); } refreshDirectories(title.id()); FileSystem::unmount(); ret = std::make_tuple(true, 0, dstPath); Logger::getInstance().log(Logger::INFO, "Backup succeeded."); return ret; } std::tuple io::restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell) { Result res = 0; std::tuple ret = std::make_tuple(false, -1, ""); 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]); // Если сейв ещё не существует (игра не запускалась) — создаём его через NACP. // fsCreateSaveDataFileSystem возвращает ошибку если сейв уже есть — это нормально. { 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}; 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); } } FsFileSystem fileSystem; res = FileSystem::mount(&fileSystem, title.id(), uid); if (R_SUCCEEDED(res)) { int rc = FileSystem::mount(fileSystem); if (rc == -1) { 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."); } std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(uid))); std::string srcPath = title.path() + "/" + suggestion + "/"; std::string dstPath = "save:/"; // Validate source exists and is non-empty before touching live save data. { Directory srcCheck(srcPath); if (!srcCheck.good() || srcCheck.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."); } } { 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()); } else { std::remove((dstPath + saveRoot.entry(i)).c_str()); } } } 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."); } res = io::copyDirectory(srcPath, dstPath); 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."); } 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(); Logger::getInstance().log(Logger::INFO, "Restore succeeded."); return ret; }