File size: 7,670 Bytes
034d0a2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | // Copyright 2020 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <chrono>
#include <sstream>
#include <cryptopp/hex.h>
#include <fmt/format.h>
#include "common/archives.h"
#include "common/file_util.h"
#include "common/logging/log.h"
#include "common/scm_rev.h"
#include "common/swap.h"
#include "common/zstd_compression.h"
#include "core/core.h"
#include "core/movie.h"
#include "core/savestate.h"
#include "core/savestate_data.h"
#include "network/network.h"
namespace Core {
#pragma pack(push, 1)
struct CSTHeader {
std::array<u8, 4> filetype; /// Unique Identifier to check the file type (always "CST"0x1B)
u64_le program_id; /// ID of the ROM being executed. Also called title_id
std::array<u8, 20> revision; /// Git hash of the revision this savestate was created with
u64_le time; /// The time when this save state was created
std::array<u8, 20> build_name; /// The build name (Canary/Nightly) with the version number
u32_le zero = 0; /// Should be zero, just in case.
std::array<u8, 192> reserved{}; /// Make heading 256 bytes so it has consistent size
};
static_assert(sizeof(CSTHeader) == 256, "CSTHeader should be 256 bytes");
#pragma pack(pop)
constexpr std::array<u8, 4> header_magic_bytes{{'C', 'S', 'T', 0x1B}};
static std::string GetSaveStatePath(u64 program_id, u64 movie_id, u32 slot) {
if (movie_id) {
return fmt::format("{}{:016X}.movie{:016X}.{:02d}.cst",
FileUtil::GetUserPath(FileUtil::UserPath::StatesDir), program_id,
movie_id, slot);
} else {
return fmt::format("{}{:016X}.{:02d}.cst",
FileUtil::GetUserPath(FileUtil::UserPath::StatesDir), program_id, slot);
}
}
static bool ValidateSaveState(const CSTHeader& header, SaveStateInfo& info, u64 program_id,
u64 movie_id) {
const auto path = GetSaveStatePath(program_id, movie_id, info.slot);
if (header.filetype != header_magic_bytes) {
LOG_WARNING(Core, "Invalid save state file {}", path);
return false;
}
info.time = header.time;
if (header.program_id != program_id) {
LOG_WARNING(Core, "Save state file isn't for the current game {}", path);
return false;
}
const std::string revision = fmt::format("{:02x}", fmt::join(header.revision, ""));
const std::string build_name =
header.zero == 0 ? reinterpret_cast<const char*>(header.build_name.data()) : "";
if (revision == Common::g_scm_rev) {
info.status = SaveStateInfo::ValidationStatus::OK;
} else {
if (!build_name.empty()) {
info.build_name = build_name;
} else if (hash_to_version.find(revision) != hash_to_version.end()) {
info.build_name = hash_to_version.at(revision);
}
if (info.build_name.empty()) {
LOG_WARNING(Core, "Save state file {} created from a different revision {}", path,
revision);
} else {
LOG_WARNING(Core,
"Save state file {} created from a different build {} with revision {}",
path, info.build_name, revision);
}
info.status = SaveStateInfo::ValidationStatus::RevisionDismatch;
}
return true;
}
std::vector<SaveStateInfo> ListSaveStates(u64 program_id, u64 movie_id) {
std::vector<SaveStateInfo> result;
result.reserve(SaveStateSlotCount);
for (u32 slot = 1; slot <= SaveStateSlotCount; ++slot) {
const auto path = GetSaveStatePath(program_id, movie_id, slot);
if (!FileUtil::Exists(path)) {
continue;
}
SaveStateInfo info;
info.slot = slot;
FileUtil::IOFile file(path, "rb");
if (!file) {
LOG_ERROR(Core, "Could not open file {}", path);
continue;
}
CSTHeader header;
if (file.GetSize() < sizeof(header)) {
LOG_ERROR(Core, "File too small {}", path);
continue;
}
if (file.ReadBytes(&header, sizeof(header)) != sizeof(header)) {
LOG_ERROR(Core, "Could not read from file {}", path);
continue;
}
if (!ValidateSaveState(header, info, program_id, movie_id)) {
continue;
}
result.emplace_back(std::move(info));
}
return result;
}
void System::SaveState(u32 slot) const {
std::ostringstream sstream{std::ios_base::binary};
// Serialize
oarchive oa{sstream};
oa&* this;
const std::string& str{sstream.str()};
const auto data = std::span<const u8>{reinterpret_cast<const u8*>(str.data()), str.size()};
auto buffer = Common::Compression::CompressDataZSTDDefault(data);
const u64 movie_id = movie.GetCurrentMovieID();
const auto path = GetSaveStatePath(title_id, movie_id, slot);
if (!FileUtil::CreateFullPath(path)) {
throw std::runtime_error("Could not create path " + path);
}
FileUtil::IOFile file(path, "wb");
if (!file) {
throw std::runtime_error("Could not open file " + path);
}
CSTHeader header{};
header.filetype = header_magic_bytes;
header.program_id = title_id;
std::string rev_bytes;
CryptoPP::StringSource ss(Common::g_scm_rev, true,
new CryptoPP::HexDecoder(new CryptoPP::StringSink(rev_bytes)));
std::memcpy(header.revision.data(), rev_bytes.data(), sizeof(header.revision));
header.time = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
const std::string build_fullname = Common::g_build_fullname;
std::memset(header.build_name.data(), 0, sizeof(header.build_name));
std::memcpy(header.build_name.data(), build_fullname.c_str(),
std::min(build_fullname.length(), sizeof(header.build_name) - 1));
if (file.WriteBytes(&header, sizeof(header)) != sizeof(header) ||
file.WriteBytes(buffer.data(), buffer.size()) != buffer.size()) {
throw std::runtime_error("Could not write to file " + path);
}
}
void System::LoadState(u32 slot) {
if (Network::GetRoomMember().lock()->IsConnected()) {
throw std::runtime_error("Unable to load while connected to multiplayer");
}
const u64 movie_id = movie.GetCurrentMovieID();
const auto path = GetSaveStatePath(title_id, movie_id, slot);
std::vector<u8> decompressed;
{
std::vector<u8> buffer(FileUtil::GetSize(path) - sizeof(CSTHeader));
FileUtil::IOFile file(path, "rb");
// load header
CSTHeader header;
if (file.ReadBytes(&header, sizeof(header)) != sizeof(header)) {
throw std::runtime_error("Could not read from file at " + path);
}
// validate header
SaveStateInfo info;
info.slot = slot;
if (!ValidateSaveState(header, info, title_id, movie_id)) {
throw std::runtime_error("Invalid savestate");
}
if (file.ReadBytes(buffer.data(), buffer.size()) != buffer.size()) {
throw std::runtime_error("Could not read from file at " + path);
}
decompressed = Common::Compression::DecompressDataZSTD(buffer);
}
std::istringstream sstream{
std::string{reinterpret_cast<char*>(decompressed.data()), decompressed.size()},
std::ios_base::binary};
decompressed.clear();
// Deserialize
iarchive ia{sstream};
ia&* this;
}
} // namespace Core
|