diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index cfc10490e..98e1cbd52 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -20,6 +20,7 @@ add_library(common cd_image_memory.cpp cd_image_mds.cpp cd_image_pbp.cpp + cd_image_ppf.cpp cd_subchannel_replacement.cpp cd_subchannel_replacement.h cd_xa.cpp diff --git a/src/common/cd_image.h b/src/common/cd_image.h index 8ac63e26e..221837d8e 100644 --- a/src/common/cd_image.h +++ b/src/common/cd_image.h @@ -135,6 +135,12 @@ public: BitField digital_copy_permitted; BitField data; BitField four_channel_audio; + + Control& operator=(const Control& rhs) + { + bits = rhs.bits; + return *this; + } }; struct @@ -205,6 +211,8 @@ public: static std::unique_ptr OpenM3uImage(const char* filename, Common::Error* error); static std::unique_ptr CreateMemoryImage(CDImage* image, ProgressCallback* progress = ProgressCallback::NullProgressCallback); + static std::unique_ptr OverlayPPFPatch(const char* filename, std::unique_ptr parent_image, + ProgressCallback* progress = ProgressCallback::NullProgressCallback); // Accessors. const std::string& GetFileName() const { return m_filename; } @@ -226,6 +234,8 @@ public: u32 GetFirstTrackNumber() const { return m_tracks.front().track_number; } u32 GetLastTrackNumber() const { return m_tracks.back().track_number; } u32 GetIndexCount() const { return static_cast(m_indices.size()); } + const std::vector& GetTracks() const { return m_tracks; } + const std::vector& GetIndices() const { return m_indices; } const Track& GetTrack(u32 track) const; const Index& GetIndex(u32 i) const; diff --git a/src/common/cd_image_ppf.cpp b/src/common/cd_image_ppf.cpp new file mode 100644 index 000000000..ef2c1120d --- /dev/null +++ b/src/common/cd_image_ppf.cpp @@ -0,0 +1,440 @@ +#include "assert.h" +#include "cd_image.h" +#include "cd_subchannel_replacement.h" +#include "file_system.h" +#include "log.h" +#include +#include +#include +#include +Log_SetChannel(CDImagePPF); + +enum : u32 +{ + DESC_SIZE = 50, + BLOCKCHECK_SIZE = 1024 +}; + +class CDImagePPF : public CDImage +{ +public: + CDImagePPF(); + ~CDImagePPF() override; + + bool Open(const char* filename, std::unique_ptr parent_image); + + bool ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) override; + bool HasNonStandardSubchannel() const override; + + std::string GetMetadata(const std::string_view& type) const override; + std::string GetSubImageMetadata(u32 index, const std::string_view& type) const override; + +protected: + bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override; + +private: + bool ReadV1Patch(std::FILE* fp); + bool ReadV2Patch(std::FILE* fp); + bool ReadV3Patch(std::FILE* fp); + u32 ReadFileIDDiz(std::FILE* fp, u32 version); + + bool AddPatch(u64 offset, const u8* patch, u32 patch_size); + + std::unique_ptr m_parent_image; + std::vector m_replacement_data; + std::unordered_map m_replacement_map; + u32 m_replacement_offset = 0; +}; + +CDImagePPF::CDImagePPF() = default; + +CDImagePPF::~CDImagePPF() = default; + +bool CDImagePPF::Open(const char* filename, std::unique_ptr parent_image) +{ + auto fp = FileSystem::OpenManagedCFile(filename, "rb"); + if (!fp) + { + Log_ErrorPrintf("Failed to open '%s'", filename); + return false; + } + + u32 magic; + if (std::fread(&magic, sizeof(magic), 1, fp.get()) != 1) + { + Log_ErrorPrintf("Failed to read magic from '%s'", filename); + return false; + } + + // work out the offset from the start of the parent image which we need to patch + // i.e. the two second implicit pregap on data sectors + if (parent_image->GetTrack(1).mode != TrackMode::Audio) + m_replacement_offset = parent_image->GetIndex(1).start_lba_on_disc; + + // copy all the stuff from the parent image + m_filename = filename; + m_tracks = parent_image->GetTracks(); + m_indices = parent_image->GetIndices(); + m_parent_image = std::move(parent_image); + + if (magic == '3FPP') + return ReadV3Patch(fp.get()); + else if (magic == '2FPP') + return ReadV2Patch(fp.get()); + else if (magic == '1FPP') + return ReadV1Patch(fp.get()); + + Log_ErrorPrintf("Unknown PPF magic %08X", magic); + return false; +} + +u32 CDImagePPF::ReadFileIDDiz(std::FILE* fp, u32 version) +{ + const int lenidx = (version == 2) ? 4 : 2; + + u32 magic; + if (std::fseek(fp, -(lenidx + 4), SEEK_END) != 0 || std::fread(&magic, sizeof(magic), 1, fp) != 1) + { + Log_WarningPrintf("Failed to read diz magic"); + return 0; + } + + if (magic != 'ZID.') + return 0; + + u32 dlen = 0; + if (std::fseek(fp, -lenidx, SEEK_END) != 0 || std::fread(&dlen, lenidx, 1, fp) != 1) + { + Log_WarningPrintf("Failed to read diz length"); + return 0; + } + + if (dlen > static_cast(std::ftell(fp))) + { + Log_WarningPrintf("diz length out of range"); + return 0; + } + + std::string fdiz; + fdiz.resize(dlen); + if (std::fseek(fp, -(lenidx + 16 + static_cast(dlen)), SEEK_END) != 0 || + std::fread(fdiz.data(), 1, dlen, fp) != dlen) + { + Log_WarningPrintf("Failed to read fdiz"); + return 0; + } + + Log_InfoPrintf("File_Id.diz: %s", fdiz.c_str()); + return dlen; +} + +bool CDImagePPF::ReadV1Patch(std::FILE* fp) +{ + char desc[DESC_SIZE + 1] = {}; + if (std::fseek(fp, 6, SEEK_SET) != 0 || std::fread(desc, sizeof(char), DESC_SIZE, fp) != DESC_SIZE) + { + Log_ErrorPrintf("Failed to read description"); + return false; + } + + u32 filelen; + if (std::fseek(fp, 0, SEEK_END) != 0 || (filelen = static_cast(std::ftell(fp))) == 0 || filelen < 56) + { + Log_ErrorPrintf("Invalid ppf file"); + return false; + } + + u32 count = filelen - 56; + if (count <= 0) + return false; + + if (std::fseek(fp, 56, SEEK_SET) != 0) + return false; + + std::vector temp; + while (count > 0) + { + u32 offset; + u8 chunk_size; + if (std::fread(&offset, sizeof(offset), 1, fp) != 1 || std::fread(&chunk_size, sizeof(chunk_size), 1, fp) != 1) + { + Log_ErrorPrintf("Incomplete ppf"); + return false; + } + + temp.resize(chunk_size); + if (std::fread(temp.data(), 1, chunk_size, fp) != chunk_size) + { + Log_ErrorPrintf("Failed to read patch data"); + return false; + } + + if (!AddPatch(offset, temp.data(), chunk_size)) + return false; + + count -= sizeof(offset) + sizeof(chunk_size) + chunk_size; + } + + Log_InfoPrintf("Loaded %zu replacement sectors from version 1 PPF", m_replacement_map.size()); + return true; +} + +bool CDImagePPF::ReadV2Patch(std::FILE* fp) +{ + char desc[DESC_SIZE + 1] = {}; + if (std::fseek(fp, 6, SEEK_SET) != 0 || std::fread(desc, sizeof(char), DESC_SIZE, fp) != DESC_SIZE) + { + Log_ErrorPrintf("Failed to read description"); + return false; + } + + Log_InfoPrintf("Patch description: %s", desc); + + const u32 idlen = ReadFileIDDiz(fp, 2); + + u32 origlen; + if (std::fseek(fp, 56, SEEK_SET) != 0 || std::fread(&origlen, sizeof(origlen), 1, fp) != 1) + { + Log_ErrorPrintf("Failed to read size"); + return false; + } + + std::vector temp; + temp.resize(BLOCKCHECK_SIZE); + if (std::fread(temp.data(), 1, BLOCKCHECK_SIZE, fp) != BLOCKCHECK_SIZE) + { + Log_ErrorPrintf("Failed to read blockcheck data"); + return false; + } + + // do blockcheck + { + u32 blockcheck_src_sector = 16 + m_replacement_offset; + u32 blockcheck_src_offset = 32; + + std::vector src_sector(RAW_SECTOR_SIZE); + if (m_parent_image->Seek(blockcheck_src_sector) && m_parent_image->ReadRawSector(src_sector.data(), nullptr)) + { + if (std::memcmp(&src_sector[blockcheck_src_offset], temp.data(), BLOCKCHECK_SIZE) != 0) + Log_WarningPrintf("Blockcheck failed. The patch may not apply correctly."); + } + else + { + Log_WarningPrintf("Failed to read blockcheck sector %u", blockcheck_src_sector); + } + } + + u32 filelen; + if (std::fseek(fp, 0, SEEK_END) != 0 || (filelen = static_cast(std::ftell(fp))) == 0 || filelen < 1084) + { + Log_ErrorPrintf("Invalid ppf file"); + return false; + } + + u32 count = filelen - 1084; + if (idlen > 0) + count -= (idlen + 38); + + if (count <= 0) + return false; + + if (std::fseek(fp, 1084, SEEK_SET) != 0) + return false; + + while (count > 0) + { + u32 offset; + u8 chunk_size; + if (std::fread(&offset, sizeof(offset), 1, fp) != 1 || std::fread(&chunk_size, sizeof(chunk_size), 1, fp) != 1) + { + Log_ErrorPrintf("Incomplete ppf"); + return false; + } + + temp.resize(chunk_size); + if (std::fread(temp.data(), 1, chunk_size, fp) != chunk_size) + { + Log_ErrorPrintf("Failed to read patch data"); + return false; + } + + if (!AddPatch(offset, temp.data(), chunk_size)) + return false; + + count -= sizeof(offset) + sizeof(chunk_size) + chunk_size; + } + + Log_InfoPrintf("Loaded %zu replacement sectors from version 2 PPF", m_replacement_map.size()); + return true; +} + +bool CDImagePPF::ReadV3Patch(std::FILE* fp) +{ + char desc[DESC_SIZE + 1] = {}; + if (std::fseek(fp, 6, SEEK_SET) != 0 || std::fread(desc, sizeof(char), DESC_SIZE, fp) != DESC_SIZE) + { + Log_ErrorPrintf("Failed to read description"); + return false; + } + + Log_InfoPrintf("Patch description: %s", desc); + + u32 idlen = ReadFileIDDiz(fp, 3); + + u8 image_type; + u8 block_check; + u8 undo; + if (std::fseek(fp, 56, SEEK_SET) != 0 || std::fread(&image_type, sizeof(image_type), 1, fp) != 1 || + std::fread(&block_check, sizeof(block_check), 1, fp) != 1 || std::fread(&undo, sizeof(undo), 1, fp) != 1) + { + Log_ErrorPrintf("Failed to read headers"); + return false; + } + + // TODO: Blockcheck + + std::fseek(fp, 0, SEEK_END); + u32 count = static_cast(std::ftell(fp)); + + u32 seekpos = (block_check) ? 1084 : 60; + if (seekpos >= count) + { + Log_ErrorPrintf("File is too short"); + return false; + } + + count -= seekpos; + if (idlen > 0) + { + const u32 extralen = idlen + 18 + 16 + 2; + if (count < extralen) + { + Log_ErrorPrintf("File is too short (diz)"); + return false; + } + + count -= extralen; + } + + if (std::fseek(fp, seekpos, SEEK_SET) != 0) + return false; + + std::vector temp; + + while (count > 0) + { + u64 offset; + u8 chunk_size; + if (std::fread(&offset, sizeof(offset), 1, fp) != 1 || std::fread(&chunk_size, sizeof(chunk_size), 1, fp) != 1) + { + Log_ErrorPrintf("Incomplete ppf"); + return false; + } + + temp.resize(chunk_size); + if (std::fread(temp.data(), 1, chunk_size, fp) != chunk_size) + { + Log_ErrorPrintf("Failed to read patch data"); + return false; + } + + if (!AddPatch(offset, temp.data(), chunk_size)) + return false; + + count -= sizeof(offset) + sizeof(chunk_size) + chunk_size; + } + + Log_InfoPrintf("Loaded %zu replacement sectors from version 3 PPF", m_replacement_map.size()); + return true; +} + +bool CDImagePPF::AddPatch(u64 offset, const u8* patch, u32 patch_size) +{ + Log_DebugPrintf("Starting applying patch of %u bytes at at offset %" PRIu64, patch_size, offset); + + while (patch_size > 0) + { + const u32 sector_index = Truncate32(offset / RAW_SECTOR_SIZE) + m_replacement_offset; + const u32 sector_offset = Truncate32(offset % RAW_SECTOR_SIZE); + if (sector_index >= m_parent_image->GetLBACount()) + { + Log_ErrorPrintf("Sector %u in patch is out of range", sector_index); + return false; + } + + const u32 bytes_to_patch = std::min(patch_size, RAW_SECTOR_SIZE - sector_offset); + + auto iter = m_replacement_map.find(sector_index); + if (iter == m_replacement_map.end()) + { + const u32 replacement_buffer_start = static_cast(m_replacement_data.size()); + m_replacement_data.resize(m_replacement_data.size() + RAW_SECTOR_SIZE); + if (!m_parent_image->Seek(sector_index) || + !m_parent_image->ReadRawSector(&m_replacement_data[replacement_buffer_start], nullptr)) + { + Log_ErrorPrintf("Failed to read sector %u from parent image", sector_index); + return false; + } + + iter = m_replacement_map.emplace(sector_index, replacement_buffer_start).first; + } + + // patch it! + Log_DebugPrintf(" Patching %u bytes at sector %u offset %u", bytes_to_patch, sector_index, sector_offset); + std::memcpy(&m_replacement_data[iter->second + sector_offset], patch, bytes_to_patch); + offset += bytes_to_patch; + patch += bytes_to_patch; + patch_size -= bytes_to_patch; + } + + return true; +} + +bool CDImagePPF::ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) +{ + return m_parent_image->ReadSubChannelQ(subq, index, lba_in_index); +} + +bool CDImagePPF::HasNonStandardSubchannel() const +{ + return m_parent_image->HasNonStandardSubchannel(); +} + +std::string CDImagePPF::GetMetadata(const std::string_view& type) const +{ + return m_parent_image->GetMetadata(type); +} + +std::string CDImagePPF::GetSubImageMetadata(u32 index, const std::string_view& type) const +{ + // We only support a single sub-image for patched games. + std::string ret; + if (index == 0) + ret = m_parent_image->GetSubImageMetadata(index, type); + + return ret; +} + +bool CDImagePPF::ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) +{ + DebugAssert(index.file_index == 0); + + const u32 sector_number = index.start_lba_on_disc + lba_in_index; + const auto it = m_replacement_map.find(sector_number); + if (it == m_replacement_map.end()) + return m_parent_image->ReadSectorFromIndex(buffer, index, lba_in_index); + + std::memcpy(buffer, &m_replacement_data[it->second], RAW_SECTOR_SIZE); + return true; +} + +std::unique_ptr +CDImage::OverlayPPFPatch(const char* filename, std::unique_ptr parent_image, + ProgressCallback* progress /* = ProgressCallback::NullProgressCallback */) +{ + std::unique_ptr memory_image = std::make_unique(); + if (!memory_image->Open(filename, std::move(parent_image))) + return {}; + + return memory_image; +} diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj index 040ea4558..e2c994153 100644 --- a/src/common/common.vcxproj +++ b/src/common/common.vcxproj @@ -141,6 +141,7 @@ + @@ -763,4 +764,4 @@ - \ No newline at end of file + diff --git a/src/common/common.vcxproj.filters b/src/common/common.vcxproj.filters index 2e4b7d145..169ad7bf8 100644 --- a/src/common/common.vcxproj.filters +++ b/src/common/common.vcxproj.filters @@ -220,6 +220,7 @@ + @@ -238,4 +239,4 @@ {fd4150b0-6f82-4251-ab23-34c25fbc5b5e} - \ No newline at end of file +