mirror of
https://github.com/niakw/Prospero.Eden-Encore.git
synced 2026-10-17 13:01:16 +02:00
185 lines
10 KiB
C++
185 lines
10 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Host check for save transfer (ryujinx_saves.h): import from a generated Ryujinx data folder and
|
|
// from hand-copied save folders, and export.
|
|
#include "ryujinx_saves.h"
|
|
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <iterator>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
namespace fs = std::filesystem;
|
|
using namespace Eden::RyujinxSaves;
|
|
|
|
void require(bool condition, const char* what) {
|
|
if (!condition) {
|
|
std::fprintf(stderr, "Ryujinx save import FAIL: %s\n", what);
|
|
std::exit(1);
|
|
}
|
|
}
|
|
|
|
void write_text(const fs::path& path, const std::string& text) {
|
|
fs::create_directories(path.parent_path());
|
|
std::ofstream(path, std::ios::binary) << text;
|
|
}
|
|
|
|
std::string read_text(const fs::path& path) {
|
|
std::ifstream in(path, std::ios::binary);
|
|
return {std::istreambuf_iterator<char>(in), {}};
|
|
}
|
|
|
|
// One IMEN entry as Ryujinx writes it: key (program ID, user ID, type) and value (save ID).
|
|
void add_entry(std::vector<char>& index, uint64_t program, uint64_t user_low, uint8_t type, uint64_t save_id) {
|
|
std::vector<char> item(0x8C, 0);
|
|
std::memcpy(item.data(), "IMEN", 4);
|
|
const uint32_t size = 0x40;
|
|
std::memcpy(item.data() + 4, &size, 4);
|
|
std::memcpy(item.data() + 8, &size, 4);
|
|
std::memcpy(item.data() + 0x0C, &program, 8);
|
|
std::memcpy(item.data() + 0x14, &user_low, 8);
|
|
item[0x2C] = static_cast<char>(type);
|
|
std::memcpy(item.data() + 0x4C, &save_id, 8);
|
|
index.insert(index.end(), item.begin(), item.end());
|
|
}
|
|
} // namespace
|
|
|
|
int main() {
|
|
char pattern[] = "/tmp/ryujinx-check-XXXXXX";
|
|
require(mkdtemp(pattern) != nullptr, "temporary folder");
|
|
const fs::path base = pattern;
|
|
const uint64_t game = 0x0100AAAA00000000ULL, other_game = 0x0100BBBB00000000ULL;
|
|
|
|
// Ryujinx portable folder: game has an account save (first user), a second user's account save,
|
|
// a device save and a cache save; other_game's only save folder is missing.
|
|
const fs::path ryujinx = base / "ryujinx", root = ryujinx / "portable";
|
|
std::vector<char> index(12, 0);
|
|
std::memcpy(index.data(), "IMKV", 4);
|
|
add_entry(index, game, 1, 1, 0x1);
|
|
add_entry(index, game, 2, 1, 0x3);
|
|
add_entry(index, game, 0, 3, 0x2);
|
|
add_entry(index, game, 0, 5, 0x5);
|
|
add_entry(index, other_game, 1, 1, 0x4);
|
|
fs::create_directories(IndexPath(root).parent_path());
|
|
std::ofstream(IndexPath(root), std::ios::binary).write(index.data(), static_cast<std::streamsize>(index.size()));
|
|
const fs::path saves_root = root / "bis" / "user" / "save";
|
|
write_text(saves_root / "0000000000000001" / "0" / "save.bin", "account-first-user");
|
|
write_text(saves_root / "0000000000000001" / "0" / "sub" / "x.dat", "nested");
|
|
write_text(saves_root / "0000000000000003" / "0" / "save.bin", "account-second-user");
|
|
write_text(saves_root / "0000000000000002" / "0" / "device.bin", "device");
|
|
write_text(saves_root / "0000000000000005" / "0" / "cache.bin", "cache");
|
|
|
|
require(DataRoot(ryujinx) == root, "portable folder found");
|
|
require(DataRoot(root) == root, "data folder itself found");
|
|
std::string error;
|
|
const auto saves = FindSaves(ryujinx, game, error);
|
|
require(saves.size() == 2, "account and device saves only");
|
|
require(saves[0].kind == Kind::Account && saves[0].folder == saves_root / "0000000000000001" / "0",
|
|
"first user's account save");
|
|
require(saves[1].kind == Kind::Device && saves[1].folder == saves_root / "0000000000000002" / "0", "device save");
|
|
require(FindSaves(ryujinx, other_game, error).empty() && error == "none for this game", "missing save folder");
|
|
require(FindSaves(base / "absent", game, error).empty() && error == "no Ryujinx data", "no data folder");
|
|
|
|
// Import over an existing account save: it moves to the backup; the device folder is new.
|
|
const fs::path users = base / "nand" / "user" / "save" / "0000000000000000";
|
|
const fs::path account = users / "00112233445566778899AABBCCDDEEFF" / "0100AAAA00000000";
|
|
const fs::path device = users / "00000000000000000000000000000000" / "0100AAAA00000000";
|
|
const fs::path backups = base / "backup";
|
|
write_text(account / "old.bin", "previous");
|
|
bool replaced = false;
|
|
require(Import(saves, account, device, backups, "0100AAAA00000000-1", error, &replaced) && replaced, "import");
|
|
require(read_text(account / "save.bin") == "account-first-user", "account save copied");
|
|
require(read_text(account / "sub" / "x.dat") == "nested", "nested file copied");
|
|
require(!fs::exists(account / "old.bin"), "old save replaced");
|
|
require(read_text(device / "device.bin") == "device", "device save copied");
|
|
require(read_text(backups / "0100AAAA00000000-1-account" / "old.bin") == "previous", "old save kept");
|
|
require(!fs::exists(backups / "0100AAAA00000000-1-device"), "no device backup without a device save");
|
|
|
|
// A failed copy leaves the current save in place and no partial copy behind.
|
|
const std::vector<Save> broken{{Kind::Account, base / "absent" / "0"}};
|
|
require(!Import(broken, account, device, backups, "0100AAAA00000000-2", error), "failed copy reported");
|
|
require(read_text(account / "save.bin") == "account-first-user", "current save restored");
|
|
require(!fs::exists(backups / "0100AAAA00000000-2-account"), "backup moved back");
|
|
|
|
// A damaged index (a cut-off entry) is reported, not read past; an empty one has no saves.
|
|
std::ofstream(IndexPath(root), std::ios::binary) << "IMKV----------IMEN-cut";
|
|
require(FindSaves(ryujinx, game, error).empty() && error == "save index unreadable", "damaged index");
|
|
std::ofstream(IndexPath(root), std::ios::binary) << "IMKV--------";
|
|
require(FindSaves(ryujinx, game, error).empty() && error == "none for this game", "empty index");
|
|
|
|
// A save folder copied by hand: its files are the account save; the title ID in either case.
|
|
const fs::path by_hand = base / "save-import";
|
|
write_text(by_hand / "0100aaaa00000000" / "save.bin", "hand-copied");
|
|
auto folder_saves = FindFolderSaves(by_hand, game, error);
|
|
require(folder_saves.size() == 1 && folder_saves[0].kind == Kind::Account &&
|
|
folder_saves[0].folder == by_hand / "0100aaaa00000000", "hand-copied account save");
|
|
require(FindFolderSaves(by_hand, other_game, error).empty() && error == "none for this game",
|
|
"no folder for another game");
|
|
fs::create_directories(by_hand / "0100BBBB00000000");
|
|
require(FindFolderSaves(by_hand, other_game, error).empty(), "an empty folder is not a save");
|
|
require(Import(folder_saves, account, device, backups, "0100AAAA00000000-3", error), "import by hand");
|
|
require(Import(folder_saves, base / "fresh-account", base / "fresh-device", backups, "fresh", error, &replaced) &&
|
|
!replaced && read_text(base / "fresh-account" / "save.bin") == "hand-copied",
|
|
"import where there was no save");
|
|
require(read_text(account / "save.bin") == "hand-copied", "hand-copied save in place");
|
|
require(read_text(backups / "0100AAAA00000000-3-account" / "save.bin") == "account-first-user",
|
|
"the save it replaced is kept");
|
|
require(read_text(device / "device.bin") == "device", "device save untouched by an account import");
|
|
|
|
// Export writes account/ and device/; what it wrote imports again as both saves.
|
|
const fs::path exported = base / "save-export" / "0100AAAA00000000-1";
|
|
require(Export(account, device, exported, error), "export");
|
|
require(read_text(exported / "account" / "save.bin") == "hand-copied", "account save exported");
|
|
require(read_text(exported / "device" / "device.bin") == "device", "device save exported");
|
|
fs::create_directories(by_hand / "0100BBBB00000000");
|
|
fs::rename(exported, by_hand / "0100AAAA00000000");
|
|
fs::remove_all(by_hand / "0100aaaa00000000");
|
|
folder_saves = FindFolderSaves(by_hand, game, error);
|
|
require(folder_saves.size() == 2 && folder_saves[0].kind == Kind::Account &&
|
|
folder_saves[1].kind == Kind::Device, "an exported folder holds both saves");
|
|
write_text(account / "newer.bin", "played since");
|
|
require(Import(folder_saves, account, device, backups, "0100AAAA00000000-4", error), "import an export");
|
|
require(!fs::exists(account / "newer.bin") && read_text(account / "save.bin") == "hand-copied",
|
|
"the export replaced the save");
|
|
require(read_text(backups / "0100AAAA00000000-4-account" / "newer.bin") == "played since" &&
|
|
read_text(backups / "0100AAAA00000000-4-device" / "device.bin") == "device",
|
|
"both replaced saves are kept");
|
|
|
|
// Nothing to export: no folders, or empty ones; a failed export leaves nothing behind.
|
|
const fs::path none = base / "save-export" / "none";
|
|
fs::create_directories(base / "empty-account");
|
|
require(!Export(base / "empty-account", base / "absent-device", none, error) && error == "no save yet",
|
|
"nothing to export");
|
|
require(!fs::exists(none), "no folder for an empty export");
|
|
|
|
// Imports must never follow symlinks. The native app has broad filesystem access on PS5,
|
|
// so a hand-copied save tree cannot be allowed to escape its selected source directory.
|
|
const fs::path outside = base / "outside";
|
|
write_text(outside / "secret.bin", "outside-save-tree");
|
|
const fs::path linked_root = base / "linked-save";
|
|
std::error_code link_error;
|
|
fs::create_directory_symlink(outside, linked_root, link_error);
|
|
require(!link_error, "create save-root symlink for safety check");
|
|
const std::vector<Save> linked{{Kind::Account, linked_root}};
|
|
require(!Import(linked, base / "symlink-target", base / "unused-device", backups,
|
|
"symlink-root", error),
|
|
"symlink save root rejected");
|
|
require(!fs::exists(base / "symlink-target" / "secret.bin"), "symlink root not copied");
|
|
|
|
const fs::path nested_source = base / "nested-symlink-save";
|
|
write_text(nested_source / "normal.bin", "normal");
|
|
fs::create_symlink(outside / "secret.bin", nested_source / "escape.bin", link_error);
|
|
require(!link_error, "create nested save symlink for safety check");
|
|
require(!CopyTree(nested_source, base / "nested-symlink-target"), "nested symlink rejected");
|
|
require(!fs::exists(base / "nested-symlink-target" / "escape.bin"), "nested symlink not copied");
|
|
|
|
fs::remove_all(base);
|
|
std::printf("Save transfer PASS: Ryujinx folder, hand-copied folder, account/device choice, backup, restore on "
|
|
"failure, export and re-import\n");
|
|
return 0;
|
|
}
|