Merge pull request #8300 from AdmiralCurtiss/gcmemcard-construction

GCMemcard: Rework construction logic.
This commit is contained in:
Anthony 2019-08-21 08:56:31 -07:00 committed by GitHub
commit 35eb63de2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 509 additions and 247 deletions

View File

@ -141,7 +141,7 @@ CEXIMemoryCard::CEXIMemoryCard(const int index, bool gciFolder) : card_index(ind
bool useMC251;
IniFile gameIni = SConfig::GetInstance().LoadGameIni();
gameIni.GetOrCreateSection("Core")->Get("MemoryCard251", &useMC251, false);
u16 sizeMb = useMC251 ? MemCard251Mb : MemCard2043Mb;
u16 sizeMb = useMC251 ? MBIT_SIZE_MEMORY_CARD_251 : MBIT_SIZE_MEMORY_CARD_2043;
if (gciFolder)
{
@ -241,7 +241,7 @@ void CEXIMemoryCard::SetupRawMemcard(u16 sizeMb)
SConfig::GetDirectoryForRegion(SConfig::ToGameCubeRegion(SConfig::GetInstance().m_region));
MemoryCard::CheckPath(filename, region_dir, is_slot_a);
if (sizeMb == MemCard251Mb)
if (sizeMb == MBIT_SIZE_MEMORY_CARD_251)
filename.insert(filename.find_last_of("."), ".251");
memorycard = std::make_unique<MemoryCard>(filename, card_index, sizeMb);

View File

@ -27,196 +27,255 @@ static void ByteSwap(u8* valueA, u8* valueB)
*valueB = tmp;
}
GCMemcard::GCMemcard(const std::string& filename, bool forceCreation, bool shift_jis)
: m_valid(false), m_filename(filename)
static constexpr std::optional<u64> BytesToMegabits(u64 bytes)
{
// Currently there is a string freeze. instead of adding a new message about needing r/w
// open file read only, if write is denied the error will be reported at that point
File::IOFile mcdFile(m_filename, "rb");
if (!mcdFile.IsOpen())
{
if (!forceCreation)
{
if (!AskYesNoT("\"%s\" does not exist.\n Create a new 16MB Memory Card?", filename.c_str()))
return;
shift_jis =
AskYesNoT("Format as Shift JIS (Japanese)?\nChoose no for Windows-1252 (Western)");
}
Format(shift_jis);
return;
}
else
{
// This function can be removed once more about hdr is known and we can check for a valid header
std::string fileType;
SplitPath(filename, nullptr, nullptr, &fileType);
if (strcasecmp(fileType.c_str(), ".raw") && strcasecmp(fileType.c_str(), ".gcp"))
{
PanicAlertT("File has the extension \"%s\".\nValid extensions are (.raw/.gcp)",
fileType.c_str());
return;
}
auto size = mcdFile.GetSize();
if (size < MC_FST_BLOCKS * BLOCK_SIZE)
{
PanicAlertT("%s failed to load as a memory card.\nFile is not large enough to be a valid "
"memory card file (0x%x bytes)",
filename.c_str(), (unsigned)size);
return;
}
if (size % BLOCK_SIZE)
{
PanicAlertT("%s failed to load as a memory card.\nCard file size is invalid (0x%x bytes)",
filename.c_str(), (unsigned)size);
return;
}
m_size_mb = (u16)((size / BLOCK_SIZE) / MBIT_TO_BLOCKS);
switch (m_size_mb)
{
case MemCard59Mb:
case MemCard123Mb:
case MemCard251Mb:
case Memcard507Mb:
case MemCard1019Mb:
case MemCard2043Mb:
break;
default:
PanicAlertT("%s failed to load as a memory card.\nCard size is invalid (0x%x bytes)",
filename.c_str(), (unsigned)size);
return;
}
}
mcdFile.Seek(0, SEEK_SET);
if (!mcdFile.ReadBytes(&m_header_block, BLOCK_SIZE))
{
PanicAlertT("Failed to read header correctly\n(0x0000-0x1FFF)");
return;
}
if (m_size_mb != m_header_block.m_size_mb)
{
PanicAlertT("Memory card file size does not match the header size");
return;
}
if (!mcdFile.ReadBytes(&m_directory_blocks[0], BLOCK_SIZE))
{
PanicAlertT("Failed to read 1st directory block correctly\n(0x2000-0x3FFF)");
return;
}
if (!mcdFile.ReadBytes(&m_directory_blocks[1], BLOCK_SIZE))
{
PanicAlertT("Failed to read 2nd directory block correctly\n(0x4000-0x5FFF)");
return;
}
if (!mcdFile.ReadBytes(&m_bat_blocks[0], BLOCK_SIZE))
{
PanicAlertT("Failed to read 1st block allocation table block correctly\n(0x6000-0x7FFF)");
return;
}
if (!mcdFile.ReadBytes(&m_bat_blocks[1], BLOCK_SIZE))
{
PanicAlertT("Failed to read 2nd block allocation table block correctly\n(0x8000-0x9FFF)");
return;
}
u32 csums = TestChecksums();
if (csums & 0x1)
{
// header checksum error!
// invalid files do not always get here
PanicAlertT("Header checksum failed");
return;
}
if (csums & 0x2) // 1st directory block checksum error!
{
if (csums & 0x4)
{
// 2nd block is also wrong!
PanicAlertT("Both directory block checksums are invalid");
return;
}
else
{
// FIXME: This is probably incorrect behavior, confirm what actually happens on hardware here.
// The currently active directory block and currently active BAT block don't necessarily have
// to correlate.
// 2nd block is correct, restore
m_directory_blocks[0] = m_directory_blocks[1];
m_bat_blocks[0] = m_bat_blocks[1];
// update checksums
csums = TestChecksums();
}
}
if (csums & 0x8) // 1st BAT checksum error!
{
if (csums & 0x10)
{
// 2nd BAT is also wrong!
PanicAlertT("Both Block Allocation Table block checksums are invalid");
return;
}
else
{
// FIXME: Same as above, this feels incorrect.
// 2nd block is correct, restore
m_directory_blocks[0] = m_directory_blocks[1];
m_bat_blocks[0] = m_bat_blocks[1];
// update checksums
csums = TestChecksums();
}
}
mcdFile.Seek(0xa000, SEEK_SET);
m_size_blocks = (u32)m_size_mb * MBIT_TO_BLOCKS;
m_data_blocks.reserve(m_size_blocks - MC_FST_BLOCKS);
m_valid = true;
for (u32 i = MC_FST_BLOCKS; i < m_size_blocks; ++i)
{
GCMBlock b;
if (mcdFile.ReadBytes(b.m_block.data(), b.m_block.size()))
{
m_data_blocks.push_back(b);
}
else
{
PanicAlertT("Failed to read block %u of the save data\nMemory card may be truncated\nFile "
"position: 0x%" PRIx64,
i, mcdFile.Tell());
m_valid = false;
break;
}
}
mcdFile.Close();
InitActiveDirBat();
const u64 factor = ((1024 * 1024) / 8);
const u64 megabits = bytes / factor;
const u64 remainder = bytes % factor;
if (remainder != 0)
return std::nullopt;
return megabits;
}
void GCMemcard::InitActiveDirBat()
bool GCMemcardErrorCode::HasCriticalErrors() const
{
if (m_directory_blocks[0].m_update_counter > m_directory_blocks[1].m_update_counter)
m_active_directory = 0;
else
m_active_directory = 1;
return Test(GCMemcardValidityIssues::FAILED_TO_OPEN) || Test(GCMemcardValidityIssues::IO_ERROR) ||
Test(GCMemcardValidityIssues::INVALID_CARD_SIZE) ||
Test(GCMemcardValidityIssues::INVALID_CHECKSUM) ||
Test(GCMemcardValidityIssues::MISMATCHED_CARD_SIZE) ||
Test(GCMemcardValidityIssues::FREE_BLOCK_MISMATCH) ||
Test(GCMemcardValidityIssues::DIR_BAT_INCONSISTENT);
}
if (m_bat_blocks[0].m_update_counter > m_bat_blocks[1].m_update_counter)
m_active_bat = 0;
bool GCMemcardErrorCode::Test(GCMemcardValidityIssues code) const
{
return m_errors.test(static_cast<size_t>(code));
}
void GCMemcardErrorCode::Set(GCMemcardValidityIssues code)
{
m_errors.set(static_cast<size_t>(code));
}
GCMemcardErrorCode& GCMemcardErrorCode::operator|=(const GCMemcardErrorCode& other)
{
this->m_errors |= other.m_errors;
return *this;
}
GCMemcard::GCMemcard()
: m_valid(false), m_size_blocks(0), m_size_mb(0), m_active_directory(0), m_active_bat(0)
{
}
std::optional<GCMemcard> GCMemcard::Create(std::string filename, u16 size_mbits, bool shift_jis)
{
GCMemcard card;
card.m_filename = std::move(filename);
// TODO: Format() not only formats the card but also writes it to disk at m_filename.
// Those tasks should probably be separated.
if (!card.Format(shift_jis, size_mbits))
return std::nullopt;
return std::move(card);
}
std::pair<GCMemcardErrorCode, std::optional<GCMemcard>> GCMemcard::Open(std::string filename)
{
GCMemcardErrorCode error_code;
File::IOFile file(filename, "rb");
if (!file.IsOpen())
{
error_code.Set(GCMemcardValidityIssues::FAILED_TO_OPEN);
return std::make_pair(error_code, std::nullopt);
}
// check if the filesize is a valid memory card size
const u64 filesize = file.GetSize();
const u64 filesize_megabits = BytesToMegabits(filesize).value_or(0);
const std::array<u16, 6> valid_megabits = {{
MBIT_SIZE_MEMORY_CARD_59,
MBIT_SIZE_MEMORY_CARD_123,
MBIT_SIZE_MEMORY_CARD_251,
MBIT_SIZE_MEMORY_CARD_507,
MBIT_SIZE_MEMORY_CARD_1019,
MBIT_SIZE_MEMORY_CARD_2043,
}};
if (!std::any_of(valid_megabits.begin(), valid_megabits.end(),
[filesize_megabits](u64 mbits) { return mbits == filesize_megabits; }))
{
error_code.Set(GCMemcardValidityIssues::INVALID_CARD_SIZE);
return std::make_pair(error_code, std::nullopt);
}
const u16 card_size_mbits = static_cast<u16>(filesize_megabits);
// read the entire card into memory
GCMemcard card;
file.Seek(0, SEEK_SET);
if (!file.ReadBytes(&card.m_header_block, BLOCK_SIZE) ||
!file.ReadBytes(&card.m_directory_blocks[0], BLOCK_SIZE) ||
!file.ReadBytes(&card.m_directory_blocks[1], BLOCK_SIZE) ||
!file.ReadBytes(&card.m_bat_blocks[0], BLOCK_SIZE) ||
!file.ReadBytes(&card.m_bat_blocks[1], BLOCK_SIZE))
{
error_code.Set(GCMemcardValidityIssues::IO_ERROR);
return std::make_pair(error_code, std::nullopt);
}
const u16 card_size_blocks = card_size_mbits * MBIT_TO_BLOCKS;
const u16 user_data_blocks = card_size_blocks - MC_FST_BLOCKS;
card.m_data_blocks.reserve(user_data_blocks);
for (u16 i = 0; i < user_data_blocks; ++i)
{
GCMBlock& block = card.m_data_blocks.emplace_back();
if (!file.ReadArray(block.m_block.data(), BLOCK_SIZE))
{
error_code.Set(GCMemcardValidityIssues::IO_ERROR);
return std::make_pair(error_code, std::nullopt);
}
}
file.Close();
card.m_filename = std::move(filename);
card.m_size_blocks = card_size_blocks;
card.m_size_mb = card_size_mbits;
// can return invalid card size, invalid checksum, data in unused area
// data in unused area is okay, otherwise fail
const GCMemcardErrorCode header_error_code = card.m_header_block.CheckForErrors(card_size_mbits);
error_code |= header_error_code;
if (header_error_code.HasCriticalErrors())
return std::make_pair(error_code, std::nullopt);
// The GC BIOS counts any card as corrupted as long as at least any two of [dir0, dir1, bat0,
// bat1] are corrupted. Yes, even if we have one valid dir and one valid bat, and even if those
// are both supposedly the newer ones.
//
// If both blocks of a single category are non-corrupted the used block depends on the update
// counter. If both blocks have the same update counter, it prefers block 0. Otherwise it prefers
// whichever block has the higher value. Essentially, if (0.update_ctr >= 1.update_ctr) { use 0 }
// else { use 1 }.
//
// If a single block of the four is corrupted, the non-corrupted one of the same category is
// immediately copied over the corrupted block with an incremented update counter. At this point
// both blocks contain the same data, so it's hard to tell which one is used, but presumably it
// uses the one with the now-higher update counter, same as it would have otherwise.
//
// This rule only applies for errors within a single block! That is, invalid checksums for both
// types, and free block mismatch for the BATs. Once two valid blocks have been selected but it
// later turns out they do not match eachother (eg. claimed block count of a file in the directory
// does not match the actual block count arrived at by following BAT), the card will be treated as
// corrupted, even if perhaps a different combination of the two blocks would result in a valid
// memory card.
// can return invalid checksum, data in unused area
GCMemcardErrorCode dir_block_0_error_code = card.m_directory_blocks[0].CheckForErrors();
GCMemcardErrorCode dir_block_1_error_code = card.m_directory_blocks[1].CheckForErrors();
// can return invalid card size, invalid checksum, data in unused area, free block mismatch
GCMemcardErrorCode bat_block_0_error_code = card.m_bat_blocks[0].CheckForErrors(card_size_mbits);
GCMemcardErrorCode bat_block_1_error_code = card.m_bat_blocks[1].CheckForErrors(card_size_mbits);
const bool dir_block_0_valid = !dir_block_0_error_code.HasCriticalErrors();
const bool dir_block_1_valid = !dir_block_1_error_code.HasCriticalErrors();
const bool bat_block_0_valid = !bat_block_0_error_code.HasCriticalErrors();
const bool bat_block_1_valid = !bat_block_1_error_code.HasCriticalErrors();
// if any two (at least) blocks are corrupted return failure
// TODO: Consider allowing a recovery option when there's still a valid one of each type.
int number_of_corrupted_dir_bat_blocks = 0;
if (!dir_block_0_valid)
++number_of_corrupted_dir_bat_blocks;
if (!dir_block_1_valid)
++number_of_corrupted_dir_bat_blocks;
if (!bat_block_0_valid)
++number_of_corrupted_dir_bat_blocks;
if (!bat_block_1_valid)
++number_of_corrupted_dir_bat_blocks;
if (number_of_corrupted_dir_bat_blocks > 1)
{
error_code |= dir_block_0_error_code;
error_code |= dir_block_1_error_code;
error_code |= bat_block_0_error_code;
error_code |= bat_block_1_error_code;
return std::make_pair(error_code, std::nullopt);
}
// if exactly one block is corrupted copy and update it over the non-corrupted block
if (number_of_corrupted_dir_bat_blocks == 1)
{
if (!dir_block_0_valid)
{
card.m_directory_blocks[0] = card.m_directory_blocks[1];
card.m_directory_blocks[0].m_update_counter = card.m_directory_blocks[0].m_update_counter + 1;
card.m_directory_blocks[0].FixChecksums();
dir_block_0_error_code = card.m_directory_blocks[0].CheckForErrors();
}
else if (!dir_block_1_valid)
{
card.m_directory_blocks[1] = card.m_directory_blocks[0];
card.m_directory_blocks[1].m_update_counter = card.m_directory_blocks[1].m_update_counter + 1;
card.m_directory_blocks[1].FixChecksums();
dir_block_1_error_code = card.m_directory_blocks[1].CheckForErrors();
}
else if (!bat_block_0_valid)
{
card.m_bat_blocks[0] = card.m_bat_blocks[1];
card.m_bat_blocks[0].m_update_counter = card.m_bat_blocks[0].m_update_counter + 1;
card.m_bat_blocks[0].FixChecksums();
bat_block_0_error_code = card.m_bat_blocks[0].CheckForErrors(card_size_mbits);
}
else if (!bat_block_1_valid)
{
card.m_bat_blocks[1] = card.m_bat_blocks[0];
card.m_bat_blocks[1].m_update_counter = card.m_bat_blocks[1].m_update_counter + 1;
card.m_bat_blocks[1].FixChecksums();
bat_block_1_error_code = card.m_bat_blocks[1].CheckForErrors(card_size_mbits);
}
else
{
// should never reach here
assert(0);
}
}
error_code |= dir_block_0_error_code;
error_code |= dir_block_1_error_code;
error_code |= bat_block_0_error_code;
error_code |= bat_block_1_error_code;
// select the in-use Dir and BAT blocks based on update counter
// These are compared as signed values by the GC BIOS. There is no protection against overflow, so
// if one block is MAX_VAL and the other is MIN_VAL it still picks the MAX_VAL one as the active
// one, even if that results in a corrupted memory card.
// TODO: We could try to be smarter about this to rescue seemingly-corrupted cards.
if (card.m_directory_blocks[0].m_update_counter >= card.m_directory_blocks[1].m_update_counter)
card.m_active_directory = 0;
else
m_active_bat = 1;
card.m_active_directory = 1;
if (card.m_bat_blocks[0].m_update_counter >= card.m_bat_blocks[1].m_update_counter)
card.m_active_bat = 0;
else
card.m_active_bat = 1;
// check for consistency between the active Dir and BAT
const GCMemcardErrorCode dir_bat_consistency_error_code =
card.GetActiveDirectory().CheckForErrorsWithBat(card.GetActiveBat());
error_code |= dir_bat_consistency_error_code;
if (error_code.HasCriticalErrors())
return std::make_pair(error_code, std::nullopt);
card.m_valid = true;
return std::make_pair(error_code, std::move(card));
}
const Directory& GCMemcard::GetActiveDirectory() const
@ -292,31 +351,6 @@ std::pair<u16, u16> CalculateMemcardChecksums(const u8* data, size_t size)
return std::make_pair(csum, inv_csum);
}
u32 GCMemcard::TestChecksums() const
{
const auto [csum_hdr, cinv_hdr] = m_header_block.CalculateChecksums();
const auto [csum_dir0, cinv_dir0] = m_directory_blocks[0].CalculateChecksums();
const auto [csum_dir1, cinv_dir1] = m_directory_blocks[1].CalculateChecksums();
const auto [csum_bat0, cinv_bat0] = m_bat_blocks[0].CalculateChecksums();
const auto [csum_bat1, cinv_bat1] = m_bat_blocks[1].CalculateChecksums();
u32 results = 0;
if ((m_header_block.m_checksum != csum_hdr) || (m_header_block.m_checksum_inv != cinv_hdr))
results |= 1;
if ((m_directory_blocks[0].m_checksum != csum_dir0) ||
(m_directory_blocks[0].m_checksum_inv != cinv_dir0))
results |= 2;
if ((m_directory_blocks[1].m_checksum != csum_dir1) ||
(m_directory_blocks[1].m_checksum_inv != cinv_dir1))
results |= 4;
if ((m_bat_blocks[0].m_checksum != csum_bat0) || (m_bat_blocks[0].m_checksum_inv != cinv_bat0))
results |= 8;
if ((m_bat_blocks[1].m_checksum != csum_bat1) || (m_bat_blocks[1].m_checksum_inv != cinv_bat1))
results |= 16;
return results;
}
bool GCMemcard::FixChecksums()
{
if (!m_valid)
@ -679,6 +713,50 @@ std::pair<u16, u16> BlockAlloc::CalculateChecksums() const
return CalculateMemcardChecksums(&raw[checksum_area_start], checksum_area_size);
}
GCMemcardErrorCode BlockAlloc::CheckForErrors(u16 size_mbits) const
{
GCMemcardErrorCode error_code;
// verify checksums
const auto [checksum_sum, checksum_inv] = CalculateChecksums();
if (checksum_sum != m_checksum || checksum_inv != m_checksum_inv)
error_code.Set(GCMemcardValidityIssues::INVALID_CHECKSUM);
if (size_mbits > 0 && size_mbits <= 256)
{
// check if free block count matches the actual amount of free blocks in m_map
const u16 total_available_blocks = (size_mbits * MBIT_TO_BLOCKS) - MC_FST_BLOCKS;
assert(total_available_blocks <= m_map.size());
u16 blocks_in_use = 0;
for (size_t i = 0; i < total_available_blocks; ++i)
{
if (m_map[i] != 0)
++blocks_in_use;
}
const u16 free_blocks = total_available_blocks - blocks_in_use;
if (free_blocks != m_free_blocks)
error_code.Set(GCMemcardValidityIssues::FREE_BLOCK_MISMATCH);
// remaining blocks map to nothing on hardware and must be empty
for (size_t i = total_available_blocks; i < m_map.size(); ++i)
{
if (m_map[i] != 0)
{
error_code.Set(GCMemcardValidityIssues::DATA_IN_UNUSED_AREA);
break;
}
}
}
else
{
// card size is outside the range of blocks that can be addressed
error_code.Set(GCMemcardValidityIssues::INVALID_CARD_SIZE);
}
return error_code;
}
GCMemcardGetSaveDataRetVal GCMemcard::GetSaveData(u8 index, std::vector<GCMBlock>& Blocks) const
{
if (!m_valid)
@ -1253,7 +1331,8 @@ bool GCMemcard::Format(bool shift_jis, u16 SizeMb)
m_data_blocks.clear();
m_data_blocks.resize(m_size_blocks - MC_FST_BLOCKS);
InitActiveDirBat();
m_active_directory = 0;
m_active_bat = 0;
m_valid = true;
return Save();
@ -1473,6 +1552,29 @@ std::pair<u16, u16> Header::CalculateChecksums() const
return CalculateMemcardChecksums(&raw[checksum_area_start], checksum_area_size);
}
GCMemcardErrorCode Header::CheckForErrors(u16 card_size_mbits) const
{
GCMemcardErrorCode error_code;
// total card size should match card size in header
if (m_size_mb != card_size_mbits)
error_code.Set(GCMemcardValidityIssues::MISMATCHED_CARD_SIZE);
// unused areas, should always be filled with 0xFF
if (std::any_of(m_unused_1.begin(), m_unused_1.end(), [](u8 val) { return val != 0xFF; }) ||
std::any_of(m_unused_2.begin(), m_unused_2.end(), [](u8 val) { return val != 0xFF; }))
{
error_code.Set(GCMemcardValidityIssues::DATA_IN_UNUSED_AREA);
}
// verify checksums
const auto [checksum_sum, checksum_inv] = CalculateChecksums();
if (checksum_sum != m_checksum || checksum_inv != m_checksum_inv)
error_code.Set(GCMemcardValidityIssues::INVALID_CHECKSUM);
return error_code;
}
Directory::Directory()
{
memset(this, 0xFF, BLOCK_SIZE);
@ -1508,3 +1610,76 @@ std::pair<u16, u16> Directory::CalculateChecksums() const
constexpr size_t checksum_area_size = checksum_area_end - checksum_area_start;
return CalculateMemcardChecksums(&raw[checksum_area_start], checksum_area_size);
}
GCMemcardErrorCode Directory::CheckForErrors() const
{
GCMemcardErrorCode error_code;
// verify checksums
const auto [checksum_sum, checksum_inv] = CalculateChecksums();
if (checksum_sum != m_checksum || checksum_inv != m_checksum_inv)
error_code.Set(GCMemcardValidityIssues::INVALID_CHECKSUM);
// unused area, should always be filled with 0xFF
if (std::any_of(m_padding.begin(), m_padding.end(), [](u8 val) { return val != 0xFF; }))
error_code.Set(GCMemcardValidityIssues::DATA_IN_UNUSED_AREA);
return error_code;
}
GCMemcardErrorCode Directory::CheckForErrorsWithBat(const BlockAlloc& bat) const
{
GCMemcardErrorCode error_code;
for (u8 i = 0; i < DIRLEN; ++i)
{
const DEntry& entry = m_dir_entries[i];
if (entry.m_gamecode == DEntry::UNINITIALIZED_GAMECODE)
continue;
// check if we end up with the same number of blocks when traversing through the BAT using the
// given first block
const u16 dir_number_of_blocks = entry.m_block_count;
const u16 dir_first_block = entry.m_first_block;
bool bat_block_count_matches = false;
{
u16 remaining_blocks = dir_number_of_blocks;
u16 current_block = dir_first_block;
while (true)
{
if (remaining_blocks == 0)
{
// we should be at the last block but haven't seen the last-block BAT indicator yet, file
// is larger according to BAT, so we're inconsistent
break;
}
--remaining_blocks;
const u16 next_block = bat.GetNextBlock(current_block);
if (next_block == 0)
{
// current block is out-of-range or next block is unallocated, this is definitely wrong
break;
}
if (next_block == 0xFFFF)
{
// we're at the final block according to the BAT
// if there are zero remaining blocks according to the directory we're consistent,
// otherwise the file is smaller according to the BAT and we're inconsistent
bat_block_count_matches = remaining_blocks == 0;
break;
}
current_block = next_block;
}
}
if (!bat_block_count_matches)
{
error_code.Set(GCMemcardValidityIssues::DIR_BAT_INCONSISTENT);
break;
}
}
// TODO: We could also check if every allocated BAT block is actually reachable with the files.
return error_code;
}

