Merge pull request #4896 from leoetlino/esformats

Use ESFormats for tickets, TMDs and views
This commit is contained in:
Matthew Parlane 2017-02-27 16:15:05 +13:00 committed by GitHub
commit 48aeb5bf4b
31 changed files with 581 additions and 532 deletions

View File

@ -33,6 +33,7 @@ set(SRCS Analytics.cpp
x64ABI.cpp
x64Emitter.cpp
MD5.cpp
Crypto/AES.cpp
Crypto/bn.cpp
Crypto/ec.cpp
Logging/LogManager.cpp)

View File

@ -143,6 +143,7 @@
<ClInclude Include="x64ABI.h" />
<ClInclude Include="x64Emitter.h" />
<ClInclude Include="x64Reg.h" />
<ClInclude Include="Crypto\AES.h" />
<ClInclude Include="Crypto\bn.h" />
<ClInclude Include="Crypto\ec.h" />
<ClInclude Include="Logging\ConsoleListener.h" />
@ -195,6 +196,7 @@
<ClCompile Include="x64CPUDetect.cpp" />
<ClCompile Include="x64Emitter.cpp" />
<ClCompile Include="x64FPURoundMode.cpp" />
<ClCompile Include="Crypto\AES.cpp" />
<ClCompile Include="Crypto\bn.cpp" />
<ClCompile Include="Crypto\ec.cpp" />
<ClCompile Include="Logging\LogManager.cpp" />

View File

@ -79,6 +79,9 @@
<ClInclude Include="Logging\LogManager.h">
<Filter>Logging</Filter>
</ClInclude>
<ClInclude Include="Crypto\AES.h">
<Filter>Crypto</Filter>
</ClInclude>
<ClInclude Include="Crypto\ec.h">
<Filter>Crypto</Filter>
</ClInclude>
@ -266,6 +269,9 @@
<ClCompile Include="x64CPUDetect.cpp" />
<ClCompile Include="x64Emitter.cpp" />
<ClCompile Include="x64FPURoundMode.cpp" />
<ClCompile Include="Crypto\AES.cpp">
<Filter>Crypto</Filter>
</ClCompile>
<ClCompile Include="Crypto\bn.cpp">
<Filter>Crypto</Filter>
</ClCompile>

View File

@ -0,0 +1,24 @@
// Copyright 2017 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include <mbedtls/aes.h>
#include "Common/Crypto/AES.h"
namespace Common
{
namespace AES
{
std::vector<u8> Decrypt(const u8* key, u8* iv, const u8* src, size_t size)
{
mbedtls_aes_context aes_ctx;
std::vector<u8> buffer(size);
mbedtls_aes_setkey_dec(&aes_ctx, key, 128);
mbedtls_aes_crypt_cbc(&aes_ctx, MBEDTLS_AES_DECRYPT, size, iv, src, buffer.data());
return buffer;
}
} // namespace AES
} // namespace Common

View File

@ -0,0 +1,18 @@
// Copyright 2017 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#pragma once
#include <cstddef>
#include <vector>
#include "Common/CommonTypes.h"
namespace Common
{
namespace AES
{
std::vector<u8> Decrypt(const u8* key, u8* iv, const u8* src, size_t size);
} // namespace AES
} // namespace Common

View File

@ -102,7 +102,7 @@ bool CBoot::FindMapFile(std::string* existing_map_file, std::string* writable_ma
DiscIO::CNANDContentManager::Access().GetNANDLoader(_StartupPara.m_strFilename);
if (Loader.IsValid())
{
u64 TitleID = Loader.GetTitleID();
u64 TitleID = Loader.GetTMD().GetTitleId();
title_id_str = StringFromFormat("%08X_%08X", (u32)(TitleID >> 32) & 0xFFFFFFFF,
(u32)TitleID & 0xFFFFFFFF);
}
@ -278,11 +278,7 @@ bool CBoot::BootUp()
PanicAlertT("Warning - starting ISO in wrong console mode!");
}
std::vector<u8> tmd_buffer = pVolume.GetTMD();
if (!tmd_buffer.empty())
{
IOS::HLE::ES_DIVerify(tmd_buffer);
}
IOS::HLE::ES_DIVerify(pVolume.GetTMD());
_StartupPara.bWii = pVolume.GetVolumeType() == DiscIO::Platform::WII_DISC;

View File

@ -346,11 +346,9 @@ bool CBoot::EmulatedBS2_Wii()
PowerPC::ppcState.gpr[1] = 0x816ffff0; // StackPointer
std::vector<u8> tmd = DVDInterface::GetVolume().GetTMD();
IOS::ES::TMDReader tmd = DVDInterface::GetVolume().GetTMD();
IOS::HLE::TMDReader tmd_reader{std::move(tmd)};
if (!SetupWiiMemory(tmd_reader.GetIOSId()))
if (!SetupWiiMemory(tmd.GetIOSId()))
return false;
// Execute the apploader

View File

@ -78,7 +78,7 @@ bool CBoot::Boot_WiiWAD(const std::string& _pFilename)
if (!ContentLoader.IsValid())
return false;
u64 titleID = ContentLoader.GetTitleID();
u64 titleID = ContentLoader.GetTMD().GetTitleId();
// create data directory
File::CreateFullPath(Common::GetTitleDataPath(titleID, Common::FROM_SESSION_ROOT));
@ -86,12 +86,11 @@ bool CBoot::Boot_WiiWAD(const std::string& _pFilename)
IOS::HLE::CreateVirtualFATFilesystem();
// setup Wii memory
u64 ios_title_id = 0x0000000100000000ULL | ContentLoader.GetIosVersion();
if (!SetupWiiMemory(ios_title_id))
if (!SetupWiiMemory(ContentLoader.GetTMD().GetIOSId()))
return false;
// DOL
const DiscIO::SNANDContent* pContent =
ContentLoader.GetContentByIndex(ContentLoader.GetBootIndex());
ContentLoader.GetContentByIndex(ContentLoader.GetTMD().GetBootIndex());
if (pContent == nullptr)
return false;

View File

@ -922,7 +922,7 @@ bool SConfig::AutoSetup(EBootBS2 _BootBS2)
const DiscIO::CNANDContentLoader& ContentLoader =
DiscIO::CNANDContentManager::Access().GetNANDLoader(m_strFilename);
if (ContentLoader.GetContentByIndex(ContentLoader.GetBootIndex()) == nullptr)
if (ContentLoader.GetContentByIndex(ContentLoader.GetTMD().GetBootIndex()) == nullptr)
{
// WAD is valid yet cannot be booted. Install instead.
u64 installed = DiscIO::CNANDContentManager::Access().Install_WiiWAD(m_strFilename);
@ -931,7 +931,7 @@ bool SConfig::AutoSetup(EBootBS2 _BootBS2)
return false; // do not boot
}
SetRegion(ContentLoader.GetRegion(), &set_region_dir);
SetRegion(ContentLoader.GetTMD().GetRegion(), &set_region_dir);
bWii = true;
m_BootType = BOOT_WII_NAND;

View File

@ -14,6 +14,7 @@
#include "Core/HW/DVDInterface.h"
#include "Core/HW/Memmap.h"
#include "Core/IOS/DI/DI.h"
#include "Core/IOS/ES/Formats.h"
#include "Core/IOS/IPC.h"
#include "DiscIO/Volume.h"
@ -105,9 +106,10 @@ IPCCommandResult DI::IOCtlV(const IOCtlVRequest& request)
INFO_LOG(IOS_DI, "DVDLowOpenPartition: partition_offset 0x%016" PRIx64, partition_offset);
// Read TMD to the buffer
std::vector<u8> tmd_buffer = DVDInterface::GetVolume().GetTMD();
Memory::CopyToEmu(request.io_vectors[0].address, tmd_buffer.data(), tmd_buffer.size());
ES_DIVerify(tmd_buffer);
const ES::TMDReader tmd = DVDInterface::GetVolume().GetTMD();
const std::vector<u8> raw_tmd = tmd.GetRawTMD();
Memory::CopyToEmu(request.io_vectors[0].address, raw_tmd.data(), raw_tmd.size());
ES_DIVerify(tmd);
return_value = 1;
break;

View File

