Cemu/src/Cafe/TitleList/TitleInfo.cpp

653 lines
17 KiB
C++

#include "TitleInfo.h"
#include "Cafe/Filesystem/fscDeviceHostFS.h"
#include "Cafe/Filesystem/FST/FST.h"
#include "pugixml.hpp"
#include "Common/FileStream.h"
#include <zarchive/zarchivereader.h>
#include "config/ActiveSettings.h"
// detect format by reading file header/footer
CafeTitleFileType DetermineCafeSystemFileType(fs::path filePath)
{
std::unique_ptr<FileStream> fs(FileStream::openFile2(filePath));
if (!fs)
return CafeTitleFileType::UNKNOWN;
// very small files (<32 bytes) are always considered unknown
uint64 fileSize = fs->GetSize();
if (fileSize < 32)
return CafeTitleFileType::UNKNOWN;
// read header bytes
uint8 headerRaw[32]{};
fs->readData(headerRaw, sizeof(headerRaw));
// check for WUX
uint8 wuxHeaderMagic[8] = { 0x57,0x55,0x58,0x30,0x2E,0xD0,0x99,0x10 };
if (memcmp(headerRaw, wuxHeaderMagic, sizeof(wuxHeaderMagic)) == 0)
return CafeTitleFileType::WUX;
// check for RPX
uint8 rpxHeaderMagic[9] = { 0x7F,0x45,0x4C,0x46,0x01,0x02,0x01,0xCA,0xFE };
if (memcmp(headerRaw, rpxHeaderMagic, sizeof(rpxHeaderMagic)) == 0)
return CafeTitleFileType::RPX;
// check for ELF
uint8 elfHeaderMagic[9] = { 0x7F,0x45,0x4C,0x46,0x01,0x02,0x01,0x00,0x00 };
if (memcmp(headerRaw, elfHeaderMagic, sizeof(elfHeaderMagic)) == 0)
return CafeTitleFileType::ELF;
// check for WUD
uint8 wudMagic1[4] = { 0x57,0x55,0x50,0x2D }; // wud files should always start with "WUP-..."
uint8 wudMagic2[4] = { 0xCC,0x54,0x9E,0xB9 };
if (fileSize >= 0x10000)
{
uint8 magic1[4];
fs->SetPosition(0);
fs->readData(magic1, 4);
if (memcmp(magic1, wudMagic1, 4) == 0)
{
uint8 magic2[4];
fs->SetPosition(0x10000);
fs->readData(magic2, 4);
if (memcmp(magic2, wudMagic2, 4) == 0)
{
return CafeTitleFileType::WUD;
}
}
}
// check for WUA
// todo
return CafeTitleFileType::UNKNOWN;
}
TitleInfo::TitleInfo(const fs::path& path)
{
m_isValid = DetectFormat(path, m_fullPath, m_titleFormat);
if (!m_isValid)
m_titleFormat = TitleDataFormat::INVALID_STRUCTURE;
else
{
m_isValid = ParseXmlInfo();
}
if (m_isValid)
CalcUID();
}
TitleInfo::TitleInfo(const fs::path& path, std::string_view subPath)
{
// path must point to a (wua) file
if (!path.has_filename())
{
m_isValid = false;
return;
}
m_isValid = true;
m_titleFormat = TitleDataFormat::WIIU_ARCHIVE;
m_fullPath = path;
m_subPath = subPath;
m_isValid = ParseXmlInfo();
if (m_isValid)
CalcUID();
}
TitleInfo::TitleInfo(const TitleInfo::CachedInfo& cachedInfo)
{
m_cachedInfo = new CachedInfo(cachedInfo);
m_fullPath = cachedInfo.path;
m_subPath = cachedInfo.subPath;
m_titleFormat = cachedInfo.titleDataFormat;
// verify some parameters
m_isValid = false;
if (cachedInfo.titleDataFormat != TitleDataFormat::HOST_FS &&
cachedInfo.titleDataFormat != TitleDataFormat::WIIU_ARCHIVE &&
cachedInfo.titleDataFormat != TitleDataFormat::WUD &&
cachedInfo.titleDataFormat != TitleDataFormat::INVALID_STRUCTURE)
return;
if (cachedInfo.path.empty())
return;
if (cachedInfo.titleDataFormat == TitleDataFormat::WIIU_ARCHIVE && m_subPath.empty())
return; // for wua files the subpath must never be empty (title must not be stored in root of archive)
m_isValid = true;
CalcUID();
}
TitleInfo::~TitleInfo()
{
cemu_assert(m_mountpoints.empty());
delete m_parsedMetaXml;
delete m_parsedAppXml;
delete m_parsedCosXml;
delete m_cachedInfo;
}
TitleInfo::CachedInfo TitleInfo::MakeCacheEntry()
{
cemu_assert_debug(IsValid());
CachedInfo e;
e.titleDataFormat = m_titleFormat;
e.path = m_fullPath;
e.subPath = m_subPath;
e.titleId = GetAppTitleId();
e.titleVersion = GetAppTitleVersion();
e.titleName = GetTitleName();
e.region = GetMetaRegion();
e.group_id = GetAppGroup();
e.app_type = GetAppType();
return e;
}
// WUA can contain multiple titles. Root directory contains one directory for each title. The name must match: <titleId>_v<version>
bool TitleInfo::ParseWuaTitleFolderName(std::string_view name, TitleId& titleIdOut, uint16& titleVersionOut)
{
std::string_view sv = name;
if (sv.size() < 16 + 2)
return false;
TitleId parsedId;
if (!TitleIdParser::ParseFromStr(sv, parsedId))
return false;
sv.remove_prefix(16);
if (sv[0] != '_' || (sv[1] != 'v' && sv[1] != 'v'))
return false;
sv.remove_prefix(2);
if (sv.empty())
return false;
if (sv[0] == '0' && sv.size() != 1) // leading zero not allowed
return false;
uint32 v = 0;
while (!sv.empty())
{
uint8 c = sv[0];
sv.remove_prefix(1);
v *= 10;
if (c >= '0' && c <= '9')
v += (uint32)(c - '0');
else
{
v = 0xFFFFFFFF;
break;
}
}
if (v > 0xFFFF)
return false;
titleIdOut = parsedId;
titleVersionOut = v;
return true;
}
bool TitleInfo::DetectFormat(const fs::path& path, fs::path& pathOut, TitleDataFormat& formatOut)
{
std::error_code ec;
if (path.has_extension() && fs::is_regular_file(path, ec))
{
std::string filenameStr = _pathToUtf8(path.filename());
if (boost::iends_with(filenameStr, ".rpx"))
{
// is in code folder?
fs::path parentPath = path.parent_path();
if (boost::iequals(_pathToUtf8(parentPath.filename()), "code"))
{
parentPath = parentPath.parent_path();
// next to content and meta?
std::error_code ec;
if (fs::exists(parentPath / "content", ec) && fs::exists(parentPath / "meta", ec))
{
formatOut = TitleDataFormat::HOST_FS;
pathOut = parentPath;
return true;
}
}
}
else if (boost::iends_with(filenameStr, ".wud") ||
boost::iends_with(filenameStr, ".wux") ||
boost::iends_with(filenameStr, ".iso"))
{
formatOut = TitleDataFormat::WUD;
pathOut = path;
return true;
}
else if (boost::iends_with(filenameStr, ".wua"))
{
formatOut = TitleDataFormat::WIIU_ARCHIVE;
pathOut = path;
// a Wii U archive file can contain multiple titles but TitleInfo only maps to one
// we use the first base title that we find. This is the most intuitive behavior when someone launches "game.wua"
ZArchiveReader* zar = ZArchiveReader::OpenFromFile(path);
if (!zar)
return false;
ZArchiveNodeHandle rootDir = zar->LookUp("", false, true);
bool foundBase = false;
for (uint32 i = 0; i < zar->GetDirEntryCount(rootDir); i++)
{
ZArchiveReader::DirEntry dirEntry;
if (!zar->GetDirEntry(rootDir, i, dirEntry))
continue;
if (!dirEntry.isDirectory)
continue;
TitleId parsedId;
uint16 parsedVersion;
if (!TitleInfo::ParseWuaTitleFolderName(dirEntry.name, parsedId, parsedVersion))
continue;
TitleIdParser tip(parsedId);
TitleIdParser::TITLE_TYPE tt = tip.GetType();
if (tt == TitleIdParser::TITLE_TYPE::BASE_TITLE || tt == TitleIdParser::TITLE_TYPE::BASE_TITLE_DEMO ||
tt == TitleIdParser::TITLE_TYPE::SYSTEM_TITLE || tt == TitleIdParser::TITLE_TYPE::SYSTEM_OVERLAY_TITLE)
{
m_subPath = dirEntry.name;
foundBase = true;
break;
}
}
delete zar;
return foundBase;
}
// 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
CafeTitleFileType fileType = DetermineCafeSystemFileType(path);
if (fileType == CafeTitleFileType::WUD ||
fileType == CafeTitleFileType::WUX)
{
formatOut = TitleDataFormat::WUD;
pathOut = path;
return true;
}
}
else
{
// does it point to the root folder of a title?
std::error_code ec;
if (fs::exists(path / "content", ec) && fs::exists(path / "meta", ec) && fs::exists(path / "code", ec))
{
formatOut = TitleDataFormat::HOST_FS;
pathOut = path;
return true;
}
}
return false;
}
bool TitleInfo::IsValid() const
{
return m_isValid;
}
fs::path TitleInfo::GetPath() const
{
if (!m_isValid)
{
cemu_assert_suspicious();
return {};
}
return m_fullPath;
}
void TitleInfo::CalcUID()
{
cemu_assert_debug(m_isValid);
if (!m_isValid)
{
m_uid = 0;
return;
}
// get absolute normalized path
fs::path normalizedPath;
if (m_fullPath.is_relative())
{
normalizedPath = ActiveSettings::GetPath();
normalizedPath /= m_fullPath;
}
else
normalizedPath = m_fullPath;
normalizedPath = normalizedPath.lexically_normal();
uint64 h = fs::hash_value(normalizedPath);
// for WUA files also hash the subpath
if (m_titleFormat == TitleDataFormat::WIIU_ARCHIVE)
{
uint64 subHash = std::hash<std::string_view>{}(m_subPath);
h += subHash;
}
m_uid = h;
}
uint64 TitleInfo::GetUID()
{
cemu_assert_debug(m_isValid);
return m_uid;
}
std::mutex sZArchivePoolMtx;
std::map<fs::path, std::pair<uint32, ZArchiveReader*>> sZArchivePool;
ZArchiveReader* _ZArchivePool_AcquireInstance(const fs::path& path)
{
std::unique_lock _lock(sZArchivePoolMtx);
auto it = sZArchivePool.find(path);
if (it != sZArchivePool.end())
{
it->second.first++; // increment ref count
return it->second.second;
}
_lock.unlock();
// opening wua files can be expensive, so we do it outside of the lock
ZArchiveReader* zar = ZArchiveReader::OpenFromFile(path);
if (!zar)
return nullptr;
_lock.lock();
// check if another instance was allocated in the meantime
it = sZArchivePool.find(path);
if (it != sZArchivePool.end())
{
delete zar;
it->second.first++; // increment ref count
return it->second.second;
}
sZArchivePool.emplace(std::piecewise_construct,
std::forward_as_tuple(path),
std::forward_as_tuple(1, zar)
);
return zar;
}
void _ZArchivePool_ReleaseInstance(const fs::path& path, ZArchiveReader* zar)
{
std::unique_lock _lock(sZArchivePoolMtx);
auto it = sZArchivePool.find(path);
cemu_assert(it != sZArchivePool.end());
cemu_assert(it->second.second == zar);
it->second.first--; // decrement ref count
if (it->second.first == 0)
{
delete it->second.second;
sZArchivePool.erase(it);
}
}
bool TitleInfo::Mount(std::string_view virtualPath, std::string_view subfolder, sint32 mountPriority)
{
cemu_assert_debug(subfolder.empty() || (subfolder.front() != '/' || subfolder.front() != '\\')); // only relative subfolder allowed
cemu_assert(m_isValid);
if (m_titleFormat == TitleDataFormat::HOST_FS)
{
fs::path hostFSPath = m_fullPath;
hostFSPath.append(subfolder);
bool r = FSCDeviceHostFS_Mount(std::string(virtualPath).c_str(), _pathToUtf8(hostFSPath), mountPriority);
cemu_assert_debug(r);
if (!r)
{
cemuLog_log(LogType::Force, "Failed to mount {} to {}", virtualPath, subfolder);
return false;
}
}
else if (m_titleFormat == TitleDataFormat::WUD)
{
if (m_mountpoints.empty())
{
cemu_assert_debug(!m_wudVolume);
m_wudVolume = FSTVolume::OpenFromDiscImage(m_fullPath);
}
if (!m_wudVolume)
return false;
bool r = FSCDeviceWUD_Mount(virtualPath, subfolder, m_wudVolume, mountPriority);
cemu_assert_debug(r);
if (!r)
{
cemuLog_log(LogType::Force, "Failed to mount {} to {}", virtualPath, subfolder);
delete m_wudVolume;
return false;
}
}
else if (m_titleFormat == TitleDataFormat::WIIU_ARCHIVE)
{
if (!m_zarchive)
{
m_zarchive = _ZArchivePool_AcquireInstance(m_fullPath);
if (!m_zarchive)
return false;
}
bool r = FSCDeviceWUA_Mount(virtualPath, std::string(m_subPath).append("/").append(subfolder), m_zarchive, mountPriority);
if (!r)
{
cemuLog_log(LogType::Force, "Failed to mount {} to {}", virtualPath, subfolder);
_ZArchivePool_ReleaseInstance(m_fullPath, m_zarchive);
return false;
}
}
else
{
cemu_assert_unimplemented();
}
m_mountpoints.emplace_back(mountPriority, virtualPath);
return true;
}
void TitleInfo::Unmount(std::string_view virtualPath)
{
for (auto& itr : m_mountpoints)
{
if (!boost::equals(itr.second, virtualPath))
continue;
fsc_unmount(itr.second.c_str(), itr.first);
std::erase(m_mountpoints, itr);
// if the last mount point got unmounted, delete any open devices
if (m_mountpoints.empty())
{
if (m_wudVolume)
{
cemu_assert_debug(m_titleFormat == TitleDataFormat::WUD);
delete m_wudVolume;
m_wudVolume = nullptr;
}
}
// wua files use reference counting
if (m_zarchive)
{
_ZArchivePool_ReleaseInstance(m_fullPath, m_zarchive);
if (m_mountpoints.empty())
m_zarchive = nullptr;
}
return;
}
cemu_assert_suspicious(); // unmount on unknown path
}
void TitleInfo::UnmountAll()
{
while (!m_mountpoints.empty())
Unmount(m_mountpoints.front().second);
}
std::atomic_uint64_t sTempMountingPathCounter = 1;
std::string TitleInfo::GetUniqueTempMountingPath()
{
uint64_t v = sTempMountingPathCounter.fetch_add(1);
return fmt::format("/internal/tempMount{:016x}/", v);
}
bool TitleInfo::ParseXmlInfo()
{
cemu_assert(m_isValid);
if (m_hasParsedXmlFiles)
return m_parsedMetaXml && m_parsedAppXml && m_parsedCosXml;
m_hasParsedXmlFiles = true;
std::string mountPath = GetUniqueTempMountingPath();
bool r = Mount(mountPath, "", FSC_PRIORITY_BASE);
if (!r)
return false;
// meta/meta.xml
auto xmlData = fsc_extractFile(fmt::format("{}meta/meta.xml", mountPath).c_str());
if(xmlData)
m_parsedMetaXml = ParsedMetaXml::Parse(xmlData->data(), xmlData->size());
// code/app.xml
xmlData = fsc_extractFile(fmt::format("{}code/app.xml", mountPath).c_str());
if(xmlData)
ParseAppXml(*xmlData);
// code/cos.xml
xmlData = fsc_extractFile(fmt::format("{}code/cos.xml", mountPath).c_str());
if (xmlData)
m_parsedCosXml = ParsedCosXml::Parse(xmlData->data(), xmlData->size());
Unmount(mountPath);
bool hasAnyXml = m_parsedMetaXml || m_parsedAppXml || m_parsedCosXml;
if (!m_parsedMetaXml || !m_parsedAppXml || !m_parsedCosXml)
{
if (hasAnyXml)
cemuLog_log(LogType::Force, "Title has missing meta .xml files. Title path: {}", _pathToUtf8(m_fullPath));
delete m_parsedMetaXml;
delete m_parsedAppXml;
delete m_parsedCosXml;
m_parsedMetaXml = nullptr;
m_parsedAppXml = nullptr;
m_parsedCosXml = nullptr;
m_isValid = false;
return false;
}
m_isValid = true;
return true;
}
bool TitleInfo::ParseAppXml(std::vector<uint8>& appXmlData)
{
pugi::xml_document app_doc;
if (!app_doc.load_buffer_inplace(appXmlData.data(), appXmlData.size()))
return false;
const auto root = app_doc.child("app");
if (!root)
return false;
m_parsedAppXml = new ParsedAppXml();
for (const auto& child : root.children())
{
std::string_view name = child.name();
if (name == "title_version")
m_parsedAppXml->title_version = (uint16)std::stoull(child.text().as_string(), nullptr, 16);
else if (name == "title_id")
m_parsedAppXml->title_id = std::stoull(child.text().as_string(), nullptr, 16);
else if (name == "app_type")
m_parsedAppXml->app_type = (uint32)std::stoull(child.text().as_string(), nullptr, 16);
else if (name == "group_id")
m_parsedAppXml->group_id = (uint32)std::stoull(child.text().as_string(), nullptr, 16);
}
return true;
}
TitleId TitleInfo::GetAppTitleId() const
{
cemu_assert_debug(m_isValid);
if (m_parsedAppXml)
return m_parsedAppXml->title_id;
if (m_cachedInfo)
return m_cachedInfo->titleId;
cemu_assert_suspicious();
return 0;
}
uint16 TitleInfo::GetAppTitleVersion() const
{
cemu_assert_debug(m_isValid);
if (m_parsedAppXml)
return m_parsedAppXml->title_version;
if (m_cachedInfo)
return m_cachedInfo->titleVersion;
cemu_assert_suspicious();
return 0;
}
uint32 TitleInfo::GetAppGroup() const
{
cemu_assert_debug(m_isValid);
if (m_parsedAppXml)
return m_parsedAppXml->group_id;
if (m_cachedInfo)
return m_cachedInfo->group_id;
cemu_assert_suspicious();
return 0;
}
uint32 TitleInfo::GetAppType() const
{
cemu_assert_debug(m_isValid);
if (m_parsedAppXml)
return m_parsedAppXml->app_type;
if (m_cachedInfo)
return m_cachedInfo->app_type;
cemu_assert_suspicious();
return 0;
}
TitleIdParser::TITLE_TYPE TitleInfo::GetTitleType()
{
TitleIdParser tip(GetAppTitleId());
return tip.GetType();
}
std::string TitleInfo::GetTitleName() const
{
cemu_assert_debug(m_isValid);
if (m_parsedMetaXml)
return m_parsedMetaXml->GetShortName(CafeConsoleLanguage::EN);
if (m_cachedInfo)
return m_cachedInfo->titleName;
cemu_assert_suspicious();
return "";
}
CafeConsoleRegion TitleInfo::GetMetaRegion() const
{
cemu_assert_debug(m_isValid);
if (m_parsedMetaXml)
return m_parsedMetaXml->GetRegion();
if (m_cachedInfo)
return m_cachedInfo->region;
cemu_assert_suspicious();
return CafeConsoleRegion::JPN;
}
std::string TitleInfo::GetArgStr() const
{
cemu_assert_debug(m_parsedCosXml);
if (!m_parsedCosXml)
return "";
return m_parsedCosXml->argstr;
}
std::string TitleInfo::GetPrintPath() const
{
if (!m_isValid)
return "invalid";
std::string tmp;
tmp.append(_pathToUtf8(m_fullPath));
switch (m_titleFormat)
{
case TitleDataFormat::HOST_FS:
tmp.append(" [Folder]");
break;
case TitleDataFormat::WUD:
tmp.append(" [WUD]");
break;
case TitleDataFormat::WIIU_ARCHIVE:
tmp.append(" [WUA]");
break;
default:
break;
}
if (m_titleFormat == TitleDataFormat::WIIU_ARCHIVE)
tmp.append(fmt::format(" [{}]", m_subPath));
return tmp;
}
std::string TitleInfo::GetInstallPath() const
{
TitleId titleId = GetAppTitleId();
TitleIdParser tip(titleId);
std::string tmp;
if (tip.IsSystemTitle())
tmp = fmt::format("sys/title/{:08x}/{:08x}", GetTitleIdHigh(titleId), GetTitleIdLow(titleId));
else
tmp = fmt::format("usr/title/{:08x}/{:08x}", GetTitleIdHigh(titleId), GetTitleIdLow(titleId));
return tmp;
}