diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 5a7cd1da6..220395860 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -18,6 +18,7 @@ add_library(common cd_image_hasher.h cd_image_memory.cpp cd_image_mds.cpp + cd_image_pbp.cpp cd_subchannel_replacement.cpp cd_subchannel_replacement.h cd_xa.cpp @@ -62,9 +63,10 @@ add_library(common memory_arena.h page_fault_handler.cpp page_fault_handler.h - rectangle.h + pbp_types.h progress_callback.cpp progress_callback.h + rectangle.h scope_guard.h shiftjis.cpp shiftjis.h diff --git a/src/common/cd_image.cpp b/src/common/cd_image.cpp index 20a12812f..0ecd091d9 100644 --- a/src/common/cd_image.cpp +++ b/src/common/cd_image.cpp @@ -50,6 +50,10 @@ std::unique_ptr CDImage::Open(const char* filename) { return OpenMdsImage(filename); } + else if (CASE_COMPARE(extension, ".pbp") == 0) + { + return OpenPBPImage(filename); + } #undef CASE_COMPARE diff --git a/src/common/cd_image.h b/src/common/cd_image.h index 42493ec6b..a5966cd7e 100644 --- a/src/common/cd_image.h +++ b/src/common/cd_image.h @@ -197,6 +197,7 @@ public: static std::unique_ptr OpenCHDImage(const char* filename); static std::unique_ptr OpenEcmImage(const char* filename); static std::unique_ptr OpenMdsImage(const char* filename); + static std::unique_ptr OpenPBPImage(const char* filename); static std::unique_ptr CreateMemoryImage(CDImage* image, ProgressCallback* progress = ProgressCallback::NullProgressCallback); diff --git a/src/common/cd_image_pbp.cpp b/src/common/cd_image_pbp.cpp new file mode 100644 index 000000000..cc86c1de1 --- /dev/null +++ b/src/common/cd_image_pbp.cpp @@ -0,0 +1,743 @@ +#include "cd_image.h" +#include "cd_subchannel_replacement.h" +#include "file_system.h" +#include "log.h" +#include "pbp_types.h" +#include "string_util.h" +#include "zlib.h" +#include +#include +#include +Log_SetChannel(CDImagePBP); + +using namespace PBP; + +class CDImagePBP final : public CDImage +{ +public: + CDImagePBP() = default; + ~CDImagePBP() override; + + bool Open(const char* filename); + + bool ReadSubChannelQ(SubChannelQ* subq) override; + bool HasNonStandardSubchannel() const override; + +protected: + bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override; + +private: + struct BlockInfo + { + u32 offset; // Absolute offset from start of file + u16 size; + }; + +#if _DEBUG + static void PrintPBPHeaderInfo(const PBPHeader& pbp_header); + static void PrintSFOHeaderInfo(const SFOHeader& sfo_header); + static void PrintSFOIndexTableEntry(const SFOIndexTableEntry& sfo_index_table_entry, size_t i); + static void PrintSFOTable(const SFOTable& sfo_table); +#endif + + bool LoadPBPHeader(); + bool LoadSFOHeader(); + bool LoadSFOIndexTable(); + bool LoadSFOTable(); + + bool IsValidEboot(); + + bool InitDecompressionStream(); + bool DecompressBlock(BlockInfo block_info); + + FILE* m_file = nullptr; + + PBPHeader m_pbp_header; + SFOHeader m_sfo_header; + SFOTable m_sfo_table; + SFOIndexTable m_sfo_index_table; + + // Absolute offsets to ISO headers for multidisc file + std::vector m_multidisc_file_offsets; + u32 m_current_disc; + + // Absolute offsets and sizes of blocks in m_file + std::array m_blockinfo_table; + + std::array m_toc; + + u32 m_current_block = static_cast(-1); + std::array m_decompressed_block; + std::vector m_compressed_block; + + z_stream m_inflate_stream; + + CDSubChannelReplacement m_sbi; +}; + +namespace EndianHelper { +static constexpr bool HostIsLittleEndian() +{ + constexpr union + { + u8 a[4]; + u32 b; + } test_val = {1}; + + return test_val.a[0] == 1; +} + +template +static void SwapByteOrder(T& val) +{ + union + { + T t; + std::array arr; + } swap_val; + + swap_val.t = val; + std::reverse(std::begin(swap_val.arr), std::end(swap_val.arr)); + val = swap_val.t; +} +} // namespace EndianHelper + +CDImagePBP::~CDImagePBP() +{ + if (m_file) + fclose(m_file); + + inflateEnd(&m_inflate_stream); +} + +bool CDImagePBP::LoadPBPHeader() +{ + if (!m_file) + return false; + + if (fseek(m_file, 0, SEEK_END) != 0) + return false; + + if (ftell(m_file) < 0) + return false; + + if (fseek(m_file, 0, SEEK_SET) != 0) + return false; + + if (fread(&m_pbp_header, sizeof(PBPHeader), 1, m_file) != 1) + { + Log_ErrorPrint("Unable to read PBP header"); + return false; + } + + if (strncmp((char*)m_pbp_header.magic, "\0PBP", 4) != 0) + { + Log_ErrorPrint("PBP magic number mismatch"); + return false; + } + +#if _DEBUG + PrintPBPHeaderInfo(m_pbp_header); +#endif + + return true; +} + +bool CDImagePBP::LoadSFOHeader() +{ + if (fseek(m_file, m_pbp_header.param_sfo_offset, SEEK_SET) != 0) + return false; + + if (fread(&m_sfo_header, sizeof(SFOHeader), 1, m_file) != 1) + return false; + + if (strncmp((char*)m_sfo_header.magic, "\0PSF", 4) != 0) + { + Log_ErrorPrint("SFO magic number mismatch"); + return false; + } + +#if _DEBUG + PrintSFOHeaderInfo(m_sfo_header); +#endif + + return true; +} + +bool CDImagePBP::LoadSFOIndexTable() +{ + m_sfo_index_table.clear(); + m_sfo_index_table.resize(m_sfo_header.num_table_entries); + + if (fseek(m_file, m_pbp_header.param_sfo_offset + sizeof(m_sfo_header), SEEK_SET) != 0) + return false; + + if (fread(m_sfo_index_table.data(), sizeof(SFOIndexTableEntry), m_sfo_header.num_table_entries, m_file) != + m_sfo_header.num_table_entries) + return false; + +#if _DEBUG + for (size_t i = 0; i < static_cast(m_sfo_header.num_table_entries); ++i) + PrintSFOIndexTableEntry(m_sfo_index_table[i], i); +#endif + + return true; +} + +bool CDImagePBP::LoadSFOTable() +{ + m_sfo_table.clear(); + + for (size_t i = 0; i < static_cast(m_sfo_header.num_table_entries); ++i) + { + u32 abs_key_offset = + m_pbp_header.param_sfo_offset + m_sfo_header.key_table_offset + m_sfo_index_table[i].key_offset; + u32 abs_data_offset = + m_pbp_header.param_sfo_offset + m_sfo_header.data_table_offset + m_sfo_index_table[i].data_offset; + + if (fseek(m_file, abs_key_offset, SEEK_SET) != 0) + { + Log_ErrorPrintf("Failed seek to key for SFO table entry %zu", i); + return false; + } + + // Longest known key string is 20 characters total, including the null character + char key_cstr[20] = {}; + if (fgets(key_cstr, sizeof(key_cstr), m_file) == nullptr) + { + Log_ErrorPrintf("Failed to read key string for SFO table entry %zu", i); + return false; + } + + if (fseek(m_file, abs_data_offset, SEEK_SET) != 0) + { + Log_ErrorPrintf("Failed seek to data for SFO table entry %zu", i); + return false; + } + + if (m_sfo_index_table[i].data_type == 0x0004) // "special mode" UTF-8 (not null terminated) + { + Log_ErrorPrintf("Unhandled special mode UTF-8 type found in SFO table for entry %zu", i); + return false; + } + else if (m_sfo_index_table[i].data_type == 0x0204) // null-terminated UTF-8 character string + { + std::vector data_cstr(m_sfo_index_table[i].data_size); + if (fgets(data_cstr.data(), static_cast(data_cstr.size() * sizeof(char)), m_file) == nullptr) + { + Log_ErrorPrintf("Failed to read data string for SFO table entry %zu", i); + return false; + } + + m_sfo_table.emplace(std::string(key_cstr), std::string(data_cstr.data())); + } + else if (m_sfo_index_table[i].data_type == 0x0404) // uint32_t + { + u32 val; + if (fread(&val, sizeof(u32), 1, m_file) != 1) + { + Log_ErrorPrintf("Failed to read unsigned data value for SFO table entry %zu", i); + return false; + } + + m_sfo_table.emplace(std::string(key_cstr), val); + } + else + { + Log_ErrorPrintf("Unhandled SFO data type 0x%04X found in SFO table for entry %zu", m_sfo_index_table[i].data_type, + i); + return false; + } + } + +#if _DEBUG + PrintSFOTable(m_sfo_table); +#endif + + return true; +} + +bool CDImagePBP::IsValidEboot() +{ + // Check some fields to make sure this is a valid PS1 EBOOT.PBP + + auto a_it = m_sfo_table.find("BOOTABLE"); + if (a_it != m_sfo_table.end()) + { + SFOTableDataValue data_value = a_it->second; + if (!std::holds_alternative(data_value) || std::get(data_value) != 1) + { + Log_ErrorPrint("Invalid BOOTABLE value"); + return false; + } + } + else + { + Log_ErrorPrint("No BOOTABLE value found"); + return false; + } + + a_it = m_sfo_table.find("CATEGORY"); + if (a_it != m_sfo_table.end()) + { + SFOTableDataValue data_value = a_it->second; + if (!std::holds_alternative(data_value) || std::get(data_value) != "ME") + { + Log_ErrorPrint("Invalid CATEGORY value"); + return false; + } + } + else + { + Log_ErrorPrint("No CATEGORY value found"); + return false; + } + + return true; +} + +bool CDImagePBP::Open(const char* filename) +{ + if (!EndianHelper::HostIsLittleEndian()) + { + Log_ErrorPrint("Big endian hosts not currently supported"); + return false; + } + + m_file = FileSystem::OpenCFile(filename, "rb"); + if (!m_file) + return false; + + m_filename = filename; + + // Read in PBP header + if (!LoadPBPHeader()) + { + Log_ErrorPrint("Failed to load PBP header"); + return false; + } + + // Read in SFO header + if (!LoadSFOHeader()) + { + Log_ErrorPrint("Failed to load SFO header"); + return false; + } + + // Read in SFO index table + if (!LoadSFOIndexTable()) + { + Log_ErrorPrint("Failed to load SFO index table"); + return false; + } + + // Read in SFO table + if (!LoadSFOTable()) + { + Log_ErrorPrint("Failed to load SFO table"); + return false; + } + + // Since PBP files can store things that aren't PS1 CD images, make sure we're loading the right kind + if (!IsValidEboot()) + { + Log_ErrorPrint("Couldn't validate EBOOT"); + return false; + } + + // Start parsing ISO stuff + if (fseek(m_file, m_pbp_header.data_psar_offset, SEEK_SET) != 0) + return false; + + // Check "PSTITLEIMG000000" for multi-disc + char data_psar_magic[16] = {}; + if (fread(data_psar_magic, sizeof(data_psar_magic), 1, m_file) != 1) + return false; + + u32 iso_header_start = m_pbp_header.data_psar_offset; + if (strncmp(data_psar_magic, "PSTITLEIMG000000", 16) == 0) // Multi-disc header found + { + // For multi-disc, the five disc offsets are located at data_psar_offset + 0x200. Non-present discs have an offset + // of 0. There are also some disc hashes, a serial (from one of the discs, but used as an identifier for the entire + // "title image" header), and some other offsets, but we don't really need to check those + + if (fseek(m_file, m_pbp_header.data_psar_offset + 0x200, SEEK_SET) != 0) + return false; + + u32 disc_table[DISC_TABLE_NUM_ENTRIES] = {}; + if (fread(disc_table, sizeof(u32), DISC_TABLE_NUM_ENTRIES, m_file) != DISC_TABLE_NUM_ENTRIES) + return false; + + // Ignore encrypted files + if (disc_table[0] == 0x44475000) // "\0PGD" + return false; + + // Convert relative offsets to absolute offsets for available discs + for (u32 i = 0; i < DISC_TABLE_NUM_ENTRIES; i++) + { + if (disc_table[i] != 0) + m_multidisc_file_offsets.push_back(m_pbp_header.data_psar_offset + disc_table[i]); + else + break; + } + + if (m_multidisc_file_offsets.size() < 2) + { + Log_ErrorPrintf("Invalid number of discs (%u) in multi-disc PBP file", + static_cast(m_multidisc_file_offsets.size())); + return false; + } + + // Default to first disc for now, we can change this later + iso_header_start = m_multidisc_file_offsets[0]; + m_current_disc = 0; + } + + // Go to ISO header + if (fseek(m_file, iso_header_start, SEEK_SET) != 0) + return false; + + char iso_header_magic[12] = {}; + if (fread(iso_header_magic, sizeof(iso_header_magic), 1, m_file) != 1) + return false; + + if (strncmp(iso_header_magic, "PSISOIMG0000", 12) != 0) + { + Log_ErrorPrint("ISO header magic number mismatch"); + return false; + } + + // Ignore encrypted files + u32 pgd_magic; + if (fseek(m_file, iso_header_start + 0x400, SEEK_SET) != 0) + return false; + + if (fread(&pgd_magic, sizeof(pgd_magic), 1, m_file) != 1) + return false; + + if (pgd_magic == 0x44475000) // "\0PGD" + return false; + + // Read in the TOC + if (fseek(m_file, iso_header_start + 0x800, SEEK_SET) != 0) + return false; + + for (u32 i = 0; i < TOC_NUM_ENTRIES; i++) + { + if (fread(&m_toc[i], sizeof(m_toc[i]), 1, m_file) != 1) + return false; + } + + // For homebrew EBOOTs, audio track table doesn't exist -- the data track block table will point to compressed blocks + // for both data and audio + + // Get the offset of the compressed iso + if (fseek(m_file, iso_header_start + 0xBFC, SEEK_SET) != 0) + return false; + + u32 iso_offset; + if (fread(&iso_offset, sizeof(iso_offset), 1, m_file) != 1) + return false; + + // Generate block info table + if (fseek(m_file, iso_header_start + 0x4000, SEEK_SET) != 0) + return false; + + for (u32 i = 0; i < BLOCK_TABLE_NUM_ENTRIES; i++) + { + BlockTableEntry bte; + if (fread(&bte, sizeof(bte), 1, m_file) != 1) + return false; + + // Only store absolute file offset into a BlockInfo if this is a valid block + m_blockinfo_table[i] = {(bte.size != 0) ? (iso_header_start + iso_offset + bte.offset) : 0, bte.size}; + + // printf("Block %u, file offset %u, size %u\n", i, m_blockinfo_table[i].offset, m_blockinfo_table[i].size); + } + + // iso_header_start + 0x12D4, 0x12D6, 0x12D8 supposedly contain data on block size, num clusters, and num blocks + // Might be useful for error checking, but probably not that important as of now + + // Ignore track types for first three TOC entries, these don't seem to be consistent, but check that the points are + // valid. Not sure what m_toc[0].userdata_start.s encodes on homebrew EBOOTs though, so ignore that + if (m_toc[0].point != 0xA0 || m_toc[1].point != 0xA1 || m_toc[2].point != 0xA2) + { + Log_ErrorPrint("Invalid points on information tracks"); + return false; + } + + const u8 first_track = PackedBCDToBinary(m_toc[0].userdata_start.m); + const u8 last_track = PackedBCDToBinary(m_toc[1].userdata_start.m); + const LBA sectors_on_file = + Position::FromBCD(m_toc[2].userdata_start.m, m_toc[2].userdata_start.s, m_toc[2].userdata_start.f).ToLBA(); + + if (first_track != 1 || last_track < first_track) + { + Log_ErrorPrint("Invalid starting track number or track count"); + return false; + } + + // We assume that the pregap for the data track (track 1) is not on file, but pregaps for any additional tracks are on + // file. Also, homebrew tools seem to create 2 second pregaps for audio tracks, even when the audio track has a pregap + // that isn't 2 seconds long. We don't have a good way to validate this, and have to assume the TOC is giving us + // correct pregap lengths... + + m_lba_count = sectors_on_file; + LBA track1_pregap_frames = 0; + for (u32 curr_track = 1; curr_track <= last_track; curr_track++) + { + // Load in all the user stuff to m_tracks and m_indices + const TOCEntry& t = m_toc[static_cast(curr_track) + 2]; + const u8 track_num = PackedBCDToBinary(t.point); + if (track_num != curr_track) + { + Log_ErrorPrintf("Mismatched TOC track number, expected %u but got %u", static_cast(curr_track), track_num); + return false; + } + + const bool is_audio_track = t.type == 0x01; + const bool is_first_track = curr_track == 1; + const bool is_last_track = curr_track == last_track; + const TrackMode track_mode = is_audio_track ? TrackMode::Audio : TrackMode::Mode2Raw; + const u32 track_sector_size = GetBytesPerSector(track_mode); + + SubChannelQ::Control track_control = {}; + track_control.data = !is_audio_track; + + const LBA pregap_start = Position::FromBCD(t.pregap_start.m, t.pregap_start.s, t.pregap_start.f).ToLBA(); + const LBA userdata_start = Position::FromBCD(t.userdata_start.m, t.userdata_start.s, t.userdata_start.f).ToLBA(); + + if (userdata_start < pregap_start) + { + Log_ErrorPrintf("Invalid TOC entry at index %u, user data should not start before pregap", + static_cast(curr_track)); + return false; + } + + const LBA pregap_frames = userdata_start - pregap_start; + if (is_first_track) + { + m_lba_count += pregap_frames; + track1_pregap_frames = pregap_frames; + } + + Index pregap_index = {}; + pregap_index.file_offset = + is_first_track ? 0 : (static_cast(pregap_start - track1_pregap_frames) * track_sector_size); + pregap_index.file_index = 0; + pregap_index.file_sector_size = track_sector_size; + pregap_index.start_lba_on_disc = pregap_start; + pregap_index.track_number = curr_track; + pregap_index.index_number = 0; + pregap_index.start_lba_in_track = static_cast(-static_cast(pregap_frames)); + pregap_index.length = pregap_frames; + pregap_index.mode = track_mode; + pregap_index.control.bits = track_control.bits; + pregap_index.is_pregap = true; + + m_indices.push_back(pregap_index); + + Index userdata_index = {}; + userdata_index.file_offset = static_cast(userdata_start - track1_pregap_frames) * track_sector_size; + userdata_index.file_index = 0; + userdata_index.file_sector_size = track_sector_size; + userdata_index.start_lba_on_disc = userdata_start; + userdata_index.track_number = curr_track; + userdata_index.index_number = 1; + userdata_index.start_lba_in_track = 0; + userdata_index.mode = track_mode; + userdata_index.control.bits = track_control.bits; + userdata_index.is_pregap = false; + + if (is_last_track) + { + if (userdata_start >= m_lba_count) + { + Log_ErrorPrintf("Last user data index on disc for TOC entry %u should not be 0 or less in length", + static_cast(curr_track)); + return false; + } + userdata_index.length = m_lba_count - userdata_start; + } + else + { + const TOCEntry& next_track = m_toc[static_cast(curr_track) + 3]; + const LBA next_track_start = + Position::FromBCD(next_track.pregap_start.m, next_track.pregap_start.s, next_track.pregap_start.f).ToLBA(); + const u8 next_track_num = PackedBCDToBinary(next_track.point); + + if (next_track_num != curr_track + 1 || next_track_start < userdata_start) + { + Log_ErrorPrintf("Unable to calculate user data index length for TOC entry %u", static_cast(curr_track)); + return false; + } + + userdata_index.length = next_track_start - userdata_start; + } + + m_indices.push_back(userdata_index); + + m_tracks.push_back(Track{curr_track, userdata_start, 2 * curr_track - 1, + pregap_index.length + userdata_index.length, track_mode, track_control}); + } + + AddLeadOutIndex(); + + // Initialize zlib stream + if (!InitDecompressionStream()) + { + Log_ErrorPrint("Failed to initialize zlib decompression stream"); + return false; + } + + if (m_multidisc_file_offsets.size() > 0) + { + std::string sbi_path = + FileSystem::StripExtension(filename) + StringUtil::StdStringFromFormat("_%u.sbi", m_current_disc + 1); + m_sbi.LoadSBI(sbi_path.c_str()); + } + else + m_sbi.LoadSBI(FileSystem::ReplaceExtension(filename, "sbi").c_str()); + + return Seek(1, Position{0, 0, 0}); +} + +bool CDImagePBP::InitDecompressionStream() +{ + m_inflate_stream = {}; + m_inflate_stream.next_in = Z_NULL; + m_inflate_stream.avail_in = 0; + m_inflate_stream.zalloc = Z_NULL; + m_inflate_stream.zfree = Z_NULL; + m_inflate_stream.opaque = Z_NULL; + + int ret = inflateInit2(&m_inflate_stream, -MAX_WBITS); + return ret == Z_OK; +} + +bool CDImagePBP::DecompressBlock(BlockInfo block_info) +{ + if (fseek(m_file, block_info.offset, SEEK_SET) != 0) + return false; + + m_compressed_block.resize(block_info.size); + + if (fread(m_compressed_block.data(), sizeof(u8), m_compressed_block.size(), m_file) != m_compressed_block.size()) + return false; + + m_inflate_stream.next_in = m_compressed_block.data(); + m_inflate_stream.avail_in = static_cast(m_compressed_block.size()); + m_inflate_stream.next_out = m_decompressed_block.data(); + m_inflate_stream.avail_out = static_cast(m_decompressed_block.size()); + + if (inflateReset(&m_inflate_stream) != Z_OK) + return false; + + int err = inflate(&m_inflate_stream, Z_FINISH); + if (err != Z_STREAM_END) + { + Log_ErrorPrintf("Inflate error %d", err); + return false; + } + + return true; +} + +bool CDImagePBP::ReadSubChannelQ(SubChannelQ* subq) +{ + if (m_sbi.GetReplacementSubChannelQ(m_position_on_disc, subq)) + return true; + + return CDImage::ReadSubChannelQ(subq); +} + +bool CDImagePBP::HasNonStandardSubchannel() const +{ + return (m_sbi.GetReplacementSectorCount() > 0); +} + +bool CDImagePBP::ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) +{ + const u32 offset_in_file = static_cast(index.file_offset) + (lba_in_index * index.file_sector_size); + const u32 offset_in_block = offset_in_file % DECOMPRESSED_BLOCK_SIZE; + const u32 requested_block = offset_in_file / DECOMPRESSED_BLOCK_SIZE; + + BlockInfo& bi = m_blockinfo_table[requested_block]; + + if (bi.size == 0) + { + Log_ErrorPrintf("Invalid block %u requested", requested_block); + return false; + } + + if (m_current_block != requested_block && !DecompressBlock(bi)) + { + Log_ErrorPrintf("Failed to decompress block %u", requested_block); + return false; + } + + std::memcpy(buffer, &m_decompressed_block[offset_in_block], RAW_SECTOR_SIZE); + return true; +} + +#if _DEBUG +void CDImagePBP::PrintPBPHeaderInfo(const PBPHeader& pbp_header) +{ + printf("PBP header info\n"); + printf("PBP format version 0x%08X\n", pbp_header.version); + printf("File offsets\n"); + printf("PARAM.SFO 0x%08X PARSE\n", pbp_header.param_sfo_offset); + printf("ICON0.PNG 0x%08X IGNORE\n", pbp_header.icon0_png_offset); + printf("ICON1.PNG 0x%08X IGNORE\n", pbp_header.icon1_png_offset); + printf("PIC0.PNG 0x%08X IGNORE\n", pbp_header.pic0_png_offset); + printf("PIC1.PNG 0x%08X IGNORE\n", pbp_header.pic1_png_offset); + printf("SND0.AT3 0x%08X IGNORE\n", pbp_header.snd0_at3_offset); + printf("DATA.PSP 0x%08X IGNORE\n", pbp_header.data_psp_offset); + printf("DATA.PSAR 0x%08X PARSE\n", pbp_header.data_psar_offset); + printf("\n"); +} + +void CDImagePBP::PrintSFOHeaderInfo(const SFOHeader& sfo_header) +{ + printf("SFO header info\n"); + printf("SFO format version 0x%08X\n", sfo_header.version); + printf("SFO key table offset 0x%08X\n", sfo_header.key_table_offset); + printf("SFO data table offset 0x%08X\n", sfo_header.data_table_offset); + printf("SFO table entry count 0x%08X\n", sfo_header.num_table_entries); + printf("\n"); +} + +void CDImagePBP::PrintSFOIndexTableEntry(const SFOIndexTableEntry& sfo_index_table_entry, size_t i) +{ + printf("SFO index table entry %zu\n", i); + printf("Key offset 0x%08X\n", sfo_index_table_entry.key_offset); + printf("Data type 0x%08X\n", sfo_index_table_entry.data_type); + printf("Data size 0x%08X\n", sfo_index_table_entry.data_size); + printf("Total data size 0x%08X\n", sfo_index_table_entry.data_total_size); + printf("Data offset 0x%08X\n", sfo_index_table_entry.data_offset); + printf("\n"); +} + +void CDImagePBP::PrintSFOTable(const SFOTable& sfo_table) +{ + for (auto it = sfo_table.begin(); it != sfo_table.end(); ++it) + { + std::string key_value = it->first; + SFOTableDataValue data_value = it->second; + + if (std::holds_alternative(data_value)) + printf("Key: %s, Data: %s\n", key_value.c_str(), std::get(data_value).c_str()); + else if (std::holds_alternative(data_value)) + printf("Key: %s, Data: %u\n", key_value.c_str(), std::get(data_value)); + } +} +#endif + +std::unique_ptr CDImage::OpenPBPImage(const char* filename) +{ + std::unique_ptr image = std::make_unique(); + if (!image->Open(filename)) + return {}; + + return image; +} diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj index da63992a4..8ca686f01 100644 --- a/src/common/common.vcxproj +++ b/src/common/common.vcxproj @@ -92,6 +92,7 @@ + @@ -134,6 +135,7 @@ + diff --git a/src/common/common.vcxproj.filters b/src/common/common.vcxproj.filters index 124f84eb0..5d941ccc6 100644 --- a/src/common/common.vcxproj.filters +++ b/src/common/common.vcxproj.filters @@ -110,6 +110,7 @@ + @@ -212,6 +213,7 @@ + diff --git a/src/common/file_system.cpp b/src/common/file_system.cpp index cd5216f55..ada31b5fe 100644 --- a/src/common/file_system.cpp +++ b/src/common/file_system.cpp @@ -263,6 +263,15 @@ bool IsAbsolutePath(const std::string_view& path) #endif } +std::string StripExtension(const std::string_view& path) +{ + std::string_view::size_type pos = path.rfind('.'); + if (pos == std::string::npos) + return std::string(path); + + return std::string(path, 0, pos); +} + std::string ReplaceExtension(const std::string_view& path, const std::string_view& new_extension) { std::string_view::size_type pos = path.rfind('.'); diff --git a/src/common/file_system.h b/src/common/file_system.h index e3622b6c7..a1843d5a2 100644 --- a/src/common/file_system.h +++ b/src/common/file_system.h @@ -143,6 +143,9 @@ void SanitizeFileName(std::string& Destination, bool StripSlashes = true); /// Returns true if the specified path is an absolute path (C:\Path on Windows or /path on Unix). bool IsAbsolutePath(const std::string_view& path); +/// Removes the extension of a filename. +std::string StripExtension(const std::string_view& path); + /// Replaces the extension of a filename with another. std::string ReplaceExtension(const std::string_view& path, const std::string_view& new_extension); diff --git a/src/common/pbp_types.h b/src/common/pbp_types.h new file mode 100644 index 000000000..7ab01e039 --- /dev/null +++ b/src/common/pbp_types.h @@ -0,0 +1,110 @@ +#pragma once +#include "types.h" +#include +#include +#include +#include + +namespace PBP { + +enum : u32 +{ + PBP_HEADER_OFFSET_COUNT = 8u, + TOC_NUM_ENTRIES = 102u, + BLOCK_TABLE_NUM_ENTRIES = 32256u, + DISC_TABLE_NUM_ENTRIES = 5u, + DECOMPRESSED_BLOCK_SIZE = 37632u // 2352 bytes per sector * 16 sectors per block +}; + +#pragma pack(push, 1) + +struct PBPHeader +{ + u8 magic[4]; // "\0PBP" + u32 version; + + union + { + u32 offsets[PBP_HEADER_OFFSET_COUNT]; + + struct + { + u32 param_sfo_offset; // 0x00000028 + u32 icon0_png_offset; + u32 icon1_png_offset; + u32 pic0_png_offset; + u32 pic1_png_offset; + u32 snd0_at3_offset; + u32 data_psp_offset; + u32 data_psar_offset; + }; + }; +}; +static_assert(sizeof(PBPHeader) == 0x28); + +struct SFOHeader +{ + u8 magic[4]; // "\0PSF" + u32 version; + u32 key_table_offset; // Relative to start of SFOHeader, 0x000000A4 expected + u32 data_table_offset; // Relative to start of SFOHeader, 0x00000100 expected + u32 num_table_entries; // 0x00000009 +}; +static_assert(sizeof(SFOHeader) == 0x14); + +struct SFOIndexTableEntry +{ + u16 key_offset; // Relative to key_table_offset + u16 data_type; + u32 data_size; // Size of actual data in bytes + u32 data_total_size; // Size of data field in bytes, data_total_size >= data_size + u32 data_offset; // Relative to data_table_offset +}; +static_assert(sizeof(SFOIndexTableEntry) == 0x10); + +using SFOIndexTable = std::vector; +using SFOTableDataValue = std::variant; +using SFOTable = std::map; + +struct BlockTableEntry +{ + u32 offset; + u16 size; + u16 marker; + u8 checksum[0x10]; + u64 padding; +}; +static_assert(sizeof(BlockTableEntry) == 0x20); + +struct TOCEntry +{ + struct Timecode + { + u8 m; + u8 s; + u8 f; + }; + + u8 type; + u8 unknown; + u8 point; + Timecode pregap_start; + u8 zero; + Timecode userdata_start; +}; +static_assert(sizeof(TOCEntry) == 0x0A); + +#if 0 +struct AudioTrackTableEntry +{ + u32 block_offset; + u32 block_size; + u32 block_padding; + u32 block_checksum; +}; +static_assert(sizeof(CDDATrackTableEntry) == 0x10); +#endif + +#pragma pack(pop) + +} // namespace PBP diff --git a/src/core/system.cpp b/src/core/system.cpp index f19405be1..70619811e 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -299,8 +299,8 @@ bool IsLoadableFilename(const char* path) static constexpr auto extensions = make_array(".bin", ".cue", ".img", ".iso", ".chd", ".ecm", ".mds", // discs ".exe", ".psexe", // exes ".psf", ".minipsf", // psf - ".m3u" // playlists - ); + ".m3u", // playlists + ".pbp"); const char* extension = std::strrchr(path, '.'); if (!extension) return false; diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index c6927c654..effb9149f 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -37,7 +37,7 @@ static constexpr char DISC_IMAGE_FILTER[] = QT_TRANSLATE_NOOP( "All File Types (*.bin *.img *.iso *.cue *.chd *.ecm *.mds *.exe *.psexe *.psf *.minipsf *.m3u);;Single-Track Raw " "Images (*.bin *.img *.iso);;Cue Sheets (*.cue);;MAME CHD Images (*.chd);;Error Code Modeler Images (*.ecm);;Media " "Descriptor Sidecar Images (*.mds);;PlayStation Executables (*.exe *.psexe);;Portable Sound Format Files (*.psf " - "*.minipsf);;Playlists (*.m3u)"); + "*.minipsf);;Playlists (*.m3u);;PlayStation EBOOTs (*.pbp)"); ALWAYS_INLINE static QString getWindowTitle() { diff --git a/src/frontend-common/fullscreen_ui.cpp b/src/frontend-common/fullscreen_ui.cpp index f90575c64..d0de33f90 100644 --- a/src/frontend-common/fullscreen_ui.cpp +++ b/src/frontend-common/fullscreen_ui.cpp @@ -515,8 +515,8 @@ bool InvalidateCachedTexture(const std::string& path) static ImGuiFullscreen::FileSelectorFilters GetDiscImageFilters() { - return {"*.bin", "*.cue", "*.iso", "*.img", "*.chd", "*.ecm", - "*.mds", "*.psexe", "*.exe", "*.psf", "*.minipsf", "*.m3u"}; + return {"*.bin", "*.cue", "*.iso", "*.img", "*.chd", "*.ecm", "*.mds", + "*.psexe", "*.exe", "*.psf", "*.minipsf", "*.m3u", "*.pbp"}; } static void DoStartPath(const std::string& path, bool allow_resume)