@ -2,35 +2,6 @@
// Licensed under GPLv2+
// Refer to the license.txt file included.
// =======================================================
// File description
// -------------
/* Here we handle /dev/es requests. We have cases for these functions, the exact
DevKitPro/libogc name is in parenthesis:
0x20 GetTitleID (ES_GetTitleID) (Input: none, Output: 8 bytes)
0x1d GetDataDir (ES_GetDataDir) (Input: 8 bytes, Output: 30 bytes)
0x1b DiGetTicketView (Input: none, Output: 216 bytes)
0x16 GetConsumption (Input: 8 bytes, Output: 0 bytes, 4 bytes) // there are two output buffers
0x12 GetNumTicketViews (ES_GetNumTicketViews) (Input: 8 bytes, Output: 4 bytes)
0x14 GetTMDViewSize (ES_GetTMDViewSize) (Input: ?, Output: ?) // I don't get this anymore,
it used to come after 0x12
but only the first two are correctly supported. For the other four we ignore any potential
input and only write zero to the out buffer. However, most games only use first two,
but some Nintendo developed games use the other ones to:
0x1b: Mario Galaxy, Mario Kart, SSBB
0x16: Mario Galaxy, Mario Kart, SSBB
0x12: Mario Kart
0x14: Mario Kart: But only if we don't return a zeroed out buffer for the 0x12 question,
and instead answer for example 1 will this question appear.
*/
// =============
#include <algorithm>
#include <cinttypes>
#include <cstdio>
@ -115,7 +86,7 @@ void ES::OpenInternal()
// check for cd ...
if (contentLoader.IsValid())
{
m_TitleID = contentLoader.GetTitleID();
m_TitleID = contentLoader.GetTMD().GetTitleId();
m_TitleIDs.clear();
DiscIO::cUIDsys uid_sys{Common::FromWhichRoot::FROM_SESSION_ROOT};
@ -215,7 +186,7 @@ u32 ES::OpenTitleContent(u32 CFD, u64 TitleID, u16 Index)
{
const DiscIO::CNANDContentLoader& Loader = AccessContentDevice(TitleID);
if (!Loader.IsValid())
if (!Loader.IsValid() || !Loader.GetTicket().IsValid())
{
WARN_LOG(IOS_ES, "ES: loader not valid for %" PRIx64, TitleID);
return 0xffffffff;
@ -230,8 +201,8 @@ u32 ES::OpenTitleContent(u32 CFD, u64 TitleID, u16 Index)
SContentAccess Access;
Access.m_Position = 0;
Access.m_Index = pContent->m_Index;
Access.m_Size = pContent->m_Size;
Access.m_Index = pContent->m_metadata.index;
Access.m_Size = static_cast<u32>(pContent->m_metadata.size);
Access.m_TitleID = TitleID;
pContent->m_Data->Open();
@ -347,7 +318,8 @@ IPCCommandResult ES::AddTicket(const IOCtlVRequest& request)
INFO_LOG(IOS_ES, "IOCTL_ES_ADDTICKET");
std::vector<u8> ticket(request.in_vectors[0].size);
Memory::CopyFromEmu(ticket.data(), request.in_vectors[0].address, request.in_vectors[0].size);
DiscIO::AddTicket(ticket);
DiscIO::AddTicket(IOS::ES::TicketReader{std::move(ticket)});
return GetDefaultReply(IPC_SUCCESS);
}
@ -441,18 +413,18 @@ IPCCommandResult ES::AddContentFinish(const IOCtlVRequest& request)
INFO_LOG(IOS_ES, "IOCTL_ES_ADDCONTENTFINISH: content fd %08x", content_fd);
// Try to find the title key from a pre-installed ticket.
std::vector<u8> ticket = DiscIO::FindSignedTicket(m_addtitle_tmd.GetTitleId());
if (ticket.size() == 0)
IOS::ES::TicketReader ticket = DiscIO::FindSignedTicket(m_addtitle_tmd.GetTitleId());
if (!ticket.IsValid())
{
return GetDefaultReply(ES_NO_TICKET_INSTALLED);
}
mbedtls_aes_context aes_ctx;
mbedtls_aes_setkey_dec(&aes_ctx, DiscIO::GetKeyFromTicket(ticket).data(), 128);
mbedtls_aes_setkey_dec(&aes_ctx, ticket.GetTitleKey().data(), 128);
// The IV for title content decryption is the lower two bytes of the
// content index, zero extended.
TMDReader::Content content_info;
IOS::ES::Content content_info;
if (!m_addtitle_tmd.FindContentById(m_addtitle_content_id, &content_info))
{
return GetDefaultReply(ES_INVALID_TMD);
@ -499,28 +471,21 @@ IPCCommandResult ES::GetTitleContentsCount(const IOCtlVRequest& request)
u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
const DiscIO::CNANDContentLoader& rNANDContent = AccessContentDevice(TitleID);
u16 NumberOfPrivateContent = 0;
s32 return_value = IPC_SUCCESS;
if (rNANDContent.IsValid()) // Not sure if dolphin will ever fail this check
{
NumberOfPrivateContent = rNANDContent.GetNumEntries();
const DiscIO::CNANDContentLoader& nand_content = AccessContentDevice(TitleID);
if (!nand_content.IsValid())
return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT);
if ((u32)(TitleID >> 32) == 0x00010000)
Memory::Write_U32(0, request.io_vectors[0].address);
else
Memory::Write_U32(NumberOfPrivateContent, request.io_vectors[0].address);
}
const u16 num_contents = nand_content.GetTMD().GetNumContents();
if ((u32)(TitleID >> 32) == 0x00010000)
Memory::Write_U32(0, request.io_vectors[0].address);
else
{
return_value = static_cast<s32>(rNANDContent.GetContentSize());
}
Memory::Write_U32(num_contents, request.io_vectors[0].address);
INFO_LOG(IOS_ES, "IOCTL_ES_GETTITLECONTENTSCNT: TitleID: %08x/%08x content count %i",
(u32)(TitleID >> 32), (u32)TitleID,
rNANDContent.IsValid() ? NumberOfPrivateContent : (u32)rNANDContent.GetContentSize());
(u32)(TitleID >> 32), (u32)TitleID, num_contents);
return GetDefaultReply(return_value);
return GetDefaultReply(IPC_SUCCESS);
}
IPCCommandResult ES::GetTitleContents(const IOCtlVRequest& request)
@ -533,25 +498,17 @@ IPCCommandResult ES::GetTitleContents(const IOCtlVRequest& request)
u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
const DiscIO::CNANDContentLoader& rNANDContent = AccessContentDevice(TitleID);
s32 return_value = IPC_SUCCESS;
if (rNANDContent.IsValid()) // Not sure if dolphin will ever fail this check
if (!rNANDContent.IsValid())
return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT);
for (const auto& content : rNANDContent.GetTMD().GetContents())
{
for (u16 i = 0; i < rNANDContent.GetNumEntries(); i++)
{
Memory::Write_U32(rNANDContent.GetContentByIndex(i)->m_ContentID,
request.io_vectors[0].address + i * 4);
INFO_LOG(IOS_ES, "IOCTL_ES_GETTITLECONTENTS: Index %d: %08x", i,
rNANDContent.GetContentByIndex(i)->m_ContentID);
}
}
else
{
return_value = static_cast<s32>(rNANDContent.GetContentSize());
ERROR_LOG(IOS_ES, "IOCTL_ES_GETTITLECONTENTS: Unable to open content %zu",
rNANDContent.GetContentSize());
const u16 index = content.index;
Memory::Write_U32(content.id, request.io_vectors[0].address + index * 4);
INFO_LOG(IOS_ES, "IOCTL_ES_GETTITLECONTENTS: Index %d: %08x", index, content.id);
}
return GetDefaultReply(return_value);
return GetDefaultReply(IPC_SUCCESS);
}
IPCCommandResult ES::OpenTitleContent(const IOCtlVRequest& request)
@ -611,7 +568,7 @@ IPCCommandResult ES::ReadContent(const IOCtlVRequest& request)
{
const DiscIO::CNANDContentLoader& ContentLoader = AccessContentDevice(rContent.m_TitleID);
// ContentLoader should never be invalid; rContent has been created by it.
if (ContentLoader.IsValid())
if (ContentLoader.IsValid() && ContentLoader.GetTicket().IsValid())
{
const DiscIO::SNANDContent* pContent = ContentLoader.GetContentByIndex(rContent.m_Index);
if (!pContent->m_Data->GetRange(rContent.m_Position, Size, pDest))
@ -777,46 +734,24 @@ IPCCommandResult ES::GetViewCount(const IOCtlVRequest& request)
u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
u32 retVal = 0;
const DiscIO::CNANDContentLoader& Loader = AccessContentDevice(TitleID);
u32 ViewCount =
static_cast<u32>(Loader.GetTicket().size()) / DiscIO::CNANDContentLoader::TICKET_SIZE;
if (!ViewCount)
size_t view_count = 0;
if (TitleID >> 32 == 0x00000001 && TitleID != TITLEID_SYSMENU)
{
std::string TicketFilename = Common::GetTicketFileName(TitleID, Common::FROM_SESSION_ROOT);
if (File::Exists(TicketFilename))
{
u32 FileSize = (u32)File::GetSize(TicketFilename);
_dbg_assert_msg_(IOS_ES, (FileSize % DiscIO::CNANDContentLoader::TICKET_SIZE) == 0,
"IOCTL_ES_GETVIEWCNT ticket file size seems to be wrong");
ViewCount = FileSize / DiscIO::CNANDContentLoader::TICKET_SIZE;
_dbg_assert_msg_(IOS_ES, (ViewCount > 0) && (ViewCount <= 4),
"IOCTL_ES_GETVIEWCNT ticket count seems to be wrong");
}
else if (TitleID >> 32 == 0x00000001)
{
// Fake a ticket view to make IOS reload work.
ViewCount = 1;
}
else
{
ViewCount = 0;
if (TitleID == TITLEID_SYSMENU)
{
PanicAlertT("There must be a ticket for 00000001/00000002. Your NAND dump is probably "
"incomplete.");
}
// retVal = ES_NO_TICKET_INSTALLED;
}
// Fake a ticket view to make IOS reload work.
view_count = 1;
}
else if (Loader.IsValid() && Loader.GetTicket().IsValid())
{
view_count = Loader.GetTicket().GetNumberOfTickets();
}
INFO_LOG(IOS_ES, "IOCTL_ES_GETVIEWCNT for titleID: %08x/%08x (View Count = %i)",
(u32)(TitleID >> 32), (u32)TitleID, ViewCount);
INFO_LOG(IOS_ES, "IOCTL_ES_GETVIEWCNT for titleID: %08x/%08x (View Count = %zu)",
static_cast<u32>(TitleID >> 32), static_cast<u32>(TitleID), view_count);
Memory::Write_U32(ViewCount, request.io_vectors[0].address);
return GetDefaultReply(retVal);
Memory::Write_U32(static_cast<u32>(view_count), request.io_vectors[0].address);
return GetDefaultReply(IPC_SUCCESS);
}
IPCCommandResult ES::GetViews(const IOCtlVRequest& request)
@ -826,70 +761,40 @@ IPCCommandResult ES::GetViews(const IOCtlVRequest& request)
u64 TitleID = Memory::Read_U64(request.in_vectors[0].address);
u32 maxViews = Memory::Read_U32(request.in_vectors[1].address);
u32 retVal = 0;
const DiscIO::CNANDContentLoader& Loader = AccessContentDevice(TitleID);
const std::vector<u8>& ticket = Loader.GetTicket();
if (ticket.empty())
if (TitleID >> 32 == 0x00000001 && TitleID != TITLEID_SYSMENU)
{
std::string TicketFilename = Common::GetTicketFileName(TitleID, Common::FROM_SESSION_ROOT);
if (File::Exists(TicketFilename))
{
File::IOFile pFile(TicketFilename, "rb");
if (pFile)
{
u8 FileTicket[DiscIO::CNANDContentLoader::TICKET_SIZE];
for (unsigned int View = 0;
View != maxViews &&
pFile.ReadBytes(FileTicket, DiscIO::CNANDContentLoader::TICKET_SIZE);
++View)
{
Memory::Write_U32(View, request.io_vectors[0].address + View * 0xD8);
Memory::CopyToEmu(request.io_vectors[0].address + 4 + View * 0xD8, FileTicket + 0x1D0,
212);
}
}
}
else if (TitleID >> 32 == 0x00000001)
{
// For IOS titles, the ticket view isn't normally parsed by either the
// SDK or libogc, just passed to LaunchTitle, so this
// shouldn't matter at all. Just fill out some fields just
// to be on the safe side.
u32 Address = request.io_vectors[0].address;
Memory::Memset(Address, 0, 0xD8);
Memory::Write_U64(TitleID, Address + 4 + (0x1dc - 0x1d0)); // title ID
Memory::Write_U16(0xffff, Address + 4 + (0x1e4 - 0x1d0)); // unnnown
Memory::Write_U32(0xff00, Address + 4 + (0x1ec - 0x1d0)); // access mask
Memory::Memset(Address + 4 + (0x222 - 0x1d0), 0xff, 0x20); // content permissions
}
else
{
// retVal = ES_NO_TICKET_INSTALLED;
PanicAlertT("IOCTL_ES_GETVIEWS: Tried to get data from an unknown ticket: %08x/%08x",
(u32)(TitleID >> 32), (u32)TitleID);
}
// For IOS titles, the ticket view isn't normally parsed by either the
// SDK or libogc, just passed to LaunchTitle, so this
// shouldn't matter at all. Just fill out some fields just
// to be on the safe side.
u32 Address = request.io_vectors[0].address;
Memory::Memset(Address, 0, 0xD8);
Memory::Write_U64(TitleID, Address + 4 + (0x1dc - 0x1d0)); // title ID
Memory::Write_U16(0xffff, Address + 4 + (0x1e4 - 0x1d0)); // unnnown
Memory::Write_U32(0xff00, Address + 4 + (0x1ec - 0x1d0)); // access mask
Memory::Memset(Address + 4 + (0x222 - 0x1d0), 0xff, 0x20); // content permissions
}
else
else if (Loader.IsValid() && Loader.GetTicket().IsValid())
{
u32 view_count =
static_cast<u32>(Loader.GetTicket().size()) / DiscIO::CNANDContentLoader::TICKET_SIZE;
for (unsigned int view = 0; view != maxViews && view < view_count; ++view)
u32 number_of_views = std::min(maxViews, Loader.GetTicket().GetNumberOfTickets());
for (u32 view = 0; view < number_of_views; ++view)
{
Memory::Write_U32(view, request.io_vectors[0].address + view * 0xD8);
Memory::CopyToEmu(request.io_vectors[0].address + 4 + view * 0xD8,
&ticket[0x1D0 + (view * DiscIO::CNANDContentLoader::TICKET_SIZE)], 212);
const std::vector<u8> ticket_view = Loader.GetTicket().GetRawTicketView(view);
Memory::CopyToEmu(request.io_vectors[0].address + view * sizeof(IOS::ES::TicketView),
ticket_view.data(), ticket_view.size());
}
}
INFO_LOG(IOS_ES, "IOCTL_ES_GETVIEWS for titleID: %08x/%08x (MaxViews = %i)", (u32)(TitleID >> 32),
(u32)TitleID, maxViews);
return GetDefaultReply(retVal);
return GetDefaultReply(IPC_SUCCESS);
}
// TODO: rename this to GetTMDViewSize. There is only one TMD, so the name doesn't make sense.
IPCCommandResult ES::GetTMDViewCount(const IOCtlVRequest& request)
{
_dbg_assert_msg_(IOS_ES, request.in_vectors.size() == 1, "IOCTL_ES_GETTMDVIEWCNT no in buffer");
@ -899,18 +804,13 @@ IPCCommandResult ES::GetTMDViewCount(const IOCtlVRequest& request)
const DiscIO::CNANDContentLoader& Loader = AccessContentDevice(TitleID);
u32 TMDViewCnt = 0;
u32 view_size = 0;
if (Loader.IsValid())
{
TMDViewCnt += DiscIO::CNANDContentLoader::TMD_VIEW_SIZE;
TMDViewCnt += 2; // title version
TMDViewCnt += 2; // num entries
TMDViewCnt += (u32)Loader.GetContentSize() * (4 + 2 + 2 + 8); // content id, index, type, size
}
Memory::Write_U32(TMDViewCnt, request.io_vectors[0].address);
view_size = static_cast<u32>(Loader.GetTMD().GetRawView().size());
Memory::Write_U32(view_size, request.io_vectors[0].address);
INFO_LOG(IOS_ES, "IOCTL_ES_GETTMDVIEWCNT: title: %08x/%08x (view size %i)", (u32)(TitleID >> 32),
(u32)TitleID, TMDViewCnt);
(u32)TitleID, view_size);
return GetDefaultReply(IPC_SUCCESS);
}
@ -929,30 +829,11 @@ IPCCommandResult ES::GetTMDViews(const IOCtlVRequest& request)
if (Loader.IsValid())
{
u32 Address = request.io_vectors[0].address;
const std::vector<u8> raw_view = Loader.GetTMD().GetRawView();
if (raw_view.size() != request.io_vectors[0].size)
return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT);
Memory::CopyToEmu(Address, Loader.GetTMDView(), DiscIO::CNANDContentLoader::TMD_VIEW_SIZE);
Address += DiscIO::CNANDContentLoader::TMD_VIEW_SIZE;
Memory::Write_U16(Loader.GetTitleVersion(), Address);
Address += 2;
Memory::Write_U16(Loader.GetNumEntries(), Address);
Address += 2;
const std::vector<DiscIO::SNANDContent>& rContent = Loader.GetContent();
for (size_t i = 0; i < Loader.GetContentSize(); i++)
{
Memory::Write_U32(rContent[i].m_ContentID, Address);
Address += 4;
Memory::Write_U16(rContent[i].m_Index, Address);
Address += 2;
Memory::Write_U16(rContent[i].m_Type, Address);
Address += 2;
Memory::Write_U64(rContent[i].m_Size, Address);
Address += 8;
}
_dbg_assert_(IOS_ES, (Address - request.io_vectors[0].address) == request.io_vectors[0].size);
Memory::CopyToEmu(request.io_vectors[0].address, raw_view.data(), raw_view.size());
}
INFO_LOG(IOS_ES, "IOCTL_ES_GETTMDVIEWS: title: %08x/%08x (buffer size: %i)", (u32)(TitleID >> 32),
@ -1004,18 +885,15 @@ IPCCommandResult ES::GetStoredTMDSize(const IOCtlVRequest& request)
const DiscIO::CNANDContentLoader& Loader = AccessContentDevice(TitleID);
_dbg_assert_(IOS_ES, Loader.IsValid());
u32 TMDCnt = 0;
u32 tmd_size = 0;
if (Loader.IsValid())
{
TMDCnt += DiscIO::CNANDContentLoader::TMD_HEADER_SIZE;
TMDCnt += (u32)Loader.GetContentSize() * DiscIO::CNANDContentLoader::CONTENT_HEADER_SIZE;
}
tmd_size = static_cast<u32>(Loader.GetTMD().GetRawTMD().size());
if (request.io_vectors.size())
Memory::Write_U32(TMDCnt, request.io_vectors[0].address);
Memory::Write_U32(tmd_size, request.io_vectors[0].address);
INFO_LOG(IOS_ES, "IOCTL_ES_GETSTOREDTMDSIZE: title: %08x/%08x (view size %i)",
(u32)(TitleID >> 32), (u32)TitleID, TMDCnt);
(u32)(TitleID >> 32), (u32)TitleID, tmd_size);
return GetDefaultReply(IPC_SUCCESS);
}
@ -1043,20 +921,11 @@ IPCCommandResult ES::GetStoredTMD(const IOCtlVRequest& request)
if (Loader.IsValid() && request.io_vectors.size())
{
u32 Address = request.io_vectors[0].address;
const std::vector<u8> raw_tmd = Loader.GetTMD().GetRawTMD();
if (raw_tmd.size() != request.io_vectors[0].size)
return GetDefaultReply(ES_PARAMETER_SIZE_OR_ALIGNMENT);
Memory::CopyToEmu(Address, Loader.GetTMDHeader(), DiscIO::CNANDContentLoader::TMD_HEADER_SIZE);
Address += DiscIO::CNANDContentLoader::TMD_HEADER_SIZE;
const std::vector<DiscIO::SNANDContent>& rContent = Loader.GetContent();
for (size_t i = 0; i < Loader.GetContentSize(); i++)
{
Memory::CopyToEmu(Address, rContent[i].m_Header,
DiscIO::CNANDContentLoader::CONTENT_HEADER_SIZE);
Address += DiscIO::CNANDContentLoader::CONTENT_HEADER_SIZE;
}
_dbg_assert_(IOS_ES, (Address - request.io_vectors[0].address) == request.io_vectors[0].size);
Memory::CopyToEmu(request.io_vectors[0].address, raw_tmd.data(), raw_tmd.size());
}
INFO_LOG(IOS_ES, "IOCTL_ES_GETSTOREDTMD: title: %08x/%08x (buffer size: %i)",
@ -1131,9 +1000,9 @@ IPCCommandResult ES::Launch(const IOCtlVRequest& request)
const DiscIO::CNANDContentLoader& ContentLoader = AccessContentDevice(TitleID);
if (ContentLoader.IsValid())
{
ios_to_load = 0x0000000100000000ULL | ContentLoader.GetIosVersion();
ios_to_load = ContentLoader.GetTMD().GetIOSId();
u32 bootInd = ContentLoader.GetBootIndex();
u32 bootInd = ContentLoader.GetTMD().GetBootIndex();
const DiscIO::SNANDContent* pContent = ContentLoader.GetContentByIndex(bootInd);
if (pContent)
{
@ -1275,10 +1144,13 @@ const DiscIO::CNANDContentLoader& ES::AccessContentDevice(u64 title_id)
return DiscIO::CNANDContentManager::Access().GetNANDLoader(title_id, Common::FROM_SESSION_ROOT);
}
u32 ES::ES_DIVerify(const std::vector<u8>& tmd)
u32 ES::ES_DIVerify(const IOS::ES::TMDReader& tmd)
{
if (!tmd.IsValid())
return -1;
u64 title_id = 0xDEADBEEFDEADBEEFull;
u64 tmd_title_id = Common::swap64(&tmd[0x18C]);
u64 tmd_title_id = tmd.GetTitleId();
DVDInterface::GetVolume().GetTitleID(&title_id);
if (title_id != tmd_title_id)
@ -1292,7 +1164,8 @@ u32 ES::ES_DIVerify(const std::vector<u8>& tmd)
if (!File::Exists(tmd_path))
{
File::IOFile tmd_file(tmd_path, "wb");
if (!tmd_file.WriteBytes(tmd.data(), tmd.size()))
const std::vector<u8>& tmd_bytes = tmd.GetRawTMD();
if (!tmd_file.WriteBytes(tmd_bytes.data(), tmd_bytes.size()))
ERROR_LOG(IOS_ES, "DIVerify failed to write disc TMD to NAND.");
}
DiscIO::cUIDsys uid_sys{Common::FromWhichRoot::FROM_SESSION_ROOT};

View File

@ -46,7 +46,7 @@ public:
void Close() override;
IPCCommandResult IOCtlV(const IOCtlVRequest& request) override;
static u32 ES_DIVerify(const std::vector<u8>& tmd);
static u32 ES_DIVerify(const IOS::ES::TMDReader& tmd);
// This should only be cleared on power reset
static std::string m_ContentFile;
@ -203,7 +203,7 @@ private:
u32 m_AccessIdentID = 0;
// For title installation (ioctls IOCTL_ES_ADDTITLE*).
TMDReader m_addtitle_tmd;
IOS::ES::TMDReader m_addtitle_tmd;
u32 m_addtitle_content_id = 0xFFFFFFFF;
std::vector<u8> m_addtitle_content_buffer;
};

View File

@ -5,17 +5,22 @@
#include "Core/IOS/ES/Formats.h"
#include <algorithm>
#include <cstddef>
#include <cstring>
#include <utility>
#include <vector>
#include "Common/ChunkFile.h"
#include "Common/CommonFuncs.h"
#include "Common/CommonTypes.h"
#include "Common/Crypto/AES.h"
namespace IOS
{
namespace HLE
namespace ES
{
constexpr size_t CONTENT_VIEW_SIZE = 0x10;
TMDReader::TMDReader(const std::vector<u8>& bytes) : m_bytes(bytes)
{
}
@ -36,13 +41,13 @@ void TMDReader::SetBytes(std::vector<u8>&& bytes)
bool TMDReader::IsValid() const
{
if (m_bytes.size() < 0x1E4)
if (m_bytes.size() < sizeof(TMDHeader))
{
// TMD is too small to contain its base fields.
return false;
}
if (m_bytes.size() < 0x1E4 + GetNumContents() * 36u)
if (m_bytes.size() < sizeof(TMDHeader) + GetNumContents() * sizeof(Content))
{
// TMD is too small to contain all its expected content entries.
return false;
@ -51,19 +56,69 @@ bool TMDReader::IsValid() const
return true;
}
const std::vector<u8>& TMDReader::GetRawTMD() const
{
return m_bytes;
}
std::vector<u8> TMDReader::GetRawHeader() const
{
return std::vector<u8>(m_bytes.begin(), m_bytes.begin() + sizeof(TMDHeader));
}
std::vector<u8> TMDReader::GetRawView() const
{
// Base fields
std::vector<u8> view(m_bytes.cbegin() + offsetof(TMDHeader, tmd_version),
m_bytes.cbegin() + offsetof(TMDHeader, access_rights));
const auto version = m_bytes.cbegin() + offsetof(TMDHeader, title_version);
view.insert(view.end(), version, version + sizeof(TMDHeader::title_version));
const auto num_contents = m_bytes.cbegin() + offsetof(TMDHeader, num_contents);
view.insert(view.end(), num_contents, num_contents + sizeof(TMDHeader::num_contents));
// Content views (same as Content, but without the hash)
for (size_t i = 0; i < GetNumContents(); ++i)
{
const auto content_iterator = m_bytes.cbegin() + sizeof(TMDHeader) + i * sizeof(Content);
view.insert(view.end(), content_iterator, content_iterator + CONTENT_VIEW_SIZE);
}
return view;
}
u16 TMDReader::GetBootIndex() const
{
return Common::swap16(m_bytes.data() + offsetof(TMDHeader, boot_index));
}
u64 TMDReader::GetIOSId() const
{
return Common::swap64(m_bytes.data() + 0x184);
return Common::swap64(m_bytes.data() + offsetof(TMDHeader, ios_id));
}
DiscIO::Region TMDReader::GetRegion() const
{
if (GetTitleId() == 0x0000000100000002)
return DiscIO::RegionSwitchWii(DiscIO::GetSysMenuRegion(GetTitleVersion()));
return DiscIO::RegionSwitchWii(static_cast<u8>(GetTitleId() & 0xff));
}
u64 TMDReader::GetTitleId() const
{
return Common::swap64(m_bytes.data() + 0x18C);
return Common::swap64(m_bytes.data() + offsetof(TMDHeader, title_id));
}
u16 TMDReader::GetTitleVersion() const
{
return Common::swap16(m_bytes.data() + offsetof(TMDHeader, title_version));
}
u16 TMDReader::GetNumContents() const
{
return Common::swap16(m_bytes.data() + 0x1DE);
return Common::swap16(m_bytes.data() + offsetof(TMDHeader, num_contents));
}
bool TMDReader::GetContent(u16 index, Content* content) const
@ -73,16 +128,24 @@ bool TMDReader::GetContent(u16 index, Content* content) const
return false;
}
const u8* content_base = m_bytes.data() + 0x1E4 + index * 36;
content->id = Common::swap32(content_base);
content->index = Common::swap16(content_base + 4);
content->type = Common::swap16(content_base + 6);
content->size = Common::swap64(content_base + 8);
std::copy(content_base + 16, content_base + 36, content->sha1.begin());
const u8* content_base = m_bytes.data() + sizeof(TMDHeader) + index * sizeof(Content);
content->id = Common::swap32(content_base + offsetof(Content, id));
content->index = Common::swap16(content_base + offsetof(Content, index));
content->type = Common::swap16(content_base + offsetof(Content, type));
content->size = Common::swap64(content_base + offsetof(Content, size));
std::copy_n(content_base + offsetof(Content, sha1), content->sha1.size(), content->sha1.begin());
return true;
}
std::vector<Content> TMDReader::GetContents() const
{
std::vector<Content> contents(GetNumContents());
for (size_t i = 0; i < contents.size(); ++i)
GetContent(static_cast<u16>(i), &contents[i]);
return contents;
}
bool TMDReader::FindContentById(u32 id, Content* content) const
{
for (u16 index = 0; index < GetNumContents(); ++index)
@ -103,5 +166,94 @@ void TMDReader::DoState(PointerWrap& p)
{
p.Do(m_bytes);
}
} // namespace HLE
TicketReader::TicketReader(const std::vector<u8>& bytes) : m_bytes(bytes)
{
}
TicketReader::TicketReader(std::vector<u8>&& bytes) : m_bytes(std::move(bytes))
{
}
void TicketReader::SetBytes(const std::vector<u8>& bytes)
{
m_bytes = bytes;
}
void TicketReader::SetBytes(std::vector<u8>&& bytes)
{
m_bytes = std::move(bytes);
}
bool TicketReader::IsValid() const
{
// Too small for the signature type.
if (m_bytes.size() < sizeof(u32))
return false;
u32 ticket_offset = GetOffset();
if (ticket_offset == 0)
return false;
// Too small for the ticket itself.
if (m_bytes.size() < ticket_offset + sizeof(Ticket))
return false;
return true;
}
u32 TicketReader::GetNumberOfTickets() const
{
return static_cast<u32>(m_bytes.size() / (GetOffset() + sizeof(Ticket)));
}
u32 TicketReader::GetOffset() const
{
u32 signature_type = Common::swap32(m_bytes.data());
if (signature_type == 0x10000) // RSA4096
return 576;
if (signature_type == 0x10001) // RSA2048
return 320;
if (signature_type == 0x10002) // ECDSA
return 128;
ERROR_LOG(COMMON, "Invalid ticket signature type: %08x", signature_type);
return 0;
}
const std::vector<u8>& TicketReader::GetRawTicket() const
{
return m_bytes;
}
std::vector<u8> TicketReader::GetRawTicketView(u32 ticket_num) const
{
// A ticket view is composed of a view ID + part of a ticket starting from the ticket_id field.
std::vector<u8> view{sizeof(TicketView)};
u32 view_id = Common::swap32(ticket_num);
std::memcpy(view.data(), &view_id, sizeof(view_id));
const size_t ticket_start = (GetOffset() + sizeof(Ticket)) * ticket_num;
const size_t view_start = ticket_start + offsetof(Ticket, ticket_id);
std::memcpy(view.data() + sizeof(view_id), &m_bytes[view_start], sizeof(view) - sizeof(view_id));
return view;
}
u64 TicketReader::GetTitleId() const
{
return Common::swap64(m_bytes.data() + GetOffset() + offsetof(Ticket, title_id));
}
std::vector<u8> TicketReader::GetTitleKey() const
{
const u8 common_key[16] = {0xeb, 0xe4, 0x2a, 0x22, 0x5e, 0x85, 0x93, 0xe4,
0x48, 0xd9, 0xc5, 0x45, 0x73, 0x81, 0xaa, 0xf7};
u8 iv[16] = {};
std::copy_n(&m_bytes[GetOffset() + offsetof(Ticket, title_id)], sizeof(Ticket::title_id), iv);
return Common::AES::Decrypt(common_key, iv, &m_bytes[GetOffset() + offsetof(Ticket, title_key)],
16);
}
} // namespace ES
} // namespace IOS

View File

@ -12,11 +12,95 @@
#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
#include "DiscIO/Enums.h"
namespace IOS
{
namespace HLE
namespace ES
{
#pragma pack(push, 4)
struct TMDHeader
{
u32 signature_type;
u8 rsa_2048_signature[256];
u8 fill[60];
u8 issuer[64];
u8 tmd_version;
u8 ca_crl_version;
u8 signer_crl_version;
u64 ios_id;
u64 title_id;
u32 title_type;
u16 group_id;
u16 zero;
u16 region;
u8 ratings[16];
u8 reserved[12];
u8 ipc_mask[12];
u8 reserved2[18];
u32 access_rights;
u16 title_version;
u16 num_contents;
u16 boot_index;
u16 fill2;
};
static_assert(sizeof(TMDHeader) == 0x1e4, "TMDHeader has the wrong size");
struct Content
{
u32 id;
u16 index;
u16 type;
u64 size;
std::array<u8, 20> sha1;
};
static_assert(sizeof(Content) == 36, "Content has the wrong size");
struct TimeLimit
{
u32 enabled;
u32 seconds;
};
struct TicketView
{
u32 view;
u64 ticket_id;
u32 device_id;
u64 title_id;
u16 access_mask;
u32 permitted_title_id;
u32 permitted_title_mask;
u8 title_export_allowed;
u8 common_key_index;
u8 unknown2[0x30];
u8 content_access_permissions[0x40];
TimeLimit time_limits[8];
};
static_assert(sizeof(TicketView) == 0xd8, "TicketView has the wrong size");
struct Ticket
{
u8 signature_issuer[0x40];
u8 ecdh_key[0x3c];
u8 unknown[0x03];
u8 title_key[0x10];
u64 ticket_id;
u32 device_id;
u64 title_id;
u16 access_mask;
u16 ticket_version;
u32 permitted_title_id;
u32 permitted_title_mask;
u8 title_export_allowed;
u8 common_key_index;
u8 unknown2[0x30];
u8 content_access_permissions[0x40];
TimeLimit time_limits[8];
};
static_assert(sizeof(Ticket) == 356, "Ticket has the wrong size");
#pragma pack(pop)
class TMDReader final
{
public:
@ -29,19 +113,20 @@ public:
bool IsValid() const;
u64 GetIOSId() const;
u64 GetTitleId() const;
// Returns the TMD or parts of it without any kind of parsing. Intended for use by ES.
const std::vector<u8>& GetRawTMD() const;
std::vector<u8> GetRawHeader() const;
std::vector<u8> GetRawView() const;
u16 GetBootIndex() const;
u64 GetIOSId() const;
DiscIO::Region GetRegion() const;
u64 GetTitleId() const;
u16 GetTitleVersion() const;
struct Content
{
u32 id;
u16 index;
u16 type;
u64 size;
std::array<u8, 20> sha1;
};
u16 GetNumContents() const;
bool GetContent(u16 index, Content* content) const;
std::vector<Content> GetContents() const;
bool FindContentById(u32 id, Content* content) const;
void DoState(PointerWrap& p);
@ -49,5 +134,34 @@ public:
private:
std::vector<u8> m_bytes;
};
} // namespace HLE
class TicketReader final
{
public:
TicketReader() = default;
explicit TicketReader(const std::vector<u8>& bytes);
explicit TicketReader(std::vector<u8>&& bytes);
void SetBytes(const std::vector<u8>& bytes);
void SetBytes(std::vector<u8>&& bytes);
bool IsValid() const;
const std::vector<u8>& GetRawTicket() const;
u32 GetNumberOfTickets() const;
u32 GetOffset() const;
// Returns a "raw" ticket view, without byte swapping. Intended for use from ES.
// Theoretically, a ticket file can contain one or more tickets. In practice, most (all?)
// official titles only have one ticket, but IOS *does* have code to handle ticket files with
// more than just one ticket and generate ticket views for them, so we implement it too.
std::vector<u8> GetRawTicketView(u32 ticket_num) const;
u64 GetTitleId() const;
std::vector<u8> GetTitleKey() const;
private:
std::vector<u8> m_bytes;
};
} // namespace ES
} // namespace IOS

