CDImageCHD: Support loading parent/delta CHDs

This commit is contained in:
Stenzek 2023-08-12 14:40:34 +10:00
parent f41384c6c8
commit 165b277421
1 changed files with 107 additions and 17 deletions

View File

@ -7,14 +7,19 @@
#include "cd_image.h"
#include "cd_subchannel_replacement.h"
#include "common/align.h"
#include "common/assert.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
#include "common/platform.h"
#include "common/string_util.h"
#include "fmt/format.h"
#include "libchdr/chd.h"
#include <algorithm>
#include <cerrno>
#include <cstdio>
@ -22,6 +27,7 @@
#include <limits>
#include <map>
#include <optional>
Log_SetChannel(CDImageCHD);
static std::optional<CDImage::TrackMode> ParseTrackModeString(const char* str)
@ -46,6 +52,7 @@ static std::optional<CDImage::TrackMode> ParseTrackModeString(const char* str)
return std::nullopt;
}
namespace {
class CDImageCHD : public CDImage
{
public:
@ -66,12 +73,13 @@ private:
enum : u32
{
CHD_CD_SECTOR_DATA_SIZE = 2352 + 96,
CHD_CD_TRACK_ALIGNMENT = 4
CHD_CD_TRACK_ALIGNMENT = 4,
MAX_PARENTS = 32 // Surely someone wouldn't be insane enough to go beyond this...
};
chd_file* OpenCHD(const char* filename, FileSystem::ManagedCFilePtr fp, Common::Error* error, u32 recursion_level);
bool ReadHunk(u32 hunk_index);
std::FILE* m_fp = nullptr;
chd_file* m_chd = nullptr;
u32 m_hunk_size = 0;
u32 m_sectors_per_hunk = 0;
@ -82,6 +90,7 @@ private:
CDSubChannelReplacement m_sbi;
};
} // namespace
CDImageCHD::CDImageCHD() = default;
@ -89,15 +98,102 @@ CDImageCHD::~CDImageCHD()
{
if (m_chd)
chd_close(m_chd);
if (m_fp)
std::fclose(m_fp);
}
chd_file* CDImageCHD::OpenCHD(const char* filename, FileSystem::ManagedCFilePtr fp, Common::Error* error,
u32 recursion_level)
{
chd_file* chd;
chd_error err = chd_open_file(fp.get(), CHD_OPEN_READ | CHD_OPEN_TRANSFER_FILE, nullptr, &chd);
if (err == CHDERR_NONE)
{
// fp is now managed by libchdr
fp.release();
return chd;
}
else if (err != CHDERR_REQUIRES_PARENT)
{
Log_ErrorPrintf("Failed to open CHD '%s': %s", filename, chd_error_string(err));
if (error)
error->SetMessage(chd_error_string(err));
return nullptr;
}
if (recursion_level >= MAX_PARENTS)
{
Log_ErrorPrintf("Failed to open CHD '%s': Too many parent files", filename);
if (error)
error->SetMessage("Too many parent files");
return nullptr;
}
// Need to get the sha1 to look for.
chd_header header;
err = chd_read_header_file(fp.get(), &header);
if (err != CHDERR_NONE)
{
Log_ErrorPrintf("Failed to read CHD header '%s': %s", filename, chd_error_string(err));
if (error)
error->SetMessage(chd_error_string(err));
return nullptr;
}
// Find a chd with a matching sha1 in the same directory.
// Have to do *.* and filter on the extension manually because Linux is case sensitive.
// We _could_ memoize the CHD headers here, but is anyone actually going to nest CHDs that deep?
chd_file* parent_chd = nullptr;
const std::string parent_dir(Path::GetDirectory(filename));
FileSystem::FindResultsArray parent_files;
FileSystem::FindFiles(parent_dir.c_str(), "*.*", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES, &parent_files);
for (FILESYSTEM_FIND_DATA& fd : parent_files)
{
if (StringUtil::EndsWithNoCase(Path::GetExtension(fd.FileName), ".chd"))
continue;
auto parent_fp =
FileSystem::OpenManagedSharedCFile(fd.FileName.c_str(), "rb", FileSystem::FileShareMode::DenyWrite);
if (!parent_fp)
continue;
chd_header parent_header;
err = chd_read_header_file(parent_fp.get(), &parent_header);
if (err != CHDERR_NONE || !chd_is_matching_parent(&header, &parent_header))
continue;
// Match! Open this one.
if ((parent_chd = OpenCHD(fd.FileName.c_str(), std::move(parent_fp), error, recursion_level + 1)) != nullptr)
{
Log_DevPrintf(fmt::format("Found parent CHD '{}' for '{}'.", Path::GetFileName(fd.FileName), Path::GetFileName(filename)).c_str());
break;
}
}
if (!parent_chd)
{
Log_ErrorPrintf("Failed to open CHD '%s': Failed to find parent CHD, it must be in the same directory.", filename);
if (error)
error->SetMessage("Failed to find parent CHD, it must be in the same directory.");
return nullptr;
}
// Now try re-opening with the parent.
err = chd_open_file(fp.get(), CHD_OPEN_READ | CHD_OPEN_TRANSFER_FILE, parent_chd, &chd);
if (err != CHDERR_NONE)
{
Log_ErrorPrintf("Failed to open CHD '%s': %s", filename, chd_error_string(err));
if (error)
error->SetMessage(chd_error_string(err));
return nullptr;
}
// fp now owned by libchdr
fp.release();
return chd;
}
bool CDImageCHD::Open(const char* filename, Common::Error* error)
{
Assert(!m_fp);
m_fp = FileSystem::OpenCFile(filename, "rb");
if (!m_fp)
auto fp = FileSystem::OpenManagedSharedCFile(filename, "rb", FileSystem::FileShareMode::DenyWrite);
if (!fp)
{
Log_ErrorPrintf("Failed to open CHD '%s': errno %d", filename, errno);
if (error)
@ -106,15 +202,9 @@ bool CDImageCHD::Open(const char* filename, Common::Error* error)
return false;
}
chd_error err = chd_open_file(m_fp, CHD_OPEN_READ, nullptr, &m_chd);
if (err != CHDERR_NONE)
{
Log_ErrorPrintf("Failed to open CHD '%s': %s", filename, chd_error_string(err));
if (error)
error->SetMessage(chd_error_string(err));
m_chd = OpenCHD(filename, std::move(fp), error, 0);
if (!m_chd)
return false;
}
const chd_header* header = chd_get_header(m_chd);
m_hunk_size = header->hunkbytes;
@ -146,8 +236,8 @@ bool CDImageCHD::Open(const char* filename, Common::Error* error)
u32 metadata_length;
int track_num = 0, frames = 0, pregap_frames = 0, postgap_frames = 0;
err = chd_get_metadata(m_chd, CDROM_TRACK_METADATA2_TAG, num_tracks, metadata_str, sizeof(metadata_str),
&metadata_length, nullptr, nullptr);
chd_error err = chd_get_metadata(m_chd, CDROM_TRACK_METADATA2_TAG, num_tracks, metadata_str, sizeof(metadata_str),
&metadata_length, nullptr, nullptr);
if (err == CHDERR_NONE)
{
if (std::sscanf(metadata_str, CDROM_TRACK_METADATA2_FORMAT, &track_num, type_str, subtype_str, &frames,