From dc480ac00bc6367f9272c490fbf2a7e4cacee218 Mon Sep 17 00:00:00 2001 From: goeiecool9999 <7033575+goeiecool9999@users.noreply.github.com> Date: Sun, 5 May 2024 02:35:01 +0200 Subject: [PATCH] Add support for WUHB file format (#1190) --- src/Cafe/CMakeLists.txt | 4 + src/Cafe/Filesystem/WUHB/RomFSStructs.h | 40 ++++ src/Cafe/Filesystem/WUHB/WUHBReader.cpp | 224 ++++++++++++++++++++++ src/Cafe/Filesystem/WUHB/WUHBReader.h | 45 +++++ src/Cafe/Filesystem/fsc.h | 3 + src/Cafe/Filesystem/fscDeviceWuhb.cpp | 151 +++++++++++++++ src/Cafe/TitleList/TitleInfo.cpp | 82 ++++++++ src/Cafe/TitleList/TitleInfo.h | 3 + src/Cafe/TitleList/TitleList.cpp | 3 +- src/gui/MainWindow.cpp | 6 +- src/gui/components/wxGameList.cpp | 10 + src/gui/components/wxTitleManagerList.cpp | 5 + src/gui/components/wxTitleManagerList.h | 1 + src/util/helpers/helpers.cpp | 41 ++++ src/util/helpers/helpers.h | 2 + 15 files changed, 617 insertions(+), 3 deletions(-) create mode 100644 src/Cafe/Filesystem/WUHB/RomFSStructs.h create mode 100644 src/Cafe/Filesystem/WUHB/WUHBReader.cpp create mode 100644 src/Cafe/Filesystem/WUHB/WUHBReader.h create mode 100644 src/Cafe/Filesystem/fscDeviceWuhb.cpp diff --git a/src/Cafe/CMakeLists.txt b/src/Cafe/CMakeLists.txt index d64a5998..851854fc 100644 --- a/src/Cafe/CMakeLists.txt +++ b/src/Cafe/CMakeLists.txt @@ -10,6 +10,7 @@ add_library(CemuCafe Filesystem/fscDeviceRedirect.cpp Filesystem/fscDeviceWua.cpp Filesystem/fscDeviceWud.cpp + Filesystem/fscDeviceWuhb.cpp Filesystem/fsc.h Filesystem/FST/FST.cpp Filesystem/FST/FST.h @@ -18,6 +19,9 @@ add_library(CemuCafe Filesystem/FST/KeyCache.h Filesystem/WUD/wud.cpp Filesystem/WUD/wud.h + Filesystem/WUHB/RomFSStructs.h + Filesystem/WUHB/WUHBReader.cpp + Filesystem/WUHB/WUHBReader.h GamePatch.cpp GamePatch.h GameProfile/GameProfile.cpp diff --git a/src/Cafe/Filesystem/WUHB/RomFSStructs.h b/src/Cafe/Filesystem/WUHB/RomFSStructs.h new file mode 100644 index 00000000..59ef503f --- /dev/null +++ b/src/Cafe/Filesystem/WUHB/RomFSStructs.h @@ -0,0 +1,40 @@ +#pragma once + +struct romfs_header_t +{ + uint32 header_magic; + uint32be header_size; + uint64be dir_hash_table_ofs; + uint64be dir_hash_table_size; + uint64be dir_table_ofs; + uint64be dir_table_size; + uint64be file_hash_table_ofs; + uint64be file_hash_table_size; + uint64be file_table_ofs; + uint64be file_table_size; + uint64be file_partition_ofs; +}; + +struct romfs_direntry_t +{ + uint32be parent; + uint32be listNext; // offset to next directory entry in linked list of parent directory (aka "sibling") + uint32be dirListHead; // offset to first entry in linked list of directory entries (aka "child") + uint32be fileListHead; // offset to first entry in linked list of file entries (aka "file") + uint32be hash; + uint32be name_size; + std::string name; +}; + +struct romfs_fentry_t +{ + uint32be parent; + uint32be listNext; // offset to next file entry in linked list of parent directory (aka "sibling") + uint64be offset; + uint64be size; + uint32be hash; + uint32be name_size; + std::string name; +}; + +#define ROMFS_ENTRY_EMPTY 0xFFFFFFFF diff --git a/src/Cafe/Filesystem/WUHB/WUHBReader.cpp b/src/Cafe/Filesystem/WUHB/WUHBReader.cpp new file mode 100644 index 00000000..e7a4c9be --- /dev/null +++ b/src/Cafe/Filesystem/WUHB/WUHBReader.cpp @@ -0,0 +1,224 @@ +#include "WUHBReader.h" +WUHBReader* WUHBReader::FromPath(const fs::path& path) +{ + FileStream* fileIn{FileStream::openFile2(path)}; + if (!fileIn) + return nullptr; + + WUHBReader* ret = new WUHBReader(fileIn); + if (!ret->CheckMagicValue()) + { + delete ret; + return nullptr; + } + + if (!ret->ReadHeader()) + { + delete ret; + return nullptr; + } + + return ret; +} + +static const romfs_direntry_t fallbackDirEntry{ + .parent = ROMFS_ENTRY_EMPTY, + .listNext = ROMFS_ENTRY_EMPTY, + .dirListHead = ROMFS_ENTRY_EMPTY, + .fileListHead = ROMFS_ENTRY_EMPTY, + .hash = ROMFS_ENTRY_EMPTY, + .name_size = 0, + .name = "" +}; +static const romfs_fentry_t fallbackFileEntry{ + .parent = ROMFS_ENTRY_EMPTY, + .listNext = ROMFS_ENTRY_EMPTY, + .offset = 0, + .size = 0, + .hash = ROMFS_ENTRY_EMPTY, + .name_size = 0, + .name = "" +}; +template +const WUHBReader::EntryType& WUHBReader::GetFallback() +{ + if constexpr (File) + return fallbackFileEntry; + else + return fallbackDirEntry; +} + +template +WUHBReader::EntryType WUHBReader::GetEntry(uint32 offset) const +{ + auto fallback = GetFallback(); + if(offset == ROMFS_ENTRY_EMPTY) + return fallback; + + const char* typeName = File ? "fentry" : "direntry"; + EntryType ret; + if (offset >= (File ? m_header.file_table_size : m_header.dir_table_size)) + { + cemuLog_log(LogType::Force, "WUHB {} offset exceeds table size declared in header", typeName); + return fallback; + } + + // read the entry + m_fileIn->SetPosition((File ? m_header.file_table_ofs : m_header.dir_table_ofs) + offset); + auto read = m_fileIn->readData(&ret, offsetof(EntryType, name)); + if (read != offsetof(EntryType, name)) + { + cemuLog_log(LogType::Force, "failed to read WUHB {} at offset: {}", typeName, offset); + return fallback; + } + + // read the name + ret.name.resize(ret.name_size); + read = m_fileIn->readData(ret.name.data(), ret.name_size); + if (read != ret.name_size) + { + cemuLog_log(LogType::Force, "failed to read WUHB {} name", typeName); + return fallback; + } + + return ret; +} + +romfs_direntry_t WUHBReader::GetDirEntry(uint32 offset) const +{ + return GetEntry(offset); +} +romfs_fentry_t WUHBReader::GetFileEntry(uint32 offset) const +{ + return GetEntry(offset); +} + +uint64 WUHBReader::GetFileSize(uint32 entryOffset) const +{ + return GetFileEntry(entryOffset).size; +} + +uint64 WUHBReader::ReadFromFile(uint32 entryOffset, uint64 fileOffset, uint64 length, void* buffer) const +{ + const auto fileEntry = GetFileEntry(entryOffset); + if (fileOffset >= fileEntry.size) + return 0; + const uint64 readAmount = std::min(length, fileEntry.size - fileOffset); + const uint64 wuhbOffset = m_header.file_partition_ofs + fileEntry.offset + fileOffset; + m_fileIn->SetPosition(wuhbOffset); + return m_fileIn->readData(buffer, readAmount); +} + +uint32 WUHBReader::GetHashTableEntryOffset(uint32 hash, bool isFile) const +{ + const uint64 hash_table_size = (isFile ? m_header.file_hash_table_size : m_header.dir_hash_table_size); + const uint64 hash_table_ofs = (isFile ? m_header.file_hash_table_ofs : m_header.dir_hash_table_ofs); + + const uint64 hash_table_entry_count = hash_table_size / sizeof(uint32); + const uint64 hash_table_entry_offset = hash_table_ofs + (hash % hash_table_entry_count) * sizeof(uint32); + + m_fileIn->SetPosition(hash_table_entry_offset); + uint32 tableOffset; + if (!m_fileIn->readU32(tableOffset)) + { + cemuLog_log(LogType::Force, "failed to read WUHB hash table entry at file offset: {}", hash_table_entry_offset); + return ROMFS_ENTRY_EMPTY; + } + + return uint32be::from_bevalue(tableOffset); +} + +template +bool WUHBReader::SearchHashList(uint32& entryOffset, const fs::path& targetName) const +{ + for (;;) + { + if (entryOffset == ROMFS_ENTRY_EMPTY) + return false; + auto entry = GetEntry(entryOffset); + + if (entry.name == targetName) + return true; + entryOffset = entry.hash; + } + return false; +} + +uint32 WUHBReader::Lookup(const std::filesystem::path& path, bool isFile) const +{ + uint32 currentEntryOffset = 0; + auto look = [&](const fs::path& part, bool lookInFileHT) { + const auto partString = part.string(); + currentEntryOffset = GetHashTableEntryOffset(CalcPathHash(currentEntryOffset, partString.c_str(), 0, partString.size()), lookInFileHT); + if (lookInFileHT) + return SearchHashList(currentEntryOffset, part); + else + return SearchHashList(currentEntryOffset, part); + }; + // look for the root entry + if (!look("", false)) + return ROMFS_ENTRY_EMPTY; + + auto it = path.begin(); + while (it != path.end()) + { + fs::path part = *it; + ++it; + // no need to recurse after trailing forward slash (e.g. directory/) + if (part.empty() && !isFile) + break; + // skip leading forward slash + if (part == "/") + continue; + + // if the lookup target is a file and this is the last iteration, look in the file hash table instead. + if (!look(part, it == path.end() && isFile)) + return ROMFS_ENTRY_EMPTY; + } + return currentEntryOffset; +} +bool WUHBReader::CheckMagicValue() const +{ + uint8 magic[4]; + m_fileIn->SetPosition(0); + int read = m_fileIn->readData(magic, 4); + if (read != 4) + { + cemuLog_log(LogType::Force, "Failed to read WUHB magic numbers"); + return false; + } + static_assert(sizeof(magic) == s_headerMagicValue.size()); + return std::memcmp(&magic, s_headerMagicValue.data(), sizeof(magic)) == 0; +} +bool WUHBReader::ReadHeader() +{ + m_fileIn->SetPosition(0); + auto read = m_fileIn->readData(&m_header, sizeof(m_header)); + auto readSuccess = read == sizeof(m_header); + if (!readSuccess) + cemuLog_log(LogType::Force, "Failed to read WUHB header"); + return readSuccess; +} +unsigned char WUHBReader::NormalizeChar(unsigned char c) +{ + if (c >= 'a' && c <= 'z') + { + return c + 'A' - 'a'; + } + else + { + return c; + } +} +uint32 WUHBReader::CalcPathHash(uint32 parent, const char* path, uint32 start, size_t path_len) +{ + cemu_assert(path != nullptr || path_len == 0); + uint32 hash = parent ^ 123456789; + for (uint32 i = 0; i < path_len; i++) + { + hash = (hash >> 5) | (hash << 27); + hash ^= NormalizeChar(path[start + i]); + } + + return hash; +} diff --git a/src/Cafe/Filesystem/WUHB/WUHBReader.h b/src/Cafe/Filesystem/WUHB/WUHBReader.h new file mode 100644 index 00000000..9187f05a --- /dev/null +++ b/src/Cafe/Filesystem/WUHB/WUHBReader.h @@ -0,0 +1,45 @@ +#pragma once +#include +#include "RomFSStructs.h" +class WUHBReader +{ + public: + static WUHBReader* FromPath(const fs::path& path); + + romfs_direntry_t GetDirEntry(uint32 offset) const; + romfs_fentry_t GetFileEntry(uint32 offset) const; + + uint64 GetFileSize(uint32 entryOffset) const; + + uint64 ReadFromFile(uint32 entryOffset, uint64 fileOffset, uint64 length, void* buffer) const; + + uint32 Lookup(const std::filesystem::path& path, bool isFile) const; + + private: + WUHBReader(FileStream* file) + : m_fileIn(file) + { + cemu_assert_debug(file != nullptr); + }; + WUHBReader() = delete; + + romfs_header_t m_header; + std::unique_ptr m_fileIn; + constexpr static std::string_view s_headerMagicValue = "WUHB"; + bool ReadHeader(); + bool CheckMagicValue() const; + + static inline unsigned char NormalizeChar(unsigned char c); + static uint32 CalcPathHash(uint32 parent, const char* path, uint32 start, size_t path_len); + + template + using EntryType = std::conditional_t; + template + static const EntryType& GetFallback(); + template + EntryType GetEntry(uint32 offset) const; + + template + bool SearchHashList(uint32& entryOffset, const fs::path& targetName) const; + uint32 GetHashTableEntryOffset(uint32 hash, bool isFile) const; +}; diff --git a/src/Cafe/Filesystem/fsc.h b/src/Cafe/Filesystem/fsc.h index 09c1f508..a3df2af2 100644 --- a/src/Cafe/Filesystem/fsc.h +++ b/src/Cafe/Filesystem/fsc.h @@ -204,6 +204,9 @@ bool FSCDeviceWUD_Mount(std::string_view mountPath, std::string_view destination // wua device bool FSCDeviceWUA_Mount(std::string_view mountPath, std::string_view destinationBaseDir, class ZArchiveReader* archive, sint32 priority); +// wuhb device +bool FSCDeviceWUHB_Mount(std::string_view mountPath, std::string_view destinationBaseDir, class WUHBReader* wuhbReader, sint32 priority); + // hostFS device bool FSCDeviceHostFS_Mount(std::string_view mountPath, std::string_view hostTargetPath, sint32 priority); diff --git a/src/Cafe/Filesystem/fscDeviceWuhb.cpp b/src/Cafe/Filesystem/fscDeviceWuhb.cpp new file mode 100644 index 00000000..5e8e6484 --- /dev/null +++ b/src/Cafe/Filesystem/fscDeviceWuhb.cpp @@ -0,0 +1,151 @@ +#include "Filesystem/WUHB/WUHBReader.h" +#include "Cafe/Filesystem/fsc.h" +#include "Cafe/Filesystem/FST/FST.h" + +class FSCDeviceWuhbFileCtx : public FSCVirtualFile +{ + public: + FSCDeviceWuhbFileCtx(WUHBReader* reader, uint32 entryOffset, uint32 fscType) + : m_wuhbReader(reader), m_entryOffset(entryOffset), m_fscType(fscType) + { + cemu_assert(entryOffset != ROMFS_ENTRY_EMPTY); + if (fscType == FSC_TYPE_DIRECTORY) + { + romfs_direntry_t entry = reader->GetDirEntry(entryOffset); + m_dirIterOffset = entry.dirListHead; + m_fileIterOffset = entry.fileListHead; + } + } + sint32 fscGetType() override + { + return m_fscType; + } + uint64 fscQueryValueU64(uint32 id) override + { + if (m_fscType == FSC_TYPE_FILE) + { + if (id == FSC_QUERY_SIZE) + return m_wuhbReader->GetFileSize(m_entryOffset); + else if (id == FSC_QUERY_WRITEABLE) + return 0; // WUHB images are read-only + else + cemu_assert_error(); + } + else + { + cemu_assert_unimplemented(); + } + return 0; + } + uint32 fscWriteData(void* buffer, uint32 size) override + { + cemu_assert_error(); + return 0; + } + uint32 fscReadData(void* buffer, uint32 size) override + { + if (m_fscType != FSC_TYPE_FILE) + return 0; + auto read = m_wuhbReader->ReadFromFile(m_entryOffset, m_seek, size, buffer); + m_seek += read; + return read; + } + void fscSetSeek(uint64 seek) override + { + m_seek = seek; + } + uint64 fscGetSeek() override + { + if (m_fscType != FSC_TYPE_FILE) + return 0; + return m_seek; + } + void fscSetFileLength(uint64 endOffset) override + { + cemu_assert_error(); + } + bool fscDirNext(FSCDirEntry* dirEntry) override + { + if (m_dirIterOffset != ROMFS_ENTRY_EMPTY) + { + romfs_direntry_t entry = m_wuhbReader->GetDirEntry(m_dirIterOffset); + m_dirIterOffset = entry.listNext; + if(entry.name_size > 0) + { + dirEntry->isDirectory = true; + dirEntry->isFile = false; + dirEntry->fileSize = 0; + std::strncpy(dirEntry->path, entry.name.c_str(), FSC_MAX_DIR_NAME_LENGTH); + return true; + } + } + if (m_fileIterOffset != ROMFS_ENTRY_EMPTY) + { + romfs_fentry_t entry = m_wuhbReader->GetFileEntry(m_fileIterOffset); + m_fileIterOffset = entry.listNext; + if(entry.name_size > 0) + { + dirEntry->isDirectory = false; + dirEntry->isFile = true; + dirEntry->fileSize = entry.size; + std::strncpy(dirEntry->path, entry.name.c_str(), FSC_MAX_DIR_NAME_LENGTH); + return true; + } + } + + return false; + } + + private: + WUHBReader* m_wuhbReader{}; + uint32 m_fscType; + uint32 m_entryOffset = ROMFS_ENTRY_EMPTY; + uint32 m_dirIterOffset = ROMFS_ENTRY_EMPTY; + uint32 m_fileIterOffset = ROMFS_ENTRY_EMPTY; + uint64 m_seek = 0; +}; + +class fscDeviceWUHB : public fscDeviceC +{ + FSCVirtualFile* fscDeviceOpenByPath(std::string_view path, FSC_ACCESS_FLAG accessFlags, void* ctx, sint32* fscStatus) override + { + WUHBReader* reader = (WUHBReader*)ctx; + cemu_assert_debug(!HAS_FLAG(accessFlags, FSC_ACCESS_FLAG::WRITE_PERMISSION)); // writing to WUHB is not supported + + bool isFile; + uint32 table_offset = ROMFS_ENTRY_EMPTY; + + if (table_offset == ROMFS_ENTRY_EMPTY && HAS_FLAG(accessFlags, FSC_ACCESS_FLAG::OPEN_DIR)) + { + table_offset = reader->Lookup(path, false); + isFile = false; + } + if (table_offset == ROMFS_ENTRY_EMPTY && HAS_FLAG(accessFlags, FSC_ACCESS_FLAG::OPEN_FILE)) + { + table_offset = reader->Lookup(path, true); + isFile = true; + } + + if (table_offset == ROMFS_ENTRY_EMPTY) + { + *fscStatus = FSC_STATUS_FILE_NOT_FOUND; + return nullptr; + } + + *fscStatus = FSC_STATUS_OK; + return new FSCDeviceWuhbFileCtx(reader, table_offset, isFile ? FSC_TYPE_FILE : FSC_TYPE_DIRECTORY); + } + + // singleton + public: + static fscDeviceWUHB& instance() + { + static fscDeviceWUHB _instance; + return _instance; + } +}; + +bool FSCDeviceWUHB_Mount(std::string_view mountPath, std::string_view destinationBaseDir, WUHBReader* wuhbReader, sint32 priority) +{ + return fsc_mount(mountPath, destinationBaseDir, &fscDeviceWUHB::instance(), wuhbReader, priority) == FSC_STATUS_OK; +} diff --git a/src/Cafe/TitleList/TitleInfo.cpp b/src/Cafe/TitleList/TitleInfo.cpp index 2f295811..12131058 100644 --- a/src/Cafe/TitleList/TitleInfo.cpp +++ b/src/Cafe/TitleList/TitleInfo.cpp @@ -1,9 +1,12 @@ #include "TitleInfo.h" #include "Cafe/Filesystem/fscDeviceHostFS.h" +#include "Cafe/Filesystem/WUHB/WUHBReader.h" #include "Cafe/Filesystem/FST/FST.h" #include "pugixml.hpp" #include "Common/FileStream.h" #include +#include "util/IniParser/IniParser.h" +#include "util/crypto/crc32.h" #include "config/ActiveSettings.h" #include "util/helpers/helpers.h" @@ -97,6 +100,7 @@ TitleInfo::TitleInfo(const TitleInfo::CachedInfo& cachedInfo) m_isValid = false; if (cachedInfo.titleDataFormat != TitleDataFormat::HOST_FS && cachedInfo.titleDataFormat != TitleDataFormat::WIIU_ARCHIVE && + cachedInfo.titleDataFormat != TitleDataFormat::WUHB && cachedInfo.titleDataFormat != TitleDataFormat::WUD && cachedInfo.titleDataFormat != TitleDataFormat::NUS && cachedInfo.titleDataFormat != TitleDataFormat::INVALID_STRUCTURE) @@ -245,6 +249,16 @@ bool TitleInfo::DetectFormat(const fs::path& path, fs::path& pathOut, TitleDataF delete zar; return foundBase; } + else if (boost::iends_with(filenameStr, ".wuhb")) + { + std::unique_ptr reader{WUHBReader::FromPath(path)}; + if(reader) + { + formatOut = TitleDataFormat::WUHB; + pathOut = path; + return true; + } + } // note: Since a Wii U archive file (.wua) contains multiple titles we shouldn't auto-detect them here // instead TitleInfo has a second constructor which takes a subpath // unable to determine type by extension, check contents @@ -436,6 +450,23 @@ bool TitleInfo::Mount(std::string_view virtualPath, std::string_view subfolder, return false; } } + else if (m_titleFormat == TitleDataFormat::WUHB) + { + if (!m_wuhbreader) + { + m_wuhbreader = WUHBReader::FromPath(m_fullPath); + if (!m_wuhbreader) + return false; + } + bool r = FSCDeviceWUHB_Mount(virtualPath, subfolder, m_wuhbreader, mountPriority); + if (!r) + { + cemuLog_log(LogType::Force, "Failed to mount {} to {}", virtualPath, subfolder); + delete m_wuhbreader; + m_wuhbreader = nullptr; + return false; + } + } else { cemu_assert_unimplemented(); @@ -467,6 +498,12 @@ void TitleInfo::Unmount(std::string_view virtualPath) if (m_mountpoints.empty()) m_zarchive = nullptr; } + if (m_wuhbreader) + { + cemu_assert_debug(m_titleFormat == TitleDataFormat::WUHB); + delete m_wuhbreader; + m_wuhbreader = nullptr; + } } return; } @@ -502,6 +539,20 @@ bool TitleInfo::ParseXmlInfo() auto xmlData = fsc_extractFile(fmt::format("{}meta/meta.xml", mountPath).c_str()); if(xmlData) m_parsedMetaXml = ParsedMetaXml::Parse(xmlData->data(), xmlData->size()); + + if(!m_parsedMetaXml) + { + // meta/meta.ini (WUHB) + auto iniData = fsc_extractFile(fmt::format("{}meta/meta.ini", mountPath).c_str()); + if (iniData) + m_parsedMetaXml = ParseAromaIni(*iniData); + if(m_parsedMetaXml) + { + m_parsedCosXml = new ParsedCosXml{.argstr = "root.rpx"}; + m_parsedAppXml = new ParsedAppXml{m_parsedMetaXml->m_title_id, 0, 0, 0, 0}; + } + } + // code/app.xml xmlData = fsc_extractFile(fmt::format("{}code/app.xml", mountPath).c_str()); if(xmlData) @@ -539,6 +590,34 @@ bool TitleInfo::ParseXmlInfo() return true; } +ParsedMetaXml* TitleInfo::ParseAromaIni(std::span content) +{ + IniParser parser{content}; + while (parser.NextSection() && parser.GetCurrentSectionName() != "menu") + continue; + if (parser.GetCurrentSectionName() != "menu") + return nullptr; + + auto parsed = std::make_unique(); + + const auto author = parser.FindOption("author"); + if (author) + parsed->m_publisher[(size_t)CafeConsoleLanguage::EN] = *author; + + const auto longName = parser.FindOption("longname"); + if (longName) + parsed->m_long_name[(size_t)CafeConsoleLanguage::EN] = *longName; + + const auto shortName = parser.FindOption("shortname"); + if (shortName) + parsed->m_short_name[(size_t)CafeConsoleLanguage::EN] = *shortName; + + auto checksumInput = std::string{*author}.append(*longName).append(*shortName); + parsed->m_title_id = (0x0005000Full<<32) | crc32_calc(checksumInput.data(), checksumInput.length()); + + return parsed.release(); +} + bool TitleInfo::ParseAppXml(std::vector& appXmlData) { pugi::xml_document app_doc; @@ -695,6 +774,9 @@ std::string TitleInfo::GetPrintPath() const case TitleDataFormat::WIIU_ARCHIVE: tmp.append(" [WUA]"); break; + case TitleDataFormat::WUHB: + tmp.append(" [WUHB]"); + break; default: break; } diff --git a/src/Cafe/TitleList/TitleInfo.h b/src/Cafe/TitleList/TitleInfo.h index e9347db7..fa5b9c89 100644 --- a/src/Cafe/TitleList/TitleInfo.h +++ b/src/Cafe/TitleList/TitleInfo.h @@ -127,6 +127,7 @@ public: WUD = 2, // WUD or WUX WIIU_ARCHIVE = 3, // Wii U compressed single-file archive (.wua) NUS = 4, // NUS format. Directory with .app files, title.tik and title.tmd + WUHB = 5, // error INVALID_STRUCTURE = 0, }; @@ -265,6 +266,7 @@ private: bool DetectFormat(const fs::path& path, fs::path& pathOut, TitleDataFormat& formatOut); void CalcUID(); void SetInvalidReason(InvalidReason reason); + ParsedMetaXml* ParseAromaIni(std::span content); bool ParseAppXml(std::vector& appXmlData); bool m_isValid{ false }; @@ -277,6 +279,7 @@ private: std::vector> m_mountpoints; class FSTVolume* m_wudVolume{}; class ZArchiveReader* m_zarchive{}; + class WUHBReader* m_wuhbreader{}; // xml info bool m_hasParsedXmlFiles{ false }; ParsedMetaXml* m_parsedMetaXml{}; diff --git a/src/Cafe/TitleList/TitleList.cpp b/src/Cafe/TitleList/TitleList.cpp index c288dd13..7b75fac7 100644 --- a/src/Cafe/TitleList/TitleList.cpp +++ b/src/Cafe/TitleList/TitleList.cpp @@ -342,7 +342,8 @@ bool _IsKnownFileNameOrExtension(const fs::path& path) fileExtension == ".wud" || fileExtension == ".wux" || fileExtension == ".iso" || - fileExtension == ".wua"; + fileExtension == ".wua" || + fileExtension == ".wuhb"; // note: To detect extracted titles with RPX we rely on the presence of the content,code,meta directory structure } diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index e8103f9a..c34c5477 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -643,16 +643,18 @@ void MainWindow::OnFileMenu(wxCommandEvent& event) if (menuId == MAINFRAME_MENU_ID_FILE_LOAD) { const auto wildcard = formatWxString( - "{}|*.wud;*.wux;*.wua;*.iso;*.rpx;*.elf;title.tmd" + "{}|*.wud;*.wux;*.wua;*.wuhb;*.iso;*.rpx;*.elf;title.tmd" "|{}|*.wud;*.wux;*.iso" "|{}|title.tmd" "|{}|*.wua" + "|{}|*.wuhb" "|{}|*.rpx;*.elf" "|{}|*", - _("All Wii U files (*.wud, *.wux, *.wua, *.iso, *.rpx, *.elf)"), + _("All Wii U files (*.wud, *.wux, *.wua, *.wuhb, *.iso, *.rpx, *.elf)"), _("Wii U image (*.wud, *.wux, *.iso, *.wad)"), _("Wii U NUS content"), _("Wii U archive (*.wua)"), + _("Wii U homebrew bundle (*.wuhb)"), _("Wii U executable (*.rpx, *.elf)"), _("All files (*.*)") ); diff --git a/src/gui/components/wxGameList.cpp b/src/gui/components/wxGameList.cpp index d7c9a4f8..eedfde5d 100644 --- a/src/gui/components/wxGameList.cpp +++ b/src/gui/components/wxGameList.cpp @@ -1230,6 +1230,16 @@ void wxGameList::AsyncWorkerThread() if(!titleInfo.Mount(tempMountPath, "", FSC_PRIORITY_BASE)) continue; auto tgaData = fsc_extractFile((tempMountPath + "/meta/iconTex.tga").c_str()); + // try iconTex.tga.gz + if (!tgaData) + { + tgaData = fsc_extractFile((tempMountPath + "/meta/iconTex.tga.gz").c_str()); + if (tgaData) + { + auto decompressed = zlibDecompress(*tgaData, 70*1024); + std::swap(tgaData, decompressed); + } + } bool iconSuccessfullyLoaded = false; if (tgaData && tgaData->size() > 16) { diff --git a/src/gui/components/wxTitleManagerList.cpp b/src/gui/components/wxTitleManagerList.cpp index c02bffb7..e8efb060 100644 --- a/src/gui/components/wxTitleManagerList.cpp +++ b/src/gui/components/wxTitleManagerList.cpp @@ -948,6 +948,8 @@ wxString wxTitleManagerList::GetTitleEntryText(const TitleEntry& entry, ItemColu return _("NUS"); case wxTitleManagerList::EntryFormat::WUA: return _("WUA"); + case wxTitleManagerList::EntryFormat::WUHB: + return _("WUHB"); } return ""; } @@ -1022,6 +1024,9 @@ void wxTitleManagerList::HandleTitleListCallback(CafeTitleListCallbackEvent* evt case TitleInfo::TitleDataFormat::WIIU_ARCHIVE: entryFormat = EntryFormat::WUA; break; + case TitleInfo::TitleDataFormat::WUHB: + entryFormat = EntryFormat::WUHB; + break; case TitleInfo::TitleDataFormat::HOST_FS: default: entryFormat = EntryFormat::Folder; diff --git a/src/gui/components/wxTitleManagerList.h b/src/gui/components/wxTitleManagerList.h index 14721c57..2780a9ce 100644 --- a/src/gui/components/wxTitleManagerList.h +++ b/src/gui/components/wxTitleManagerList.h @@ -44,6 +44,7 @@ public: WUD, NUS, WUA, + WUHB, }; // sort by column, if -1 will sort by last column or default (=titleid) diff --git a/src/util/helpers/helpers.cpp b/src/util/helpers/helpers.cpp index 7e22e9fb..bac2d446 100644 --- a/src/util/helpers/helpers.cpp +++ b/src/util/helpers/helpers.cpp @@ -11,6 +11,8 @@ #include +#include + #if BOOST_OS_WINDOWS #include @@ -437,3 +439,42 @@ std::string GenerateRandomString(const size_t length, const std::string_view cha return result; } + +std::optional> zlibDecompress(const std::vector& compressed, size_t sizeHint) +{ + int err; + std::vector decompressed; + size_t outWritten = 0; + size_t bytesPerIteration = sizeHint; + z_stream stream; + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + stream.avail_in = compressed.size(); + stream.next_in = (Bytef*)compressed.data(); + err = inflateInit2(&stream, 32); // 32 is a zlib magic value to enable header detection + if (err != Z_OK) + return {}; + + do + { + decompressed.resize(decompressed.size() + bytesPerIteration); + const auto availBefore = decompressed.size() - outWritten; + stream.avail_out = availBefore; + stream.next_out = decompressed.data() + outWritten; + err = inflate(&stream, Z_NO_FLUSH); + if (!(err == Z_OK || err == Z_STREAM_END)) + { + inflateEnd(&stream); + return {}; + } + outWritten += availBefore - stream.avail_out; + bytesPerIteration *= 2; + } + while (err != Z_STREAM_END); + + inflateEnd(&stream); + decompressed.resize(stream.total_out); + + return decompressed; +} diff --git a/src/util/helpers/helpers.h b/src/util/helpers/helpers.h index 09b80fed..1edc2e19 100644 --- a/src/util/helpers/helpers.h +++ b/src/util/helpers/helpers.h @@ -257,3 +257,5 @@ bool IsWindows81OrGreater(); bool IsWindows10OrGreater(); fs::path GetParentProcess(); + +std::optional> zlibDecompress(const std::vector& compressed, size_t sizeHint = 32*1024);