diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcard.h b/Source/Core/Core/HW/GCMemcard/GCMemcard.h index 7af8e0dfde..ffa3697119 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcard.h +++ b/Source/Core/Core/HW/GCMemcard/GCMemcard.h @@ -400,6 +400,12 @@ static_assert(sizeof(BlockAlloc) == BLOCK_SIZE); static_assert(std::is_trivially_copyable_v); #pragma pack(pop) +struct Savefile +{ + DEntry dir_entry; + std::vector blocks; +}; + class GCMemcard { private: diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.cpp b/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.cpp index 3230e86339..c603ca83f2 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.cpp +++ b/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.cpp @@ -1,9 +1,25 @@ #include "Core/HW/GCMemcard/GCMemcardUtils.h" +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Common/IOFile.h" + #include "Core/HW/GCMemcard/GCMemcard.h" namespace Memcard { +constexpr u32 GCI_HEADER_SIZE = DENTRY_SIZE; +constexpr std::array SAV_MAGIC = {0x44, 0x41, 0x54, 0x45, 0x4C, 0x47, + 0x43, 0x5F, 0x53, 0x41, 0x56, 0x45}; // "DATELGC_SAVE" +constexpr u32 SAV_HEADER_SIZE = 0xC0; +constexpr u32 SAV_DENTRY_OFFSET = 0x80; +constexpr std::array GCS_MAGIC = {0x47, 0x43, 0x53, 0x41, 0x56, 0x45}; // "GCSAVE" +constexpr u32 GCS_HEADER_SIZE = 0x150; +constexpr u32 GCS_DENTRY_OFFSET = 0x110; + bool HasSameIdentity(const DEntry& lhs, const DEntry& rhs) { // The Gamecube BIOS identifies two files as being 'the same' (that is, disallows copying from one @@ -53,4 +69,218 @@ bool HasSameIdentity(const DEntry& lhs, const DEntry& rhs) return true; } + +static void ByteswapDEntrySavHeader(std::array& entry) +{ + // several bytes in SAV are swapped compared to the internal memory card format + for (size_t p : {0x06, 0x2C, 0x2E, 0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x3C, 0x3E}) + std::swap(entry[p], entry[p + 1]); +} + +static DEntry ExtractDEntryFromSavHeader(const std::array& sav_header) +{ + std::array entry; + std::memcpy(entry.data(), &sav_header[SAV_DENTRY_OFFSET], DENTRY_SIZE); + ByteswapDEntrySavHeader(entry); + + DEntry dir_entry; + std::memcpy(&dir_entry, entry.data(), DENTRY_SIZE); + return dir_entry; +} + +static void InjectDEntryToSavHeader(std::array& sav_header, + const DEntry& dir_entry) +{ + std::array entry; + std::memcpy(entry.data(), &dir_entry, DENTRY_SIZE); + ByteswapDEntrySavHeader(entry); + std::memcpy(&sav_header[SAV_DENTRY_OFFSET], entry.data(), DENTRY_SIZE); +} + +static bool ReadBlocksFromIOFile(File::IOFile& file, std::vector& blocks, + size_t block_count) +{ + blocks.reserve(block_count); + for (size_t i = 0; i < block_count; ++i) + { + GCMBlock& block = blocks.emplace_back(); + if (!file.ReadBytes(block.m_block.data(), block.m_block.size())) + return false; + } + return true; +} + +static std::variant ReadSavefileInternalGCI(File::IOFile& file, + u64 filesize) +{ + Savefile savefile; + if (!file.ReadBytes(&savefile.dir_entry, DENTRY_SIZE)) + return ReadSavefileErrorCode::IOError; + + const size_t block_count = savefile.dir_entry.m_block_count; + const u64 expected_size = DENTRY_SIZE + block_count * BLOCK_SIZE; + if (expected_size != filesize) + return ReadSavefileErrorCode::DataCorrupted; + + if (!ReadBlocksFromIOFile(file, savefile.blocks, block_count)) + return ReadSavefileErrorCode::IOError; + + return savefile; +} + +static std::variant ReadSavefileInternalGCS(File::IOFile& file, + u64 filesize) +{ + std::array gcs_header; + if (!file.ReadBytes(gcs_header.data(), gcs_header.size())) + return ReadSavefileErrorCode::IOError; + + if (std::memcmp(gcs_header.data(), GCS_MAGIC.data(), GCS_MAGIC.size()) != 0) + return ReadSavefileErrorCode::DataCorrupted; + + Savefile savefile; + std::memcpy(&savefile.dir_entry, &gcs_header[GCS_DENTRY_OFFSET], DENTRY_SIZE); + + // field containing the Block count as displayed within + // the GameSaves software is not stored in the GCS file. + // It is stored only within the corresponding GSV file. + // If the GCS file is added without using the GameSaves software, + // the value stored is always "1" + + // to get the actual block count calculate backwards from the filesize + const u64 total_block_size = filesize - GCS_HEADER_SIZE; + if ((total_block_size % BLOCK_SIZE) != 0) + return ReadSavefileErrorCode::DataCorrupted; + + const size_t block_count = total_block_size / BLOCK_SIZE; + savefile.dir_entry.m_block_count = static_cast(block_count); + + if (!ReadBlocksFromIOFile(file, savefile.blocks, block_count)) + return ReadSavefileErrorCode::IOError; + + return savefile; +} + +static std::variant ReadSavefileInternalSAV(File::IOFile& file, + u64 filesize) +{ + std::array sav_header; + if (!file.ReadBytes(sav_header.data(), sav_header.size())) + return ReadSavefileErrorCode::IOError; + + if (std::memcmp(sav_header.data(), SAV_MAGIC.data(), SAV_MAGIC.size()) != 0) + return ReadSavefileErrorCode::DataCorrupted; + + Savefile savefile; + savefile.dir_entry = ExtractDEntryFromSavHeader(sav_header); + + const size_t block_count = savefile.dir_entry.m_block_count; + const u64 expected_size = SAV_HEADER_SIZE + block_count * BLOCK_SIZE; + if (expected_size != filesize) + return ReadSavefileErrorCode::DataCorrupted; + + if (!ReadBlocksFromIOFile(file, savefile.blocks, block_count)) + return ReadSavefileErrorCode::IOError; + + return savefile; +} + +std::variant ReadSavefile(const std::string& filename) +{ + File::IOFile file(filename, "rb"); + if (!file) + return ReadSavefileErrorCode::OpenFileFail; + + // Since GCI, GCS and SAV all have different header lengths but the block size is always the same, + // we can detect the type from the filesize. + const u64 filesize = file.GetSize(); + const u64 header_size = filesize % BLOCK_SIZE; + + switch (header_size) + { + case GCI_HEADER_SIZE: + return ReadSavefileInternalGCI(file, filesize); + case GCS_HEADER_SIZE: + return ReadSavefileInternalGCS(file, filesize); + case SAV_HEADER_SIZE: + return ReadSavefileInternalSAV(file, filesize); + default: + return ReadSavefileErrorCode::DataCorrupted; + } +} + +static bool WriteSavefileInternalGCI(File::IOFile& file, const Savefile& savefile) +{ + if (!file.WriteBytes(&savefile.dir_entry, DENTRY_SIZE)) + return false; + + for (const GCMBlock& block : savefile.blocks) + { + if (!file.WriteBytes(block.m_block.data(), block.m_block.size())) + return false; + } + + return file.IsGood(); +} + +static bool WriteSavefileInternalGCS(File::IOFile& file, const Savefile& savefile) +{ + std::array header; + std::memset(header.data(), 0, header.size()); + std::memcpy(header.data(), GCS_MAGIC.data(), GCS_MAGIC.size()); + + DEntry gcs_entry = savefile.dir_entry; + gcs_entry.m_block_count = 1; // always stored as 1 in GCS files + std::memcpy(&header[GCS_DENTRY_OFFSET], &gcs_entry, DENTRY_SIZE); + + if (!file.WriteBytes(header.data(), header.size())) + return false; + + for (const GCMBlock& block : savefile.blocks) + { + if (!file.WriteBytes(block.m_block.data(), block.m_block.size())) + return false; + } + + return file.IsGood(); +} + +static bool WriteSavefileInternalSAV(File::IOFile& file, const Savefile& savefile) +{ + std::array header; + std::memset(header.data(), 0, header.size()); + std::memcpy(header.data(), SAV_MAGIC.data(), SAV_MAGIC.size()); + + InjectDEntryToSavHeader(header, savefile.dir_entry); + + if (!file.WriteBytes(header.data(), header.size())) + return false; + + for (const GCMBlock& block : savefile.blocks) + { + if (!file.WriteBytes(block.m_block.data(), block.m_block.size())) + return false; + } + + return file.IsGood(); +} + +bool WriteSavefile(const std::string& filename, const Savefile& savefile, SavefileFormat format) +{ + File::IOFile file(filename, "wb"); + if (!file) + return false; + + switch (format) + { + case SavefileFormat::GCI: + return WriteSavefileInternalGCI(file, savefile); + case SavefileFormat::GCS: + return WriteSavefileInternalGCS(file, savefile); + case SavefileFormat::SAV: + return WriteSavefileInternalSAV(file, savefile); + default: + return false; + } +} } // namespace Memcard diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.h b/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.h index ebe0daf4fa..3734d03d28 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.h +++ b/Source/Core/Core/HW/GCMemcard/GCMemcardUtils.h @@ -4,9 +4,33 @@ #pragma once +#include +#include + +#include "Core/HW/GCMemcard/GCMemcard.h" + namespace Memcard { -struct DEntry; - bool HasSameIdentity(const DEntry& lhs, const DEntry& rhs); + +enum class ReadSavefileErrorCode +{ + OpenFileFail, + IOError, + DataCorrupted, +}; + +// Reads a Gamecube memory card savefile from a file. +// Supported formats are GCI, GCS (Gameshark), and SAV (MaxDrive). +std::variant ReadSavefile(const std::string& filename); + +enum class SavefileFormat +{ + GCI, + GCS, + SAV, +}; + +// Writes a Gamecube memory card savefile to a file. +bool WriteSavefile(const std::string& filename, const Savefile& savefile, SavefileFormat format); } // namespace Memcard