View File

@ -6,6 +6,7 @@
#include <algorithm>
#include <array>
#include <bitset>
#include <string>
#include <vector>
@ -79,6 +80,31 @@ enum class GCMemcardRemoveFileRetVal
DELETE_FAIL,
};
enum class GCMemcardValidityIssues
{
FAILED_TO_OPEN,
IO_ERROR,
INVALID_CARD_SIZE,
INVALID_CHECKSUM,
MISMATCHED_CARD_SIZE,
FREE_BLOCK_MISMATCH,
DIR_BAT_INCONSISTENT,
DATA_IN_UNUSED_AREA,
COUNT
};
class GCMemcardErrorCode
{
public:
bool HasCriticalErrors() const;
bool Test(GCMemcardValidityIssues code) const;
void Set(GCMemcardValidityIssues code);
GCMemcardErrorCode& operator|=(const GCMemcardErrorCode& other);
private:
std::bitset<static_cast<size_t>(GCMemcardValidityIssues::COUNT)> m_errors;
};
// size of a single memory card block in bytes
constexpr u32 BLOCK_SIZE = 0x2000;
@ -103,17 +129,17 @@ constexpr u16 BAT_SIZE = 0xFFB;
// possible sizes of memory cards in megabits
// TODO: Do memory card sizes have to be power of two?
// TODO: Are these all of them? A 4091 block card should work in theory at least.
constexpr u16 MemCard59Mb = 0x04;
constexpr u16 MemCard123Mb = 0x08;
constexpr u16 MemCard251Mb = 0x10;
constexpr u16 Memcard507Mb = 0x20;
constexpr u16 MemCard1019Mb = 0x40;
constexpr u16 MemCard2043Mb = 0x80;
constexpr u16 MBIT_SIZE_MEMORY_CARD_59 = 0x04;
constexpr u16 MBIT_SIZE_MEMORY_CARD_123 = 0x08;
constexpr u16 MBIT_SIZE_MEMORY_CARD_251 = 0x10;
constexpr u16 MBIT_SIZE_MEMORY_CARD_507 = 0x20;
constexpr u16 MBIT_SIZE_MEMORY_CARD_1019 = 0x40;
constexpr u16 MBIT_SIZE_MEMORY_CARD_2043 = 0x80;
class MemoryCardBase
{
public:
explicit MemoryCardBase(int card_index = 0, int size_mbits = MemCard2043Mb)
explicit MemoryCardBase(int card_index = 0, int size_mbits = MBIT_SIZE_MEMORY_CARD_2043)
: m_card_index(card_index), m_nintendo_card_id(size_mbits)
{
}
@ -185,13 +211,16 @@ struct Header
// 0x1e00 bytes at 0x0200: Unused (0xff)
std::array<u8, 7680> m_unused_2;
explicit Header(int slot = 0, u16 size_mbits = MemCard2043Mb, bool shift_jis = false);
explicit Header(int slot = 0, u16 size_mbits = MBIT_SIZE_MEMORY_CARD_2043,
bool shift_jis = false);
// Calculates the card serial numbers used for encrypting some save files.
std::pair<u32, u32> CalculateSerial() const;
void FixChecksums();
std::pair<u16, u16> CalculateChecksums() const;
GCMemcardErrorCode CheckForErrors(u16 card_size_mbits) const;
};
static_assert(sizeof(Header) == BLOCK_SIZE);
@ -274,6 +303,8 @@ struct DEntry
};
static_assert(sizeof(DEntry) == DENTRY_SIZE);
struct BlockAlloc;
struct Directory
{
// 127 files of 0x40 bytes each
@ -283,8 +314,7 @@ struct Directory
std::array<u8, 0x3a> m_padding;
// 2 bytes at 0x1ffa: Update Counter
// TODO: What happens if this overflows? Is there a special case for preferring 0 over max value?
Common::BigEndianValue<u16> m_update_counter;
Common::BigEndianValue<s16> m_update_counter;
// 2 bytes at 0x1ffc: Additive Checksum
u16 m_checksum;
@ -301,6 +331,10 @@ struct Directory
void FixChecksums();
std::pair<u16, u16> CalculateChecksums() const;
GCMemcardErrorCode CheckForErrors() const;
GCMemcardErrorCode CheckForErrorsWithBat(const BlockAlloc& bat) const;
};
static_assert(sizeof(Directory) == BLOCK_SIZE);
@ -313,7 +347,7 @@ struct BlockAlloc
u16 m_checksum_inv;
// 2 bytes at 0x0004: Update Counter
Common::BigEndianValue<u16> m_update_counter;
Common::BigEndianValue<s16> m_update_counter;
// 2 bytes at 0x0006: Free Blocks
Common::BigEndianValue<u16> m_free_blocks;
@ -324,7 +358,7 @@ struct BlockAlloc
// 0x1ff8 bytes at 0x000a: Map of allocated Blocks
std::array<Common::BigEndianValue<u16>, BAT_SIZE> m_map;
explicit BlockAlloc(u16 size_mbits = MemCard2043Mb);
explicit BlockAlloc(u16 size_mbits = MBIT_SIZE_MEMORY_CARD_2043);
u16 GetNextBlock(u16 block) const;
u16 NextFreeBlock(u16 max_block, u16 starting_block = MC_FST_BLOCKS) const;
@ -333,6 +367,8 @@ struct BlockAlloc
void FixChecksums();
std::pair<u16, u16> CalculateChecksums() const;
GCMemcardErrorCode CheckForErrors(u16 size_mbits) const;
};
static_assert(sizeof(BlockAlloc) == BLOCK_SIZE);
#pragma pack(pop)
@ -354,8 +390,9 @@ private:
int m_active_directory;
int m_active_bat;
GCMemcard();
GCMemcardImportFileRetVal ImportGciInternal(File::IOFile&& gci, const std::string& inputFile);
void InitActiveDirBat();
const Directory& GetActiveDirectory() const;
const BlockAlloc& GetActiveBat() const;
@ -364,8 +401,9 @@ private:
void UpdateBat(const BlockAlloc& bat);
public:
explicit GCMemcard(const std::string& fileName, bool forceCreation = false,
bool shift_jis = false);
static std::optional<GCMemcard> Create(std::string filename, u16 size_mbits, bool shift_jis);
static std::pair<GCMemcardErrorCode, std::optional<GCMemcard>> Open(std::string filename);
GCMemcard(const GCMemcard&) = delete;
GCMemcard& operator=(const GCMemcard&) = delete;
@ -375,14 +413,14 @@ public:
bool IsValid() const { return m_valid; }
bool IsShiftJIS() const;
bool Save();
bool Format(bool shift_jis = false, u16 SizeMb = MemCard2043Mb);
static bool Format(u8* card_data, bool shift_jis = false, u16 SizeMb = MemCard2043Mb);
bool Format(bool shift_jis = false, u16 SizeMb = MBIT_SIZE_MEMORY_CARD_2043);
static bool Format(u8* card_data, bool shift_jis = false,
u16 SizeMb = MBIT_SIZE_MEMORY_CARD_2043);
static s32 FZEROGX_MakeSaveGameValid(const Header& cardheader, const DEntry& direntry,
std::vector<GCMBlock>& FileBuffer);
static s32 PSO_MakeSaveGameValid(const Header& cardheader, const DEntry& direntry,
std::vector<GCMBlock>& FileBuffer);
u32 TestChecksums() const;
bool FixChecksums();
// get number of file entries in the directory

