/ src / core / savestate.cpp
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