mirror of https://github.com/cemu-project/Cemu.git
Add support for WUHB file format (#1190)
This commit is contained in:
parent
f28043e0e9
commit
dc480ac00b
|
@ -10,6 +10,7 @@ add_library(CemuCafe
|
||||||
Filesystem/fscDeviceRedirect.cpp
|
Filesystem/fscDeviceRedirect.cpp
|
||||||
Filesystem/fscDeviceWua.cpp
|
Filesystem/fscDeviceWua.cpp
|
||||||
Filesystem/fscDeviceWud.cpp
|
Filesystem/fscDeviceWud.cpp
|
||||||
|
Filesystem/fscDeviceWuhb.cpp
|
||||||
Filesystem/fsc.h
|
Filesystem/fsc.h
|
||||||
Filesystem/FST/FST.cpp
|
Filesystem/FST/FST.cpp
|
||||||
Filesystem/FST/FST.h
|
Filesystem/FST/FST.h
|
||||||
|
@ -18,6 +19,9 @@ add_library(CemuCafe
|
||||||
Filesystem/FST/KeyCache.h
|
Filesystem/FST/KeyCache.h
|
||||||
Filesystem/WUD/wud.cpp
|
Filesystem/WUD/wud.cpp
|
||||||
Filesystem/WUD/wud.h
|
Filesystem/WUD/wud.h
|
||||||
|
Filesystem/WUHB/RomFSStructs.h
|
||||||
|
Filesystem/WUHB/WUHBReader.cpp
|
||||||
|
Filesystem/WUHB/WUHBReader.h
|
||||||
GamePatch.cpp
|
GamePatch.cpp
|
||||||
GamePatch.h
|
GamePatch.h
|
||||||
GameProfile/GameProfile.cpp
|
GameProfile/GameProfile.cpp
|
||||||
|
|
|
@ -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
|
|
@ -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<bool File>
|
||||||
|
const WUHBReader::EntryType<File>& WUHBReader::GetFallback()
|
||||||
|
{
|
||||||
|
if constexpr (File)
|
||||||
|
return fallbackFileEntry;
|
||||||
|
else
|
||||||
|
return fallbackDirEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<bool File>
|
||||||
|
WUHBReader::EntryType<File> WUHBReader::GetEntry(uint32 offset) const
|
||||||
|
{
|
||||||
|
auto fallback = GetFallback<File>();
|
||||||
|
if(offset == ROMFS_ENTRY_EMPTY)
|
||||||
|
return fallback;
|
||||||
|
|
||||||
|
const char* typeName = File ? "fentry" : "direntry";
|
||||||
|
EntryType<File> 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<File>, name));
|
||||||
|
if (read != offsetof(EntryType<File>, 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<false>(offset);
|
||||||
|
}
|
||||||
|
romfs_fentry_t WUHBReader::GetFileEntry(uint32 offset) const
|
||||||
|
{
|
||||||
|
return GetEntry<true>(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 T>
|
||||||
|
bool WUHBReader::SearchHashList(uint32& entryOffset, const fs::path& targetName) const
|
||||||
|
{
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
if (entryOffset == ROMFS_ENTRY_EMPTY)
|
||||||
|
return false;
|
||||||
|
auto entry = GetEntry<T>(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<true>(currentEntryOffset, part);
|
||||||
|
else
|
||||||
|
return SearchHashList<false>(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;
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
#pragma once
|
||||||
|
#include <Common/FileStream.h>
|
||||||
|
#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<FileStream> 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<bool File>
|
||||||
|
using EntryType = std::conditional_t<File, romfs_fentry_t, romfs_direntry_t>;
|
||||||
|
template<bool File>
|
||||||
|
static const EntryType<File>& GetFallback();
|
||||||
|
template<bool File>
|
||||||
|
EntryType<File> GetEntry(uint32 offset) const;
|
||||||
|
|
||||||
|
template<bool T>
|
||||||
|
bool SearchHashList(uint32& entryOffset, const fs::path& targetName) const;
|
||||||
|
uint32 GetHashTableEntryOffset(uint32 hash, bool isFile) const;
|
||||||
|
};
|
|
@ -204,6 +204,9 @@ bool FSCDeviceWUD_Mount(std::string_view mountPath, std::string_view destination
|
||||||
// wua device
|
// wua device
|
||||||
bool FSCDeviceWUA_Mount(std::string_view mountPath, std::string_view destinationBaseDir, class ZArchiveReader* archive, sint32 priority);
|
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
|
// hostFS device
|
||||||
bool FSCDeviceHostFS_Mount(std::string_view mountPath, std::string_view hostTargetPath, sint32 priority);
|
bool FSCDeviceHostFS_Mount(std::string_view mountPath, std::string_view hostTargetPath, sint32 priority);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -1,9 +1,12 @@
|
||||||
#include "TitleInfo.h"
|
#include "TitleInfo.h"
|
||||||
#include "Cafe/Filesystem/fscDeviceHostFS.h"
|
#include "Cafe/Filesystem/fscDeviceHostFS.h"
|
||||||
|
#include "Cafe/Filesystem/WUHB/WUHBReader.h"
|
||||||
#include "Cafe/Filesystem/FST/FST.h"
|
#include "Cafe/Filesystem/FST/FST.h"
|
||||||
#include "pugixml.hpp"
|
#include "pugixml.hpp"
|
||||||
#include "Common/FileStream.h"
|
#include "Common/FileStream.h"
|
||||||
#include <zarchive/zarchivereader.h>
|
#include <zarchive/zarchivereader.h>
|
||||||
|
#include "util/IniParser/IniParser.h"
|
||||||
|
#include "util/crypto/crc32.h"
|
||||||
#include "config/ActiveSettings.h"
|
#include "config/ActiveSettings.h"
|
||||||
#include "util/helpers/helpers.h"
|
#include "util/helpers/helpers.h"
|
||||||
|
|
||||||
|
@ -97,6 +100,7 @@ TitleInfo::TitleInfo(const TitleInfo::CachedInfo& cachedInfo)
|
||||||
m_isValid = false;
|
m_isValid = false;
|
||||||
if (cachedInfo.titleDataFormat != TitleDataFormat::HOST_FS &&
|
if (cachedInfo.titleDataFormat != TitleDataFormat::HOST_FS &&
|
||||||
cachedInfo.titleDataFormat != TitleDataFormat::WIIU_ARCHIVE &&
|
cachedInfo.titleDataFormat != TitleDataFormat::WIIU_ARCHIVE &&
|
||||||
|
cachedInfo.titleDataFormat != TitleDataFormat::WUHB &&
|
||||||
cachedInfo.titleDataFormat != TitleDataFormat::WUD &&
|
cachedInfo.titleDataFormat != TitleDataFormat::WUD &&
|
||||||
cachedInfo.titleDataFormat != TitleDataFormat::NUS &&
|
cachedInfo.titleDataFormat != TitleDataFormat::NUS &&
|
||||||
cachedInfo.titleDataFormat != TitleDataFormat::INVALID_STRUCTURE)
|
cachedInfo.titleDataFormat != TitleDataFormat::INVALID_STRUCTURE)
|
||||||
|
@ -245,6 +249,16 @@ bool TitleInfo::DetectFormat(const fs::path& path, fs::path& pathOut, TitleDataF
|
||||||
delete zar;
|
delete zar;
|
||||||
return foundBase;
|
return foundBase;
|
||||||
}
|
}
|
||||||
|
else if (boost::iends_with(filenameStr, ".wuhb"))
|
||||||
|
{
|
||||||
|
std::unique_ptr<WUHBReader> 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
|
// 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
|
// instead TitleInfo has a second constructor which takes a subpath
|
||||||
// unable to determine type by extension, check contents
|
// 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;
|
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
|
else
|
||||||
{
|
{
|
||||||
cemu_assert_unimplemented();
|
cemu_assert_unimplemented();
|
||||||
|
@ -466,6 +497,12 @@ void TitleInfo::Unmount(std::string_view virtualPath)
|
||||||
_ZArchivePool_ReleaseInstance(m_fullPath, m_zarchive);
|
_ZArchivePool_ReleaseInstance(m_fullPath, m_zarchive);
|
||||||
if (m_mountpoints.empty())
|
if (m_mountpoints.empty())
|
||||||
m_zarchive = nullptr;
|
m_zarchive = nullptr;
|
||||||
|
}
|
||||||
|
if (m_wuhbreader)
|
||||||
|
{
|
||||||
|
cemu_assert_debug(m_titleFormat == TitleDataFormat::WUHB);
|
||||||
|
delete m_wuhbreader;
|
||||||
|
m_wuhbreader = nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
@ -502,6 +539,20 @@ bool TitleInfo::ParseXmlInfo()
|
||||||
auto xmlData = fsc_extractFile(fmt::format("{}meta/meta.xml", mountPath).c_str());
|
auto xmlData = fsc_extractFile(fmt::format("{}meta/meta.xml", mountPath).c_str());
|
||||||
if(xmlData)
|
if(xmlData)
|
||||||
m_parsedMetaXml = ParsedMetaXml::Parse(xmlData->data(), xmlData->size());
|
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
|
// code/app.xml
|
||||||
xmlData = fsc_extractFile(fmt::format("{}code/app.xml", mountPath).c_str());
|
xmlData = fsc_extractFile(fmt::format("{}code/app.xml", mountPath).c_str());
|
||||||
if(xmlData)
|
if(xmlData)
|
||||||
|
@ -539,6 +590,34 @@ bool TitleInfo::ParseXmlInfo()
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ParsedMetaXml* TitleInfo::ParseAromaIni(std::span<unsigned char> content)
|
||||||
|
{
|
||||||
|
IniParser parser{content};
|
||||||
|
while (parser.NextSection() && parser.GetCurrentSectionName() != "menu")
|
||||||
|
continue;
|
||||||
|
if (parser.GetCurrentSectionName() != "menu")
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
auto parsed = std::make_unique<ParsedMetaXml>();
|
||||||
|
|
||||||
|
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<uint8>& appXmlData)
|
bool TitleInfo::ParseAppXml(std::vector<uint8>& appXmlData)
|
||||||
{
|
{
|
||||||
pugi::xml_document app_doc;
|
pugi::xml_document app_doc;
|
||||||
|
@ -695,6 +774,9 @@ std::string TitleInfo::GetPrintPath() const
|
||||||
case TitleDataFormat::WIIU_ARCHIVE:
|
case TitleDataFormat::WIIU_ARCHIVE:
|
||||||
tmp.append(" [WUA]");
|
tmp.append(" [WUA]");
|
||||||
break;
|
break;
|
||||||
|
case TitleDataFormat::WUHB:
|
||||||
|
tmp.append(" [WUHB]");
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,6 +127,7 @@ public:
|
||||||
WUD = 2, // WUD or WUX
|
WUD = 2, // WUD or WUX
|
||||||
WIIU_ARCHIVE = 3, // Wii U compressed single-file archive (.wua)
|
WIIU_ARCHIVE = 3, // Wii U compressed single-file archive (.wua)
|
||||||
NUS = 4, // NUS format. Directory with .app files, title.tik and title.tmd
|
NUS = 4, // NUS format. Directory with .app files, title.tik and title.tmd
|
||||||
|
WUHB = 5,
|
||||||
// error
|
// error
|
||||||
INVALID_STRUCTURE = 0,
|
INVALID_STRUCTURE = 0,
|
||||||
};
|
};
|
||||||
|
@ -265,6 +266,7 @@ private:
|
||||||
bool DetectFormat(const fs::path& path, fs::path& pathOut, TitleDataFormat& formatOut);
|
bool DetectFormat(const fs::path& path, fs::path& pathOut, TitleDataFormat& formatOut);
|
||||||
void CalcUID();
|
void CalcUID();
|
||||||
void SetInvalidReason(InvalidReason reason);
|
void SetInvalidReason(InvalidReason reason);
|
||||||
|
ParsedMetaXml* ParseAromaIni(std::span<unsigned char> content);
|
||||||
bool ParseAppXml(std::vector<uint8>& appXmlData);
|
bool ParseAppXml(std::vector<uint8>& appXmlData);
|
||||||
|
|
||||||
bool m_isValid{ false };
|
bool m_isValid{ false };
|
||||||
|
@ -277,6 +279,7 @@ private:
|
||||||
std::vector<std::pair<sint32, std::string>> m_mountpoints;
|
std::vector<std::pair<sint32, std::string>> m_mountpoints;
|
||||||
class FSTVolume* m_wudVolume{};
|
class FSTVolume* m_wudVolume{};
|
||||||
class ZArchiveReader* m_zarchive{};
|
class ZArchiveReader* m_zarchive{};
|
||||||
|
class WUHBReader* m_wuhbreader{};
|
||||||
// xml info
|
// xml info
|
||||||
bool m_hasParsedXmlFiles{ false };
|
bool m_hasParsedXmlFiles{ false };
|
||||||
ParsedMetaXml* m_parsedMetaXml{};
|
ParsedMetaXml* m_parsedMetaXml{};
|
||||||
|
|
|
@ -342,7 +342,8 @@ bool _IsKnownFileNameOrExtension(const fs::path& path)
|
||||||
fileExtension == ".wud" ||
|
fileExtension == ".wud" ||
|
||||||
fileExtension == ".wux" ||
|
fileExtension == ".wux" ||
|
||||||
fileExtension == ".iso" ||
|
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
|
// note: To detect extracted titles with RPX we rely on the presence of the content,code,meta directory structure
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -643,16 +643,18 @@ void MainWindow::OnFileMenu(wxCommandEvent& event)
|
||||||
if (menuId == MAINFRAME_MENU_ID_FILE_LOAD)
|
if (menuId == MAINFRAME_MENU_ID_FILE_LOAD)
|
||||||
{
|
{
|
||||||
const auto wildcard = formatWxString(
|
const auto wildcard = formatWxString(
|
||||||
"{}|*.wud;*.wux;*.wua;*.iso;*.rpx;*.elf;title.tmd"
|
"{}|*.wud;*.wux;*.wua;*.wuhb;*.iso;*.rpx;*.elf;title.tmd"
|
||||||
"|{}|*.wud;*.wux;*.iso"
|
"|{}|*.wud;*.wux;*.iso"
|
||||||
"|{}|title.tmd"
|
"|{}|title.tmd"
|
||||||
"|{}|*.wua"
|
"|{}|*.wua"
|
||||||
|
"|{}|*.wuhb"
|
||||||
"|{}|*.rpx;*.elf"
|
"|{}|*.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 image (*.wud, *.wux, *.iso, *.wad)"),
|
||||||
_("Wii U NUS content"),
|
_("Wii U NUS content"),
|
||||||
_("Wii U archive (*.wua)"),
|
_("Wii U archive (*.wua)"),
|
||||||
|
_("Wii U homebrew bundle (*.wuhb)"),
|
||||||
_("Wii U executable (*.rpx, *.elf)"),
|
_("Wii U executable (*.rpx, *.elf)"),
|
||||||
_("All files (*.*)")
|
_("All files (*.*)")
|
||||||
);
|
);
|
||||||
|
|
|
@ -1230,6 +1230,16 @@ void wxGameList::AsyncWorkerThread()
|
||||||
if(!titleInfo.Mount(tempMountPath, "", FSC_PRIORITY_BASE))
|
if(!titleInfo.Mount(tempMountPath, "", FSC_PRIORITY_BASE))
|
||||||
continue;
|
continue;
|
||||||
auto tgaData = fsc_extractFile((tempMountPath + "/meta/iconTex.tga").c_str());
|
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;
|
bool iconSuccessfullyLoaded = false;
|
||||||
if (tgaData && tgaData->size() > 16)
|
if (tgaData && tgaData->size() > 16)
|
||||||
{
|
{
|
||||||
|
|
|
@ -948,6 +948,8 @@ wxString wxTitleManagerList::GetTitleEntryText(const TitleEntry& entry, ItemColu
|
||||||
return _("NUS");
|
return _("NUS");
|
||||||
case wxTitleManagerList::EntryFormat::WUA:
|
case wxTitleManagerList::EntryFormat::WUA:
|
||||||
return _("WUA");
|
return _("WUA");
|
||||||
|
case wxTitleManagerList::EntryFormat::WUHB:
|
||||||
|
return _("WUHB");
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -1022,6 +1024,9 @@ void wxTitleManagerList::HandleTitleListCallback(CafeTitleListCallbackEvent* evt
|
||||||
case TitleInfo::TitleDataFormat::WIIU_ARCHIVE:
|
case TitleInfo::TitleDataFormat::WIIU_ARCHIVE:
|
||||||
entryFormat = EntryFormat::WUA;
|
entryFormat = EntryFormat::WUA;
|
||||||
break;
|
break;
|
||||||
|
case TitleInfo::TitleDataFormat::WUHB:
|
||||||
|
entryFormat = EntryFormat::WUHB;
|
||||||
|
break;
|
||||||
case TitleInfo::TitleDataFormat::HOST_FS:
|
case TitleInfo::TitleDataFormat::HOST_FS:
|
||||||
default:
|
default:
|
||||||
entryFormat = EntryFormat::Folder;
|
entryFormat = EntryFormat::Folder;
|
||||||
|
|
|
@ -44,6 +44,7 @@ public:
|
||||||
WUD,
|
WUD,
|
||||||
NUS,
|
NUS,
|
||||||
WUA,
|
WUA,
|
||||||
|
WUHB,
|
||||||
};
|
};
|
||||||
|
|
||||||
// sort by column, if -1 will sort by last column or default (=titleid)
|
// sort by column, if -1 will sort by last column or default (=titleid)
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
|
|
||||||
#include <boost/random/uniform_int.hpp>
|
#include <boost/random/uniform_int.hpp>
|
||||||
|
|
||||||
|
#include <zlib.h>
|
||||||
|
|
||||||
|
|
||||||
#if BOOST_OS_WINDOWS
|
#if BOOST_OS_WINDOWS
|
||||||
#include <TlHelp32.h>
|
#include <TlHelp32.h>
|
||||||
|
@ -437,3 +439,42 @@ std::string GenerateRandomString(const size_t length, const std::string_view cha
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<std::vector<uint8>> zlibDecompress(const std::vector<uint8>& compressed, size_t sizeHint)
|
||||||
|
{
|
||||||
|
int err;
|
||||||
|
std::vector<uint8> 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;
|
||||||
|
}
|
||||||
|
|
|
@ -257,3 +257,5 @@ bool IsWindows81OrGreater();
|
||||||
bool IsWindows10OrGreater();
|
bool IsWindows10OrGreater();
|
||||||
|
|
||||||
fs::path GetParentProcess();
|
fs::path GetParentProcess();
|
||||||
|
|
||||||
|
std::optional<std::vector<uint8>> zlibDecompress(const std::vector<uint8>& compressed, size_t sizeHint = 32*1024);
|
||||||
|
|
Loading…
Reference in New Issue