Qt/MemoryCardEditor: Support undeleting files

This commit is contained in:
Connor McLaughlin 2021-06-25 14:43:19 +10:00
parent f7587eaeac
commit 1ecf5cf76a
5 changed files with 215 additions and 42 deletions

View file

@ -223,15 +223,19 @@ u32 GetFreeBlockCount(const DataArray& data)
return count; return count;
} }
std::vector<FileInfo> EnumerateFiles(const DataArray& data) std::vector<FileInfo> EnumerateFiles(const DataArray& data, bool include_deleted)
{ {
std::vector<FileInfo> files; std::vector<FileInfo> files;
for (u32 dir_frame = 1; dir_frame < FRAMES_PER_BLOCK; dir_frame++) for (u32 dir_frame = 1; dir_frame < FRAMES_PER_BLOCK; dir_frame++)
{ {
const DirectoryFrame* df = GetFramePtr<DirectoryFrame>(data, 0, dir_frame); const DirectoryFrame* df = GetFramePtr<DirectoryFrame>(data, 0, dir_frame);
if (df->block_allocation_state != 0x51) if (df->block_allocation_state != 0x51 &&
(!include_deleted || (df->block_allocation_state != 0xA1 && df->block_allocation_state != 0xA2 &&
df->block_allocation_state != 0xA3)))
{
continue; continue;
}
u32 filename_length = 0; u32 filename_length = 0;
while (filename_length < sizeof(df->filename) && df->filename[filename_length] != '\0') while (filename_length < sizeof(df->filename) && df->filename[filename_length] != '\0')
@ -242,6 +246,7 @@ std::vector<FileInfo> EnumerateFiles(const DataArray& data)
fi.first_block = dir_frame; fi.first_block = dir_frame;
fi.size = df->file_size; fi.size = df->file_size;
fi.num_blocks = 1; fi.num_blocks = 1;
fi.deleted = (df->block_allocation_state != 0x51);
const DirectoryFrame* next_df = df; const DirectoryFrame* next_df = df;
while (next_df->next_block_number < (NUM_BLOCKS - 1) && fi.num_blocks < FRAMES_PER_BLOCK) while (next_df->next_block_number < (NUM_BLOCKS - 1) && fi.num_blocks < FRAMES_PER_BLOCK)
@ -373,7 +378,7 @@ bool WriteFile(DataArray* data, const std::string_view& filename, const std::vec
return true; return true;
} }
bool DeleteFile(DataArray* data, const FileInfo& fi) bool DeleteFile(DataArray* data, const FileInfo& fi, bool clear_sectors)
{ {
Log_InfoPrintf("Deleting '%s' from memory card (%u blocks)", fi.filename.c_str(), fi.num_blocks); Log_InfoPrintf("Deleting '%s' from memory card (%u blocks)", fi.filename.c_str(), fi.num_blocks);
@ -382,13 +387,20 @@ bool DeleteFile(DataArray* data, const FileInfo& fi)
{ {
DirectoryFrame* df = GetFramePtr<DirectoryFrame>(data, 0, block_number); DirectoryFrame* df = GetFramePtr<DirectoryFrame>(data, 0, block_number);
block_number = ZeroExtend32(df->next_block_number) + 1; block_number = ZeroExtend32(df->next_block_number) + 1;
if (clear_sectors)
{
std::memset(df, 0, sizeof(DirectoryFrame)); std::memset(df, 0, sizeof(DirectoryFrame));
df->block_allocation_state = 0xA0;
}
else
{
if (i == 0) if (i == 0)
df->block_allocation_state = 0xA1; df->block_allocation_state = 0xA1;
else if (i == (fi.num_blocks - 1)) else if (i == (fi.num_blocks - 1))
df->block_allocation_state = 0xA3; df->block_allocation_state = 0xA3;
else else
df->block_allocation_state = 0xA2; df->block_allocation_state = 0xA2;
}
df->next_block_number = 0xFFFF; df->next_block_number = 0xFFFF;
UpdateChecksum(df); UpdateChecksum(df);
@ -397,6 +409,83 @@ bool DeleteFile(DataArray* data, const FileInfo& fi)
return true; return true;
} }
bool UndeleteFile(DataArray* data, const FileInfo& fi)
{
if (!fi.deleted)
{
Log_ErrorPrintf("File '%s' is not deleted", fi.filename.c_str());
return false;
}
Log_InfoPrintf("Undeleting '%s' from memory card (%u blocks)", fi.filename.c_str(), fi.num_blocks);
// check that all blocks are present first
u32 block_number = fi.first_block;
for (u32 i = 0; i < fi.num_blocks && (block_number > 0 && block_number < NUM_BLOCKS); i++)
{
DirectoryFrame* df = GetFramePtr<DirectoryFrame>(data, 0, block_number);
block_number = ZeroExtend32(df->next_block_number) + 1;
if (i == 0)
{
if (df->block_allocation_state != 0xA1)
{
Log_ErrorPrintf("Incorrect block state for %u, expected 0xA1 got 0x%02X", df->block_allocation_state);
return false;
}
}
else if (i == (fi.num_blocks - 1))
{
if (df->block_allocation_state != 0xA3)
{
Log_ErrorPrintf("Incorrect block state for %u, expected 0xA3 got 0x%02X", df->block_allocation_state);
return false;
}
}
else
{
if (df->block_allocation_state != 0xA2)
{
Log_WarningPrintf("Incorrect block state for %u, expected 0xA2 got 0x%02X", df->block_allocation_state);
return false;
}
}
}
block_number = fi.first_block;
for (u32 i = 0; i < fi.num_blocks && (block_number > 0 && block_number < NUM_BLOCKS); i++)
{
DirectoryFrame* df = GetFramePtr<DirectoryFrame>(data, 0, block_number);
block_number = ZeroExtend32(df->next_block_number) + 1;
if (i == 0)
{
if (df->block_allocation_state != 0xA1)
Log_WarningPrintf("Incorrect block state for %u, expected 0xA1 got 0x%02X", df->block_allocation_state);
df->block_allocation_state = 0x51;
}
else if (i == (fi.num_blocks - 1))
{
if (df->block_allocation_state != 0xA3)
Log_WarningPrintf("Incorrect block state for %u, expected 0xA3 got 0x%02X", df->block_allocation_state);
df->block_allocation_state = 0x53;
}
else
{
if (df->block_allocation_state != 0xA2)
Log_WarningPrintf("Incorrect block state for %u, expected 0xA2 got 0x%02X", df->block_allocation_state);
df->block_allocation_state = 0x52;
}
UpdateChecksum(df);
}
return true;
}
static bool ImportCardMCD(DataArray* data, const char* filename, std::vector<u8> file_data) static bool ImportCardMCD(DataArray* data, const char* filename, std::vector<u8> file_data)
{ {
if (file_data.size() != DATA_SIZE) if (file_data.size() != DATA_SIZE)
@ -542,17 +631,6 @@ static bool ImportSaveWithDirectoryFrame(DataArray* data, const char* filename,
return false; return false;
} }
// Make sure there isn't already a save with the same name
std::vector<FileInfo> fileinfos = EnumerateFiles(*data);
for (const FileInfo& fi : fileinfos)
{
if (fi.filename.compare(0, sizeof(df.filename), df.filename) == 0)
{
Log_ErrorPrintf("Save file with the same name already exists in memory card");
return false;
}
}
std::vector<u8> blocks = std::vector<u8>(static_cast<size_t>(df.file_size)); std::vector<u8> blocks = std::vector<u8>(static_cast<size_t>(df.file_size));
if (stream->Read(blocks.data(), df.file_size) != df.file_size) if (stream->Read(blocks.data(), df.file_size) != df.file_size)
{ {
@ -560,6 +638,29 @@ static bool ImportSaveWithDirectoryFrame(DataArray* data, const char* filename,
return false; return false;
} }
const u32 num_blocks = (static_cast<u32>(blocks.size()) + (BLOCK_SIZE - 1)) / BLOCK_SIZE;
if (GetFreeBlockCount(*data) < num_blocks)
{
Log_ErrorPrintf("Failed to write file to memory card: insufficient free blocks");
return false;
}
// Make sure there isn't already a save with the same name
std::vector<FileInfo> fileinfos = EnumerateFiles(*data, true);
for (const FileInfo& fi : fileinfos)
{
if (fi.filename.compare(0, sizeof(df.filename), df.filename) == 0)
{
if (!fi.deleted)
{
Log_ErrorPrintf("Save file with the same name '%s' already exists in memory card", fi.filename.c_str());
return false;
}
DeleteFile(data, fi, true);
}
}
return WriteFile(data, df.filename, blocks); return WriteFile(data, df.filename, blocks);
} }
@ -575,17 +676,6 @@ static bool ImportRawSave(DataArray* data, const char* filename, const FILESYSTE
if (save_name.length() > DirectoryFrame::FILE_NAME_LENGTH) if (save_name.length() > DirectoryFrame::FILE_NAME_LENGTH)
save_name.erase(DirectoryFrame::FILE_NAME_LENGTH); save_name.erase(DirectoryFrame::FILE_NAME_LENGTH);
// Make sure there isn't already a save with the same name
std::vector<FileInfo> fileinfos = EnumerateFiles(*data);
for (const FileInfo& fi : fileinfos)
{
if (fi.filename.compare(save_name) == 0)
{
Log_ErrorPrintf("Save file with the same name (%s) already exists in memory card", save_name.c_str());
return false;
}
}
std::optional<std::vector<u8>> blocks = FileSystem::ReadBinaryFile(filename); std::optional<std::vector<u8>> blocks = FileSystem::ReadBinaryFile(filename);
if (!blocks.has_value()) if (!blocks.has_value())
{ {
@ -593,6 +683,29 @@ static bool ImportRawSave(DataArray* data, const char* filename, const FILESYSTE
return false; return false;
} }
const u32 num_blocks = (static_cast<u32>(blocks->size()) + (BLOCK_SIZE - 1)) / BLOCK_SIZE;
if (GetFreeBlockCount(*data) < num_blocks)
{
Log_ErrorPrintf("Failed to write file to memory card: insufficient free blocks");
return false;
}
// Make sure there isn't already a save with the same name
std::vector<FileInfo> fileinfos = EnumerateFiles(*data, true);
for (const FileInfo& fi : fileinfos)
{
if (fi.filename.compare(save_name) == 0)
{
if (!fi.deleted)
{
Log_ErrorPrintf("Save file with the same name '%s' already exists in memory card", fi.filename.c_str());
return false;
}
DeleteFile(data, fi, true);
}
}
return WriteFile(data, save_name, blocks.value()); return WriteFile(data, save_name, blocks.value());
} }

View file

@ -39,16 +39,18 @@ struct FileInfo
u32 size; u32 size;
u32 first_block; u32 first_block;
u32 num_blocks; u32 num_blocks;
bool deleted;
std::vector<IconFrame> icon_frames; std::vector<IconFrame> icon_frames;
}; };
bool IsValid(const DataArray& data); bool IsValid(const DataArray& data);
u32 GetFreeBlockCount(const DataArray& data); u32 GetFreeBlockCount(const DataArray& data);
std::vector<FileInfo> EnumerateFiles(const DataArray& data); std::vector<FileInfo> EnumerateFiles(const DataArray& data, bool include_deleted);
bool ReadFile(const DataArray& data, const FileInfo& fi, std::vector<u8>* buffer); bool ReadFile(const DataArray& data, const FileInfo& fi, std::vector<u8>* buffer);
bool WriteFile(DataArray* data, const std::string_view& filename, const std::vector<u8>& buffer); bool WriteFile(DataArray* data, const std::string_view& filename, const std::vector<u8>& buffer);
bool DeleteFile(DataArray* data, const FileInfo& fi); bool DeleteFile(DataArray* data, const FileInfo& fi, bool clear_sectors);
bool UndeleteFile(DataArray* data, const FileInfo& fi);
bool ImportCard(DataArray* data, const char* filename); bool ImportCard(DataArray* data, const char* filename);
bool ImportCard(DataArray* data, const char* filename, std::vector<u8> file_data); bool ImportCard(DataArray* data, const char* filename, std::vector<u8> file_data);
bool ExportSave(DataArray* data, const FileInfo& fi, const char* filename); bool ExportSave(DataArray* data, const FileInfo& fi, const char* filename);

View file

@ -105,6 +105,7 @@ void MemoryCardEditorDialog::connectUi()
connect(m_ui.moveLeft, &QPushButton::clicked, this, &MemoryCardEditorDialog::doCopyFile); connect(m_ui.moveLeft, &QPushButton::clicked, this, &MemoryCardEditorDialog::doCopyFile);
connect(m_ui.moveRight, &QPushButton::clicked, this, &MemoryCardEditorDialog::doCopyFile); connect(m_ui.moveRight, &QPushButton::clicked, this, &MemoryCardEditorDialog::doCopyFile);
connect(m_ui.deleteFile, &QPushButton::clicked, this, &MemoryCardEditorDialog::doDeleteFile); connect(m_ui.deleteFile, &QPushButton::clicked, this, &MemoryCardEditorDialog::doDeleteFile);
connect(m_ui.undeleteFile, &QPushButton::clicked, this, &MemoryCardEditorDialog::doUndeleteFile);
connect(m_ui.cardAPath, QOverload<int>::of(&QComboBox::currentIndexChanged), connect(m_ui.cardAPath, QOverload<int>::of(&QComboBox::currentIndexChanged),
[this](int index) { loadCardFromComboBox(&m_card_a, index); }); [this](int index) { loadCardFromComboBox(&m_card_a, index); });
@ -219,11 +220,20 @@ bool MemoryCardEditorDialog::loadCard(const QString& filename, Card* card)
return true; return true;
} }
static void setCardTableItemProperties(QTableWidgetItem* item, const MemoryCardImage::FileInfo& fi)
{
if (fi.deleted)
{
item->setBackground(Qt::darkRed);
item->setForeground(Qt::white);
}
}
void MemoryCardEditorDialog::updateCardTable(Card* card) void MemoryCardEditorDialog::updateCardTable(Card* card)
{ {
card->table->setRowCount(0); card->table->setRowCount(0);
card->files = MemoryCardImage::EnumerateFiles(card->data); card->files = MemoryCardImage::EnumerateFiles(card->data, true);
for (const MemoryCardImage::FileInfo& fi : card->files) for (const MemoryCardImage::FileInfo& fi : card->files)
{ {
const int row = card->table->rowCount(); const int row = card->table->rowCount();
@ -235,13 +245,26 @@ void MemoryCardEditorDialog::updateCardTable(Card* card)
MemoryCardImage::ICON_HEIGHT, QImage::Format_RGBA8888); MemoryCardImage::ICON_HEIGHT, QImage::Format_RGBA8888);
QTableWidgetItem* icon = new QTableWidgetItem(); QTableWidgetItem* icon = new QTableWidgetItem();
setCardTableItemProperties(icon, fi);
icon->setIcon(QIcon(QPixmap::fromImage(image))); icon->setIcon(QIcon(QPixmap::fromImage(image)));
card->table->setItem(row, 0, icon); card->table->setItem(row, 0, icon);
} }
card->table->setItem(row, 1, new QTableWidgetItem(QString::fromStdString(fi.title))); QString title_str(QString::fromStdString(fi.title));
card->table->setItem(row, 2, new QTableWidgetItem(QString::fromStdString(fi.filename))); if (fi.deleted)
card->table->setItem(row, 3, new QTableWidgetItem(QString::number(fi.num_blocks))); title_str += tr(" (Deleted)");
QTableWidgetItem* item = new QTableWidgetItem(title_str);
setCardTableItemProperties(item, fi);
card->table->setItem(row, 1, item);
item = new QTableWidgetItem(QString::fromStdString(fi.filename));
setCardTableItemProperties(item, fi);
card->table->setItem(row, 2, item);
item = new QTableWidgetItem(QString::number(fi.num_blocks));
setCardTableItemProperties(item, fi);
card->table->setItem(row, 3, item);
} }
} }
@ -390,9 +413,9 @@ void MemoryCardEditorDialog::doCopyFile()
} }
clearSelection(); clearSelection();
setCardDirty(dst);
updateCardTable(dst); updateCardTable(dst);
updateCardBlocksFree(dst); updateCardBlocksFree(dst);
setCardDirty(dst);
updateButtonState(); updateButtonState();
} }
@ -402,16 +425,38 @@ void MemoryCardEditorDialog::doDeleteFile()
if (!fi) if (!fi)
return; return;
if (!MemoryCardImage::DeleteFile(&card->data, *fi)) if (!MemoryCardImage::DeleteFile(&card->data, *fi, fi->deleted))
{ {
QMessageBox::critical(this, tr("Error"), tr("Failed to delete file %1").arg(QString::fromStdString(fi->filename))); QMessageBox::critical(this, tr("Error"), tr("Failed to delete file %1").arg(QString::fromStdString(fi->filename)));
return; return;
} }
clearSelection(); clearSelection();
setCardDirty(card);
updateCardTable(card); updateCardTable(card);
updateCardBlocksFree(card); updateCardBlocksFree(card);
updateButtonState();
}
void MemoryCardEditorDialog::doUndeleteFile()
{
const auto [card, fi] = getSelectedFile();
if (!fi)
return;
if (!MemoryCardImage::UndeleteFile(&card->data, *fi))
{
QMessageBox::critical(
this, tr("Error"),
tr("Failed to undelete file %1. The file may have been partially overwritten by another save.")
.arg(QString::fromStdString(fi->filename)));
return;
}
clearSelection();
setCardDirty(card); setCardDirty(card);
updateCardTable(card);
updateCardBlocksFree(card);
updateButtonState(); updateButtonState();
} }
@ -455,9 +500,9 @@ void MemoryCardEditorDialog::importCard(Card* card)
clearSelection(); clearSelection();
card->data = *temp; card->data = *temp;
setCardDirty(card);
updateCardTable(card); updateCardTable(card);
updateCardBlocksFree(card); updateCardBlocksFree(card);
setCardDirty(card);
updateButtonState(); updateButtonState();
} }
@ -478,9 +523,9 @@ void MemoryCardEditorDialog::formatCard(Card* card)
MemoryCardImage::Format(&card->data); MemoryCardImage::Format(&card->data);
setCardDirty(card);
updateCardTable(card); updateCardTable(card);
updateCardBlocksFree(card); updateCardBlocksFree(card);
setCardDirty(card);
updateButtonState(); updateButtonState();
} }
@ -500,9 +545,9 @@ void MemoryCardEditorDialog::importSaveFile(Card* card)
return; return;
} }
setCardDirty(card);
updateCardTable(card); updateCardTable(card);
updateCardBlocksFree(card); updateCardBlocksFree(card);
setCardDirty(card);
} }
std::tuple<MemoryCardEditorDialog::Card*, const MemoryCardImage::FileInfo*> MemoryCardEditorDialog::getSelectedFile() std::tuple<MemoryCardEditorDialog::Card*, const MemoryCardImage::FileInfo*> MemoryCardEditorDialog::getSelectedFile()
@ -530,10 +575,12 @@ void MemoryCardEditorDialog::updateButtonState()
const auto [selected_card, selected_file] = getSelectedFile(); const auto [selected_card, selected_file] = getSelectedFile();
const bool is_card_b = (selected_card == &m_card_b); const bool is_card_b = (selected_card == &m_card_b);
const bool has_selection = (selected_file != nullptr); const bool has_selection = (selected_file != nullptr);
const bool is_deleted = (selected_file != nullptr && selected_file->deleted);
const bool card_a_present = !m_card_a.filename.empty(); const bool card_a_present = !m_card_a.filename.empty();
const bool card_b_present = !m_card_b.filename.empty(); const bool card_b_present = !m_card_b.filename.empty();
const bool both_cards_present = card_a_present && card_b_present; const bool both_cards_present = card_a_present && card_b_present;
m_ui.deleteFile->setEnabled(has_selection); m_ui.deleteFile->setEnabled(has_selection);
m_ui.undeleteFile->setEnabled(is_deleted);
m_ui.exportFile->setEnabled(has_selection); m_ui.exportFile->setEnabled(has_selection);
m_ui.moveLeft->setEnabled(both_cards_present && has_selection && is_card_b); m_ui.moveLeft->setEnabled(both_cards_present && has_selection && is_card_b);
m_ui.moveRight->setEnabled(both_cards_present && has_selection && !is_card_b); m_ui.moveRight->setEnabled(both_cards_present && has_selection && !is_card_b);

View file

@ -29,6 +29,7 @@ private Q_SLOTS:
void onCardBSelectionChanged(); void onCardBSelectionChanged();
void doCopyFile(); void doCopyFile();
void doDeleteFile(); void doDeleteFile();
void doUndeleteFile();
private: private:
struct Card struct Card

View file

@ -288,6 +288,16 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QPushButton" name="undeleteFile">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Undelete File</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QPushButton" name="exportFile"> <widget class="QPushButton" name="exportFile">
<property name="enabled"> <property name="enabled">