View File

@ -703,12 +703,12 @@ void MigrateFromMemcardFile(const std::string& directory_name, int card_index)
Config::Get(Config::MAIN_MEMCARD_B_PATH);
if (File::Exists(ini_memcard))
{
GCMemcard memcard(ini_memcard.c_str());
if (memcard.IsValid())
auto [error_code, memcard] = GCMemcard::Open(ini_memcard.c_str());
if (!error_code.HasCriticalErrors() && memcard && memcard->IsValid())
{
for (u8 i = 0; i < DIRLEN; i++)
{
memcard.ExportGci(i, "", directory_name);
memcard->ExportGci(i, "", directory_name);
}
}
}

View File

@ -17,7 +17,8 @@ class PointerWrap;
class MemoryCard : public MemoryCardBase
{
public:
MemoryCard(const std::string& filename, int card_index, u16 size_mbits = MemCard2043Mb);
MemoryCard(const std::string& filename, int card_index,
u16 size_mbits = MBIT_SIZE_MEMORY_CARD_2043);
~MemoryCard();
static void CheckPath(std::string& memcardPath, const std::string& gameRegion, bool isSlotA);
void FlushThread();

View File

@ -17,6 +17,8 @@
#include <QLineEdit>
#include <QPixmap>
#include <QPushButton>
#include <QString>
#include <QStringList>
#include <QTableWidget>
#include <QTimer>
@ -241,13 +243,20 @@ void GCMemcardManager::UpdateActions()
void GCMemcardManager::SetSlotFile(int slot, QString path)
{
auto memcard = std::make_unique<GCMemcard>(path.toStdString());
auto [error_code, memcard] = GCMemcard::Open(path.toStdString());
if (!memcard->IsValid())
return;
m_slot_file_edit[slot]->setText(path);
m_slot_memcard[slot] = std::move(memcard);
if (!error_code.HasCriticalErrors() && memcard && memcard->IsValid())
{
m_slot_file_edit[slot]->setText(path);
m_slot_memcard[slot] = std::make_unique<GCMemcard>(std::move(*memcard));
}
else
{
m_slot_memcard[slot] = nullptr;
ModalMessageBox::critical(
this, tr("Error"),
tr("Failed opening memory card:\n%1").arg(GetErrorMessagesForErrorCode(error_code)));
}
UpdateSlotTable(slot);
UpdateActions();
@ -523,3 +532,37 @@ std::vector<QPixmap> GCMemcardManager::GetIconFromSaveFile(int file_index, int s
return frame_pixmaps;
}
QString GCMemcardManager::GetErrorMessagesForErrorCode(const GCMemcardErrorCode& code)
{
QStringList sl;
if (code.Test(GCMemcardValidityIssues::FAILED_TO_OPEN))
sl.push_back(tr("Couldn't open file."));
if (code.Test(GCMemcardValidityIssues::IO_ERROR))
sl.push_back(tr("Couldn't read file."));
if (code.Test(GCMemcardValidityIssues::INVALID_CARD_SIZE))
sl.push_back(tr("Filesize does not match any known GameCube Memory Card size."));
if (code.Test(GCMemcardValidityIssues::MISMATCHED_CARD_SIZE))
sl.push_back(tr("Filesize in header mismatches actual card size."));
if (code.Test(GCMemcardValidityIssues::INVALID_CHECKSUM))
sl.push_back(tr("Invalid checksums."));
if (code.Test(GCMemcardValidityIssues::FREE_BLOCK_MISMATCH))
sl.push_back(tr("Mismatch between free block count in header and actually unused blocks."));
if (code.Test(GCMemcardValidityIssues::DIR_BAT_INCONSISTENT))
sl.push_back(tr("Mismatch between internal data structures."));
if (code.Test(GCMemcardValidityIssues::DATA_IN_UNUSED_AREA))
sl.push_back(tr("Data in area of file that should be unused."));
if (sl.empty())
return QStringLiteral("No errors.");
return sl.join(QStringLiteral("\n"));
}

View File

@ -12,6 +12,7 @@
#include <QDialog>
class GCMemcard;
class GCMemcardErrorCode;
class QDialogButtonBox;
class QGroupBox;
@ -19,6 +20,7 @@ class QLabel;
class QLineEdit;
class QPixmap;
class QPushButton;
class QString;
class QTableWidget;
class QTimer;
@ -29,6 +31,8 @@ public:
explicit GCMemcardManager(QWidget* parent = nullptr);
~GCMemcardManager();
static QString GetErrorMessagesForErrorCode(const GCMemcardErrorCode& code);
private:
void CreateWidgets();
void ConnectWidgets();

View File

@ -28,6 +28,7 @@
#include "Core/HW/GCMemcard/GCMemcard.h"
#include "DolphinQt/Config/Mapping/MappingWindow.h"
#include "DolphinQt/GCMemcardManager.h"
#include "DolphinQt/QtUtils/ModalMessageBox.h"
enum
@ -212,15 +213,15 @@ void GameCubePane::OnConfigPressed(int slot)
{
if (File::Exists(filename.toStdString()))
{
GCMemcard mc(filename.toStdString());
auto [error_code, mc] = GCMemcard::Open(filename.toStdString());
if (!mc.IsValid())
if (error_code.HasCriticalErrors() || !mc || !mc->IsValid())
{
ModalMessageBox::critical(this, tr("Error"),
tr("Cannot use that file as a memory card.\n%1\n"
"is not a valid GameCube memory card file")
.arg(filename));
ModalMessageBox::critical(
this, tr("Error"),
tr("The file\n%1\nis either corrupted or not a GameCube memory card file.\n%2")
.arg(filename)
.arg(GCMemcardManager::GetErrorMessagesForErrorCode(error_code)));
return;
}
}