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