View File

@ -658,7 +658,7 @@ void SetDefaultContentFile(const std::string& file_name)
es->LoadWAD(file_name);
}
void ES_DIVerify(const std::vector<u8>& tmd)
void ES_DIVerify(const ES::TMDReader& tmd)
{
Device::ES::ES_DIVerify(tmd);
}

View File

@ -16,6 +16,11 @@ class PointerWrap;
namespace IOS
{
namespace ES
{
class TMDReader;
}
namespace HLE
{
namespace Device
@ -61,7 +66,7 @@ void DoState(PointerWrap& p);
// Set default content file
void SetDefaultContentFile(const std::string& file_name);
void ES_DIVerify(const std::vector<u8>& tmd);
void ES_DIVerify(const ES::TMDReader& tmd);
void SDIO_EventNotify();

View File

@ -23,6 +23,7 @@
#include "Core/HW/DVDThread.h"
#include "Core/HW/Memmap.h"
#include "Core/HW/SystemTimers.h"
#include "Core/IOS/ES/Formats.h"
#include "Core/IOS/MIOS.h"
#include "Core/PowerPC/PPCSymbolDB.h"
#include "Core/PowerPC/PowerPC.h"
@ -97,7 +98,7 @@ static std::vector<u8> GetMIOSBinary()
if (!loader.IsValid())
return {};
const auto* content = loader.GetContentByIndex(loader.GetBootIndex());
const auto* content = loader.GetContentByIndex(loader.GetTMD().GetBootIndex());
if (!content)
return {};

View File

@ -109,14 +109,14 @@ IPCCommandResult WFSI::IOCtl(const IOCtlRequest& request)
Memory::CopyFromEmu(tmd_bytes.data(), tmd_addr, tmd_size);
m_tmd.SetBytes(std::move(tmd_bytes));
std::vector<u8> ticket = DiscIO::FindSignedTicket(m_tmd.GetTitleId());
if (ticket.size() == 0)
ES::TicketReader ticket = DiscIO::FindSignedTicket(m_tmd.GetTitleId());
if (!ticket.IsValid())
{
return_error_code = -11028;
break;
}
memcpy(m_aes_key, DiscIO::GetKeyFromTicket(ticket).data(), sizeof(m_aes_key));
memcpy(m_aes_key, ticket.GetTitleKey().data(), sizeof(m_aes_key));
mbedtls_aes_setkey_dec(&m_aes_ctx, m_aes_key, 128);
break;
@ -134,7 +134,7 @@ IPCCommandResult WFSI::IOCtl(const IOCtlRequest& request)
// Initializes the IV from the index of the content in the TMD contents.
u32 content_id = Memory::Read_U32(request.buffer_in + 8);
TMDReader::Content content_info;
ES::Content content_info;
if (!m_tmd.FindContentById(content_id, &content_info))
{
WARN_LOG(IOS, "%s: Content id %08x not found", ioctl_name, content_id);

View File

@ -50,7 +50,7 @@ private:
u8 m_aes_key[0x10] = {};
u8 m_aes_iv[0x10] = {};
TMDReader m_tmd;
IOS::ES::TMDReader m_tmd;
std::string m_base_extract_path;
ARCUnpacker m_arc_unpacker;

View File

@ -4,12 +4,12 @@
#include <algorithm>
#include <array>
#include <cinttypes>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <functional>
#include <map>
#include <mbedtls/aes.h>
#include <string>
#include <utility>
#include <vector>
@ -17,6 +17,7 @@
#include "Common/Align.h"
#include "Common/CommonFuncs.h"
#include "Common/CommonTypes.h"
#include "Common/Crypto/AES.h"
#include "Common/FileUtil.h"
#include "Common/Logging/Log.h"
#include "Common/MsgHandler.h"
@ -29,50 +30,6 @@
namespace DiscIO
{
namespace
{
// Strips the signature part of a ticket, which has variable size based on
// signature type. Returns a new vector which has only the ticket structure
// itself.
std::vector<u8> SignedTicketToTicket(const std::vector<u8>& signed_ticket)
{
u32 signature_type = Common::swap32(signed_ticket.data());
u32 entry_offset;
if (signature_type == 0x10000) // RSA4096
{
entry_offset = 576;
}
else if (signature_type == 0x10001) // RSA2048
{
entry_offset = 320;
}
else if (signature_type == 0x10002) // ECDSA
{
entry_offset = 128;
}
else
{
ERROR_LOG(DISCIO, "Invalid ticket signature type: %08x", signature_type);
return std::vector<u8>();
}
std::vector<u8> ticket(signed_ticket.size() - entry_offset);
std::copy(signed_ticket.begin() + entry_offset, signed_ticket.end(), ticket.begin());
return ticket;
}
std::vector<u8> AESDecode(const u8* key, u8* iv, const u8* src, u32 size)
{
mbedtls_aes_context aes_ctx;
std::vector<u8> buffer(size);
mbedtls_aes_setkey_dec(&aes_ctx, key, 128);
mbedtls_aes_crypt_cbc(&aes_ctx, MBEDTLS_AES_DECRYPT, size, iv, src, buffer.data());
return buffer;
}
}
CNANDContentData::~CNANDContentData() = default;
CSharedContent::CSharedContent(Common::FromWhichRoot root) : m_root(root)
@ -191,7 +148,6 @@ bool CNANDContentDataBuffer::GetRange(u32 start, u32 size, u8* buffer)
}
CNANDContentLoader::CNANDContentLoader(const std::string& content_name)
: m_Valid(false), m_IsWAD(false), m_TitleID(-1), m_IosVersion(0x09), m_BootIndex(-1)
{
m_Valid = Initialize(content_name);
}
@ -200,11 +156,16 @@ CNANDContentLoader::~CNANDContentLoader()
{
}
bool CNANDContentLoader::IsValid() const
{
return m_Valid && m_tmd.IsValid();
}
const SNANDContent* CNANDContentLoader::GetContentByIndex(int index) const
{
for (auto& Content : m_Content)
{
if (Content.m_Index == index)
if (Content.m_metadata.index == index)
{
return &Content;
}
@ -221,15 +182,12 @@ bool CNANDContentLoader::Initialize(const std::string& name)
WiiWAD wad(name);
std::vector<u8> data_app;
std::vector<u8> tmd;
std::vector<u8> decrypted_title_key;
if (wad.IsValid())
{
m_IsWAD = true;
m_Ticket = wad.GetTicket();
decrypted_title_key = GetKeyFromTicket(m_Ticket);
tmd = wad.GetTMD();
m_ticket = wad.GetTicket();
m_tmd = wad.GetTMD();
data_app = wad.GetDataApp();
}
else
@ -248,92 +206,64 @@ bool CNANDContentLoader::Initialize(const std::string& name)
return false;
}
tmd.resize(static_cast<size_t>(File::GetSize(tmd_filename)));
tmd_file.ReadBytes(tmd.data(), tmd.size());
std::vector<u8> bytes(File::GetSize(tmd_filename));
tmd_file.ReadBytes(bytes.data(), bytes.size());
m_tmd.SetBytes(std::move(bytes));
m_ticket = FindSignedTicket(m_tmd.GetTitleId());
}
std::copy(&tmd[0], &tmd[TMD_HEADER_SIZE], m_TMDHeader);
std::copy(&tmd[0x180], &tmd[0x180 + TMD_VIEW_SIZE], m_TMDView);
m_TitleVersion = Common::swap16(&tmd[0x01DC]);
m_NumEntries = Common::swap16(&tmd[0x01DE]);
m_BootIndex = Common::swap16(&tmd[0x01E0]);
m_TitleID = Common::swap64(&tmd[0x018C]);
m_IosVersion = Common::swap16(&tmd[0x018A]);
m_Country = static_cast<u8>(m_TitleID & 0xFF);
if (m_Country == 2) // SYSMENU
m_Country = GetSysMenuRegion(m_TitleVersion);
InitializeContentEntries(tmd, decrypted_title_key, data_app);
InitializeContentEntries(data_app);
return true;
}
void CNANDContentLoader::InitializeContentEntries(const std::vector<u8>& tmd,
const std::vector<u8>& decrypted_title_key,
const std::vector<u8>& data_app)
void CNANDContentLoader::InitializeContentEntries(const std::vector<u8>& data_app)
{
m_Content.resize(m_NumEntries);
if (!m_ticket.IsValid())
{
ERROR_LOG(IOS_ES, "No valid ticket for title %016" PRIx64, m_tmd.GetTitleId());
return;
}
const std::vector<IOS::ES::Content> contents = m_tmd.GetContents();
m_Content.resize(contents.size());
std::array<u8, 16> iv;
u32 data_app_offset = 0;
const std::vector<u8> title_key = m_ticket.GetTitleKey();
CSharedContent shared_content{Common::FromWhichRoot::FROM_SESSION_ROOT};
for (u32 i = 0; i < m_NumEntries; i++)
for (size_t i = 0; i < contents.size(); ++i)
{
const u32 entry_offset = 0x24 * i;
SNANDContent& content = m_Content[i];
content.m_ContentID = Common::swap32(&tmd[entry_offset + 0x01E4]);
content.m_Index = Common::swap16(&tmd[entry_offset + 0x01E8]);
content.m_Type = Common::swap16(&tmd[entry_offset + 0x01EA]);
content.m_Size = static_cast<u32>(Common::swap64(&tmd[entry_offset + 0x01EC]));
const auto header_begin = std::next(tmd.begin(), entry_offset + 0x01E4);
const auto header_end = std::next(header_begin, ArraySize(content.m_Header));
std::copy(header_begin, header_end, content.m_Header);
const auto hash_begin = std::next(tmd.begin(), entry_offset + 0x01F4);
const auto hash_end = std::next(hash_begin, ArraySize(content.m_SHA1Hash));
std::copy(hash_begin, hash_end, content.m_SHA1Hash);
const auto& content = contents.at(i);
if (m_IsWAD)
{
u32 rounded_size = Common::AlignUp(content.m_Size, 0x40);
// The content index is used as IV (2 bytes); the remaining 14 bytes are zeroes.
std::array<u8, 16> iv{};
iv[0] = static_cast<u8>(content.index >> 8) & 0xFF;
iv[1] = static_cast<u8>(content.index) & 0xFF;
iv.fill(0);
std::copy(&tmd[entry_offset + 0x01E8], &tmd[entry_offset + 0x01E8 + 2], iv.begin());
content.m_Data = std::make_unique<CNANDContentDataBuffer>(AESDecode(
decrypted_title_key.data(), iv.data(), &data_app[data_app_offset], rounded_size));
u32 rounded_size = Common::AlignUp(static_cast<u32>(content.size), 0x40);
m_Content[i].m_Data = std::make_unique<CNANDContentDataBuffer>(Common::AES::Decrypt(
title_key.data(), iv.data(), &data_app[data_app_offset], rounded_size));
data_app_offset += rounded_size;
continue;
}
else
{
std::string filename;
if (content.type & 0x8000) // shared app
filename = shared_content.GetFilenameFromSHA1(content.sha1.data());
else
filename = StringFromFormat("%s/%08x.app", m_Path.c_str(), content.id);
m_Content[i].m_Data = std::make_unique<CNANDContentDataFile>(filename);
}
std::string filename;
if (content.m_Type & 0x8000) // shared app
filename = shared_content.GetFilenameFromSHA1(content.m_SHA1Hash);
else
filename = StringFromFormat("%s/%08x.app", m_Path.c_str(), content.m_ContentID);
content.m_Data = std::make_unique<CNANDContentDataFile>(filename);
// Be graceful about incorrect TMDs.
if (File::Exists(filename))
content.m_Size = static_cast<u32>(File::GetSize(filename));
m_Content[i].m_metadata = std::move(content);
}
}
DiscIO::Region CNANDContentLoader::GetRegion() const
{
if (!IsValid())
return DiscIO::Region::UNKNOWN_REGION;
return RegionSwitchWii(m_Country);
}
CNANDContentManager::~CNANDContentManager()
{
}
@ -372,18 +302,18 @@ void CNANDContentManager::ClearCache()
void CNANDContentLoader::RemoveTitle() const
{
INFO_LOG(DISCIO, "RemoveTitle %08x/%08x", (u32)(m_TitleID >> 32), (u32)m_TitleID);
const u64 title_id = m_tmd.GetTitleId();
INFO_LOG(DISCIO, "RemoveTitle %08x/%08x", (u32)(title_id >> 32), (u32)title_id);
if (IsValid())
{
// remove TMD?
for (u32 i = 0; i < m_NumEntries; i++)
for (const auto& content : m_Content)
{
if (!(m_Content[i].m_Type & 0x8000)) // skip shared apps
if (!(content.m_metadata.type & 0x8000)) // skip shared apps
{
std::string filename =
StringFromFormat("%s/%08x.app", m_Path.c_str(), m_Content[i].m_ContentID);
INFO_LOG(DISCIO, "Delete %s", filename.c_str());
File::Delete(filename);
std::string path = StringFromFormat("%s/%08x.app", m_Path.c_str(), content.m_metadata.id);
INFO_LOG(DISCIO, "Delete %s", path.c_str());
File::Delete(path);
}
}
CNANDContentManager::Access().ClearCache(); // deletes 'this'
@ -470,7 +400,7 @@ u64 CNANDContentManager::Install_WiiWAD(const std::string& filename)
if (content_loader.IsValid() == false)
return 0;
u64 title_id = content_loader.GetTitleID();
const u64 title_id = content_loader.GetTMD().GetTitleId();
// copy WAD's TMD header and contents to content directory
@ -485,20 +415,17 @@ u64 CNANDContentManager::Install_WiiWAD(const std::string& filename)
return 0;
}
tmd_file.WriteBytes(content_loader.GetTMDHeader(), CNANDContentLoader::TMD_HEADER_SIZE);
const auto& raw_tmd = content_loader.GetTMD().GetRawTMD();
tmd_file.WriteBytes(raw_tmd.data(), raw_tmd.size());
CSharedContent shared_content{Common::FromWhichRoot::FROM_CONFIGURED_ROOT};
for (u32 i = 0; i < content_loader.GetContentSize(); i++)
for (const auto& content : content_loader.GetContent())
{
const SNANDContent& content = content_loader.GetContent()[i];
tmd_file.WriteBytes(content.m_Header, CNANDContentLoader::CONTENT_HEADER_SIZE);
std::string app_filename;
if (content.m_Type & 0x8000) // shared
app_filename = shared_content.AddSharedContent(content.m_SHA1Hash);
if (content.m_metadata.type & 0x8000) // shared
app_filename = shared_content.AddSharedContent(content.m_metadata.sha1.data());
else
app_filename = StringFromFormat("%s%08x.app", content_path.c_str(), content.m_ContentID);
app_filename = StringFromFormat("%s%08x.app", content_path.c_str(), content.m_metadata.id);
if (!File::Exists(app_filename))
{
@ -510,7 +437,7 @@ u64 CNANDContentManager::Install_WiiWAD(const std::string& filename)
return 0;
}
app_file.WriteBytes(content.m_Data->Get().data(), content.m_Size);
app_file.WriteBytes(content.m_Data->Get().data(), content.m_metadata.size);
}
else
{
@ -533,14 +460,14 @@ u64 CNANDContentManager::Install_WiiWAD(const std::string& filename)
return title_id;
}
bool AddTicket(const std::vector<u8>& signed_ticket)
bool AddTicket(const IOS::ES::TicketReader& signed_ticket)
{
std::vector<u8> ticket = SignedTicketToTicket(signed_ticket);
if (ticket.empty())
if (!signed_ticket.IsValid())
{
return false;
}
u64 title_id = Common::swap64(ticket.data() + 0x9c);
u64 title_id = signed_ticket.GetTitleId();
std::string ticket_filename = Common::GetTicketFileName(title_id, Common::FROM_CONFIGURED_ROOT);
File::CreateFullPath(ticket_filename);
@ -549,46 +476,25 @@ bool AddTicket(const std::vector<u8>& signed_ticket)
if (!ticket_file)
return false;
return ticket_file.WriteBytes(signed_ticket.data(), signed_ticket.size());
const std::vector<u8>& raw_ticket = signed_ticket.GetRawTicket();
return ticket_file.WriteBytes(raw_ticket.data(), raw_ticket.size());
}
std::vector<u8> FindSignedTicket(u64 title_id)
IOS::ES::TicketReader FindSignedTicket(u64 title_id)
{
std::string ticket_filename = Common::GetTicketFileName(title_id, Common::FROM_CONFIGURED_ROOT);
File::IOFile ticket_file(ticket_filename, "rb");
if (!ticket_file)
{
return std::vector<u8>();
return IOS::ES::TicketReader{};
}
std::vector<u8> signed_ticket(ticket_file.GetSize());
if (!ticket_file.ReadBytes(signed_ticket.data(), signed_ticket.size()))
{
return std::vector<u8>();
return IOS::ES::TicketReader{};
}
return signed_ticket;
return IOS::ES::TicketReader{std::move(signed_ticket)};
}
std::vector<u8> FindTicket(u64 title_id)
{
std::vector<u8> signed_ticket = FindSignedTicket(title_id);
if (signed_ticket.empty())
{
return std::vector<u8>();
}
return SignedTicketToTicket(signed_ticket);
}
std::vector<u8> GetKeyFromTicket(const std::vector<u8>& signed_ticket)
{
const u8 common_key[16] = {0xeb, 0xe4, 0x2a, 0x22, 0x5e, 0x85, 0x93, 0xe4,
0x48, 0xd9, 0xc5, 0x45, 0x73, 0x81, 0xaa, 0xf7};
u8 iv[16] = {};
std::copy(&signed_ticket[0x01DC], &signed_ticket[0x01DC + 8], iv);
return AESDecode(common_key, iv, &signed_ticket[0x01BF], 16);
}
} // namespace end

View File

@ -12,6 +12,7 @@
#include "Common/CommonTypes.h"
#include "Common/NandPaths.h"
#include "Core/IOS/ES/Formats.h"
namespace File
{
@ -22,10 +23,8 @@ namespace DiscIO
{
enum class Region;
bool AddTicket(const std::vector<u8>& signed_ticket);
std::vector<u8> FindSignedTicket(u64 title_id);
std::vector<u8> FindTicket(u64 title_id);
std::vector<u8> GetKeyFromTicket(const std::vector<u8>& ticket);
bool AddTicket(const IOS::ES::TicketReader& signed_ticket);
IOS::ES::TicketReader FindSignedTicket(u64 title_id);
class CNANDContentData
{
@ -67,13 +66,7 @@ private:
struct SNANDContent
{
u32 m_ContentID;
u16 m_Index;
u16 m_Type;
u32 m_Size;
u8 m_SHA1Hash[20];
u8 m_Header[36]; // all of the above
IOS::ES::Content m_metadata;
std::unique_ptr<CNANDContentData> m_Data;
};
@ -84,47 +77,21 @@ public:
explicit CNANDContentLoader(const std::string& content_name);
~CNANDContentLoader();
bool IsValid() const { return m_Valid; }
bool IsValid() const;
void RemoveTitle() const;
u64 GetTitleID() const { return m_TitleID; }
u16 GetIosVersion() const { return m_IosVersion; }
u32 GetBootIndex() const { return m_BootIndex; }
size_t GetContentSize() const { return m_Content.size(); }
const SNANDContent* GetContentByIndex(int index) const;
const u8* GetTMDView() const { return m_TMDView; }
const u8* GetTMDHeader() const { return m_TMDHeader; }
const std::vector<u8>& GetTicket() const { return m_Ticket; }
const IOS::ES::TMDReader& GetTMD() const { return m_tmd; }
const IOS::ES::TicketReader& GetTicket() const { return m_ticket; }
const std::vector<SNANDContent>& GetContent() const { return m_Content; }
u16 GetTitleVersion() const { return m_TitleVersion; }
u16 GetNumEntries() const { return m_NumEntries; }
DiscIO::Region GetRegion() const;
u8 GetCountryChar() const { return m_Country; }
enum
{
TMD_VIEW_SIZE = 0x58,
TMD_HEADER_SIZE = 0x1E4,
CONTENT_HEADER_SIZE = 0x24,
TICKET_SIZE = 0x2A4
};
private:
bool Initialize(const std::string& name);
void InitializeContentEntries(const std::vector<u8>& tmd,
const std::vector<u8>& decrypted_title_key,
const std::vector<u8>& data_app);
void InitializeContentEntries(const std::vector<u8>& data_app);
bool m_Valid;
bool m_IsWAD;
bool m_Valid = false;
bool m_IsWAD = false;
std::string m_Path;
u64 m_TitleID;
u16 m_IosVersion;
u32 m_BootIndex;
u16 m_NumEntries;
u16 m_TitleVersion;
u8 m_TMDView[TMD_VIEW_SIZE];
u8 m_TMDHeader[TMD_HEADER_SIZE];
std::vector<u8> m_Ticket;
u8 m_Country;
IOS::ES::TMDReader m_tmd;
IOS::ES::TicketReader m_ticket;
std::vector<SNANDContent> m_Content;
};

View File

@ -12,6 +12,7 @@
#include "Common/CommonFuncs.h"
#include "Common/CommonTypes.h"
#include "Common/StringUtil.h"
#include "Core/IOS/ES/Formats.h"
#include "DiscIO/Enums.h"
namespace DiscIO
@ -36,7 +37,7 @@ public:
}
virtual bool GetTitleID(u64*) const { return false; }
virtual std::vector<u8> GetTMD() const { return {}; }
virtual IOS::ES::TMDReader GetTMD() const { return {}; }
virtual u64 PartitionOffsetToRawOffset(u64 offset) const { return offset; }
virtual std::string GetGameID() const = 0;
virtual std::string GetMakerID() const = 0;

View File

@ -36,6 +36,16 @@ CVolumeWAD::CVolumeWAD(std::unique_ptr<IBlobReader> reader) : m_reader(std::move
Common::AlignUp(m_tick_size, 0x40);
m_opening_bnr_offset =
m_tmd_offset + Common::AlignUp(m_tmd_size, 0x40) + Common::AlignUp(m_data_size, 0x40);
if (m_tmd_size > 1024 * 1024 * 4)
{
ERROR_LOG(DISCIO, "TMD is too large: %u bytes", m_tmd_size);
return;
}
std::vector<u8> tmd_buffer(m_tmd_size);
Read(m_tmd_offset, m_tmd_size, tmd_buffer.data(), false);
m_tmd.SetBytes(std::move(tmd_buffer));
}
CVolumeWAD::~CVolumeWAD()
@ -55,40 +65,26 @@ bool CVolumeWAD::Read(u64 offset, u64 length, u8* buffer, bool decrypt) const
Region CVolumeWAD::GetRegion() const
{
u8 country_code;
if (!Read(m_tmd_offset + 0x0193, 1, &country_code))
if (!m_tmd.IsValid())
return Region::UNKNOWN_REGION;
return RegionSwitchWii(country_code);
return m_tmd.GetRegion();
}
Country CVolumeWAD::GetCountry() const
{
// read the last digit of the titleID in the ticket
u8 country_code;
if (!Read(m_tmd_offset + 0x0193, 1, &country_code))
if (!m_tmd.IsValid())
return Country::COUNTRY_UNKNOWN;
u8 country_code = static_cast<u8>(m_tmd.GetTitleId() & 0xff);
if (country_code == 2) // SYSMENU
{
u16 title_version = 0;
Read(m_tmd_offset + 0x01dc, 2, (u8*)&title_version);
country_code = GetSysMenuRegion(Common::swap16(title_version));
}
country_code = GetSysMenuRegion(m_tmd.GetTitleVersion());
return CountrySwitch(country_code);
}
std::vector<u8> CVolumeWAD::GetTMD() const
IOS::ES::TMDReader CVolumeWAD::GetTMD() const
{
if (m_tmd_size > 1024 * 1024 * 4)
{
ERROR_LOG(DISCIO, "TMD is too large: %u bytes", m_tmd_size);
return {};
}
std::vector<u8> buffer(m_tmd_size);
Read(m_tmd_offset, m_tmd_size, buffer.data(), false);
return buffer;
return m_tmd;
}
std::string CVolumeWAD::GetGameID() const
@ -125,11 +121,10 @@ bool CVolumeWAD::GetTitleID(u64* buffer) const
u16 CVolumeWAD::GetRevision() const
{
u16 revision;
if (!m_reader->Read(m_tmd_offset + 0x1dc, 2, (u8*)&revision))
if (!m_tmd.IsValid())
return 0;
return Common::swap16(revision);
return m_tmd.GetTitleVersion();
}
Platform CVolumeWAD::GetVolumeType() const

View File

@ -10,6 +10,7 @@
#include <vector>
#include "Common/CommonTypes.h"
#include "Core/IOS/ES/Formats.h"
#include "DiscIO/Volume.h"
// --- this volume type is used for Wad files ---
@ -32,7 +33,7 @@ public:
~CVolumeWAD();
bool Read(u64 offset, u64 length, u8* buffer, bool decrypt = false) const override;
bool GetTitleID(u64* buffer) const override;
std::vector<u8> GetTMD() const override;
IOS::ES::TMDReader GetTMD() const override;
std::string GetGameID() const override;
std::string GetMakerID() const override;
u16 GetRevision() const override;
@ -51,6 +52,7 @@ public:
private:
std::unique_ptr<IBlobReader> m_reader;
IOS::ES::TMDReader m_tmd;
u32 m_offset = 0;
u32 m_tmd_offset = 0;
u32 m_opening_bnr_offset = 0;

View File

@ -114,7 +114,7 @@ bool CVolumeWiiCrypted::GetTitleID(u64* buffer) const
return true;
}
std::vector<u8> CVolumeWiiCrypted::GetTMD() const
IOS::ES::TMDReader CVolumeWiiCrypted::GetTMD() const
{
u32 tmd_size;
u32 tmd_address;
@ -137,7 +137,7 @@ std::vector<u8> CVolumeWiiCrypted::GetTMD() const
std::vector<u8> buffer(tmd_size);
Read(m_VolumeOffset + tmd_address, tmd_size, buffer.data(), false);
return buffer;
return IOS::ES::TMDReader{std::move(buffer)};
}
u64 CVolumeWiiCrypted::PartitionOffsetToRawOffset(u64 offset) const

View File

@ -11,6 +11,7 @@
#include <vector>
#include "Common/CommonTypes.h"
#include "Core/IOS/ES/Formats.h"
#include "DiscIO/Volume.h"
// --- this volume type is used for encrypted Wii images ---
@ -32,7 +33,7 @@ public:
~CVolumeWiiCrypted();
bool Read(u64 _Offset, u64 _Length, u8* _pBuffer, bool decrypt) const override;
bool GetTitleID(u64* buffer) const override;
std::vector<u8> GetTMD() const override;
IOS::ES::TMDReader GetTMD() const override;
u64 PartitionOffsetToRawOffset(u64 offset) const override;
std::string GetGameID() const override;
std::string GetMakerID() const override;

View File

@ -88,9 +88,9 @@ bool WiiWAD::ParseWAD(IBlobReader& reader)
u32 offset = 0x40;
m_certificate_chain = CreateWADEntry(reader, certificate_chain_size, offset);
offset += Common::AlignUp(certificate_chain_size, 0x40);
m_ticket = CreateWADEntry(reader, ticket_size, offset);
m_ticket.SetBytes(CreateWADEntry(reader, ticket_size, offset));
offset += Common::AlignUp(ticket_size, 0x40);
m_tmd = CreateWADEntry(reader, tmd_size, offset);
m_tmd.SetBytes(CreateWADEntry(reader, tmd_size, offset));
offset += Common::AlignUp(tmd_size, 0x40);
m_data_app = CreateWADEntry(reader, data_app_size, offset);
offset += Common::AlignUp(data_app_size, 0x40);

View File

@ -8,6 +8,7 @@
#include <vector>
#include "Common/CommonTypes.h"
#include "Core/IOS/ES/Formats.h"
namespace DiscIO
{
@ -22,8 +23,8 @@ public:
bool IsValid() const { return m_valid; }
const std::vector<u8>& GetCertificateChain() const { return m_certificate_chain; }
const std::vector<u8>& GetTicket() const { return m_ticket; }
const std::vector<u8>& GetTMD() const { return m_tmd; }
const IOS::ES::TicketReader& GetTicket() const { return m_ticket; }
const IOS::ES::TMDReader& GetTMD() const { return m_tmd; }
const std::vector<u8>& GetDataApp() const { return m_data_app; }
const std::vector<u8>& GetFooter() const { return m_footer; }
private:
@ -32,8 +33,8 @@ private:
bool m_valid;
std::vector<u8> m_certificate_chain;
std::vector<u8> m_ticket;
std::vector<u8> m_tmd;
IOS::ES::TicketReader m_ticket;
IOS::ES::TMDReader m_tmd;
std::vector<u8> m_data_app;
std::vector<u8> m_footer;
};

View File

@ -1225,23 +1225,7 @@ void CFrame::OnInstallWAD(wxCommandEvent& event)
void CFrame::UpdateLoadWiiMenuItem() const
{
auto* const menu_item = GetMenuBar()->FindItem(IDM_LOAD_WII_MENU);
const auto& sys_menu_loader = DiscIO::CNANDContentManager::Access().GetNANDLoader(
TITLEID_SYSMENU, Common::FROM_CONFIGURED_ROOT);
if (sys_menu_loader.IsValid())
{
const int version = sys_menu_loader.GetTitleVersion();
const char region = sys_menu_loader.GetCountryChar();
menu_item->Enable();
menu_item->SetItemLabel(wxString::Format(_("Load Wii System Menu %d%c"), version, region));
}
else
{
menu_item->Enable(false);
menu_item->SetItemLabel(_("Load Wii System Menu"));
}
GetMenuBar()->Refresh(true, nullptr);
}
void CFrame::OnFifoPlayer(wxCommandEvent& WXUNUSED(event))

View File

@ -182,7 +182,7 @@ void InfoPanel::LoadISODetails()
m_fst->SetValue(StrToWxStr(std::to_string(m_opened_iso->GetFSTSize())));
if (m_ios_version)
{
IOS::HLE::TMDReader tmd{m_opened_iso->GetTMD()};
const IOS::ES::TMDReader tmd = m_opened_iso->GetTMD();
if (tmd.IsValid())
m_ios_version->SetValue(StringFromFormat("IOS%u", static_cast<u32>(tmd.GetIOSId())));
}
@ -223,7 +223,7 @@ wxStaticBoxSizer* InfoPanel::CreateISODetailsSizer()
{_("Apploader Date:"), m_date},
{_("FST Size:"), m_fst},
}};
if (!m_opened_iso->GetTMD().empty())
if (m_opened_iso->GetTMD().IsValid())
controls.emplace_back(_("IOS Version:"), m_ios_version);
const int space_10 = FromDIP(10);

View File

@ -12,6 +12,7 @@
#include "Core/Core.h"
#include "Core/PowerPC/PowerPC.h"
#include "Core/State.h"
#include "DiscIO/Enums.h"
#include "DiscIO/NANDContentLoader.h"
#include "DolphinWX/Globals.h"
#include "DolphinWX/WxUtils.h"
@ -546,8 +547,8 @@ void MainMenuBar::RefreshWiiSystemMenuLabel() const
if (sys_menu_loader.IsValid())
{
const auto sys_menu_version = sys_menu_loader.GetTitleVersion();
const auto sys_menu_region = sys_menu_loader.GetCountryChar();
const u16 sys_menu_version = sys_menu_loader.GetTMD().GetTitleVersion();
const char sys_menu_region = DiscIO::GetSysMenuRegion(sys_menu_version);
item->Enable();
item->SetItemLabel(
wxString::Format(_("Load Wii System Menu %u%c"), sys_menu_version, sys_menu_region));