[libromdata] WiiTMD: New RomData parser for Wii and Wii U title metadata.

Possibly for other Nintendo systems too, e.g. DSi and 3DS.

Currently displays the following fields:
- Title ID
- Issuer
- Title version
- OS version (if non-zero) (identifies IOS and IOSU)
- Access rights (for Wii and Wii U only; but not Wii IOS)

wii_structs.h: Some updates for TMD.

wiiu_structs.h: Add CMD v1 structs.

[xdg] Add WiiTMD with "application/x-nintendo-tmd".
This commit is contained in:
David Korth 2024-03-11 20:01:41 -04:00
parent c068c83522
commit d59f9cefc0
8 changed files with 534 additions and 25 deletions

View File

@ -35,6 +35,7 @@ SET(${PROJECT_NAME}_SRCS
Console/WiiCommon.cpp
Console/WiiSave.cpp
Console/WiiTicket.cpp
Console/WiiTMD.cpp
Console/WiiU.cpp
Console/WiiWAD.cpp
Console/WiiWAD_ops.cpp
@ -167,6 +168,7 @@ SET(${PROJECT_NAME}_H
Console/SufamiTurbo.hpp
Console/WiiCommon.hpp
Console/WiiTicket.hpp
Console/WiiTMD.hpp
Console/WiiSave.hpp
Console/WiiU.hpp
Console/WiiWAD.hpp

View File

@ -0,0 +1,429 @@
/***************************************************************************
* ROM Properties Page shell extension. (libromdata) *
* WiiTMD.hpp: Nintendo Wii (and Wii U) title metadata reader. *
* *
* Copyright (c) 2016-2024 by David Korth. *
* SPDX-License-Identifier: GPL-2.0-or-later *
***************************************************************************/
#include "stdafx.h"
#include "WiiTMD.hpp"
#include "wii_structs.h"
#include "wiiu_structs.h"
// Other rom-properties libraries
using namespace LibRpBase;
using namespace LibRpFile;
using namespace LibRpText;
// C++ STL classes
using std::string;
using std::vector;
namespace LibRomData {
class WiiTMDPrivate final : public RomDataPrivate
{
public:
WiiTMDPrivate(const IRpFilePtr &file);
private:
typedef RomDataPrivate super;
RP_DISABLE_COPY(WiiTMDPrivate)
public:
/** RomDataInfo **/
static const char *const exts[];
static const char *const mimeTypes[];
static const RomDataInfo romDataInfo;
public:
// TMD header
RVL_TMD_Header tmdHeader;
};
ROMDATA_IMPL(WiiTMD)
/** WiiTMDPrivate **/
/* RomDataInfo */
const char *const WiiTMDPrivate::exts[] = {
".tmd",
nullptr
};
const char *const WiiTMDPrivate::mimeTypes[] = {
// Unofficial MIME types.
// TODO: Get these upstreamed on FreeDesktop.org.
"application/x-nintendo-tmd",
nullptr
};
const RomDataInfo WiiTMDPrivate::romDataInfo = {
"WiiTMD", exts, mimeTypes
};
WiiTMDPrivate::WiiTMDPrivate(const IRpFilePtr &file)
: super(file, &romDataInfo)
{
// Clear the TMD header struct.
memset(&tmdHeader, 0, sizeof(tmdHeader));
}
/** WiiTMD **/
/**
* Read a Nintendo Wii (or Wii U) ticket file. (.tik)
*
* A ROM image must be opened by the caller. The file handle
* will be ref()'d and must be kept open in order to load
* data from the ROM image.
*
* To close the file, either delete this object or call close().
*
* NOTE: Check isValid() to determine if this is a valid ROM.
*
* @param file Open ROM image.
*/
WiiTMD::WiiTMD(const IRpFilePtr &file)
: super(new WiiTMDPrivate(file))
{
RP_D(WiiTMD);
d->mimeType = WiiTMDPrivate::mimeTypes[0]; // unofficial
d->fileType = FileType::MetadataFile;
if (!d->file) {
// Could not ref() the file handle.
return;
}
// Read the ticket. (either v0 or v1, depending on how much was read)
d->file->rewind();
size_t size = d->file->read(&d->tmdHeader, sizeof(d->tmdHeader));
if (size != sizeof(_RVL_TMD_Header)) {
// Ticket is too small.
d->file.reset();
return;
}
// Check if this ticket is supported.
const char *const filename = file->filename();
const DetectInfo info = {
{0, sizeof(d->tmdHeader), reinterpret_cast<const uint8_t*>(&d->tmdHeader)},
FileSystem::file_ext(filename), // ext
d->file->size() // szFile
};
d->isValid = (isRomSupported_static(&info) >= 0);
if (!d->isValid) {
d->file.reset();
}
}
/**
* Is a ROM image supported by this class?
* @param info DetectInfo containing ROM detection information.
* @return Class-specific system ID (>= 0) if supported; -1 if not.
*/
int WiiTMD::isRomSupported_static(const DetectInfo *info)
{
assert(info != nullptr);
assert(info->header.pData != nullptr);
assert(info->header.addr == 0);
if (!info || !info->ext || !info->header.pData ||
info->header.addr != 0 ||
info->header.size < sizeof(RVL_TMD_Header))
{
// Either no detection information was specified,
// or the header is too small.
return -1;
}
// NOTE: File extension must match.
bool ok = false;
for (const char *const *ext = WiiTMDPrivate::exts;
*ext != nullptr; ext++)
{
if (!strcasecmp(info->ext, *ext)) {
// File extension is supported.
ok = true;
break;
}
}
if (!ok) {
// File extension doesn't match.
return -1;
}
// Compare the TMD version to the file size.
const RVL_TMD_Header *const tmdHeader = reinterpret_cast<const RVL_TMD_Header*>(info->header.pData);
switch (tmdHeader->tmd_format_version) {
default:
// Unsupported ticket version.
return -1;
case 0:
// TODO: Calculate the actual CMD size.
if (info->szFile < static_cast<off64_t>(
sizeof(RVL_TMD_Header) +
sizeof(RVL_Content_Entry)))
{
// Incorrect file size.
return -1;
}
break;
case 1:
// TODO: Calculate the actual CMD size.
if (info->szFile < static_cast<off64_t>(
sizeof(RVL_TMD_Header) +
sizeof(WUP_CMD_GroupHeader) +
sizeof(WUP_CMD_GroupEntry) +
sizeof(WUP_Content_Entry)))
{
// Incorrect file size.
// TODO: Allow larger tickets?
return -1;
}
break;
}
// Validate the ticket signature format.
switch (be32_to_cpu(tmdHeader->signature_type)) {
default:
// Unsupported signature format.
return -1;
case RVL_CERT_SIGTYPE_RSA2048_SHA1:
// RSA-2048 with SHA-1 (Wii, DSi)
break;
case WUP_CERT_SIGTYPE_RSA2048_SHA256:
case WUP_CERT_SIGTYPE_RSA2048_SHA256 | WUP_CERT_SIGTYPE_FLAG_DISC:
// RSA-2048 with SHA-256 (Wii U, 3DS)
// NOTE: Requires TMD format v1 or later.
if (tmdHeader->tmd_format_version < 1)
return -1;
break;
}
// Certificate issuer must start with "Root-".
if (memcmp(tmdHeader->signature_issuer, "Root-", 5) != 0) {
// Incorrect issuer.
return -1;
}
// This appears to be a valid Nintendo title metadata.
return 0;
}
/**
* Get the name of the system the loaded ROM is designed for.
* @param type System name type. (See the SystemName enum.)
* @return System name, or nullptr if type is invalid.
*/
const char *WiiTMD::systemName(unsigned int type) const
{
RP_D(const WiiTMD);
if (!d->isValid || !isSystemNameTypeValid(type))
return nullptr;
// GBA has the same name worldwide, so we can
// ignore the region selection.
// TODO: Abbreviation might be different... (Japan uses AGB?)
static_assert(SYSNAME_TYPE_MASK == 3,
"WiiTMD::systemName() array index optimization needs to be updated.");
// Use the title ID to determine the system.
static const char *const sysNames[8][4] = {
{"Nintendo Wii", "Wii", "Wii", nullptr}, // Wii IOS
{"Nintendo Wii", "Wii", "Wii", nullptr}, // Wii
{"GBA NetCard", "NetCard", "NetCard", nullptr}, // GBA NetCard
{"Nintendo DSi", "DSi", "DSi", nullptr}, // DSi
{"Nintendo 3DS", "3DS", "3DS", nullptr}, // 3DS
{"Nintendo Wii U", "Wii U", "Wii U", nullptr}, // Wii U
{nullptr, nullptr, nullptr, nullptr}, // unused
{"Nintendo Wii U", "Wii U", "Wii U", nullptr}, // Wii U (vWii)
};
const unsigned int sysID = be16_to_cpu(d->tmdHeader.title_id.sysID);
return (likely(sysID < ARRAY_SIZE(sysNames)))
? sysNames[sysID][type & SYSNAME_TYPE_MASK]
: nullptr;
}
/**
* Load field data.
* Called by RomData::fields() if the field data hasn't been loaded yet.
* @return Number of fields read on success; negative POSIX error code on error.
*/
int WiiTMD::loadFieldData(void)
{
RP_D(WiiTMD);
if (!d->fields.empty()) {
// Field data *has* been loaded...
return 0;
} else if (!d->file) {
// File isn't open.
return -EBADF;
} else if (!d->isValid) {
// TMD isn't valid.
return -EIO;
}
// TMD header is read in the constructor.
const RVL_TMD_Header *const tmdHeader = &d->tmdHeader;
d->fields.reserve(4); // Maximum of 4 fields.
// Title ID
char s_title_id[24];
snprintf(s_title_id, sizeof(s_title_id), "%08X-%08X",
be32_to_cpu(tmdHeader->title_id.hi),
be32_to_cpu(tmdHeader->title_id.lo));
d->fields.addField_string(C_("Nintendo", "Title ID"), s_title_id, RomFields::STRF_MONOSPACE);
// Issuer
d->fields.addField_string(C_("Nintendo", "Issuer"),
latin1_to_utf8(tmdHeader->signature_issuer, sizeof(tmdHeader->signature_issuer)),
RomFields::STRF_MONOSPACE | RomFields::STRF_TRIM_END);
// Title version
// TODO: Might be different on 3DS?
const unsigned int title_version = be16_to_cpu(tmdHeader->title_version);
d->fields.addField_string(C_("Nintendo", "Title Version"),
rp_sprintf("%u.%u (v%u)", title_version >> 8, title_version & 0xFF, title_version));
// OS version (if non-zero)
const Nintendo_TitleID_BE_t os_tid = tmdHeader->sys_version;
const unsigned int sysID = be16_to_cpu(os_tid.sysID);
if (os_tid.id != 0) {
// OS display depends on the system ID.
char buf[24];
buf[0] = '\0';
switch (sysID) {
default:
break;
case NINTENDO_SYSID_IOS: {
// Wii (IOS)
if (be32_to_cpu(os_tid.hi) != 1)
break;
// IOS slots
const uint32_t tid_lo = be32_to_cpu(os_tid.lo);
switch (tid_lo) {
case 1:
strcpy(buf, "boot2");
break;
case 2:
// TODO: Localize this?
strcpy(buf, "System Menu");
break;
case 256:
strcpy(buf, "BC");
break;
case 257:
strcpy(buf, "MIOS");
break;
case 512:
strcpy(buf, "BC-NAND");
break;
case 513:
strcpy(buf, "BC-WFS");
break;
default:
if (tid_lo < 256) {
snprintf(buf, sizeof(buf), "IOS%u", tid_lo);
}
break;
}
break;
}
case NINTENDO_SYSID_WUP: {
// Wii U (IOSU)
// TODO: Add pre-release versions.
if (be32_to_cpu(os_tid.hi) != 0x00050010)
break;
const uint32_t tid_lo = be32_to_cpu(os_tid.lo);
if ((tid_lo & 0xFFFF3F00) != 0x10000000) {
// Not an IOSU title.
// tid_lo should be:
// - 0x100040xx for NDEBUG
// - 0x100080xx for DEBUG
break;
}
const unsigned int debug_flag = (tid_lo & 0xC000);
if (debug_flag != 0x4000 && debug_flag != 0x8000) {
// Incorrect debug flag.
break;
}
snprintf(buf, sizeof(buf), "OSv%u %s", (tid_lo & 0xFF),
(likely(debug_flag == 0x4000)) ? "NDEBUG" : "DEBUG");
break;
}
}
if (unlikely(buf[0] == '\0')) {
// Print the OS title ID.
snprintf(buf, sizeof(buf), "%08X-%08X",
be32_to_cpu(os_tid.hi),
be32_to_cpu(os_tid.lo));
}
d->fields.addField_string(C_("RomData", "OS Version"), buf);
}
// Access rights
if (sysID == NINTENDO_SYSID_WII || sysID == NINTENDO_SYSID_WUP) {
vector<string> *const v_access_rights_hdr = new vector<string>();
v_access_rights_hdr->reserve(2);
v_access_rights_hdr->emplace_back("AHBPROT");
v_access_rights_hdr->emplace_back(C_("Wii", "DVD Video"));
d->fields.addField_bitfield(C_("Wii", "Access Rights"),
v_access_rights_hdr, 0, be32_to_cpu(tmdHeader->access_rights));
}
// TODO: Region code, if available?
// Finished reading the field data.
return static_cast<int>(d->fields.count());
}
/**
* Load metadata properties.
* Called by RomData::metaData() if the metadata hasn't been loaded yet.
* @return Number of metadata properties read on success; negative POSIX error code on error.
*/
int WiiTMD::loadMetaData(void)
{
RP_D(WiiTMD);
if (d->metaData != nullptr) {
// Metadata *has* been loaded...
return 0;
} else if (!d->file) {
// File isn't open.
return -EBADF;
} else if (!d->isValid) {
// TMD isn't valid.
return -EIO;
}
// Create the metadata object.
d->metaData = new RomMetaData();
d->metaData->reserve(1); // Maximum of 1 metadata property.
// TMD header is read in the constructor.
const RVL_TMD_Header *const tmdHeader = &d->tmdHeader;
// Title ID (using as Title)
char s_title_id[24];
snprintf(s_title_id, sizeof(s_title_id), "%08X-%08X",
be32_to_cpu(tmdHeader->title_id.hi),
be32_to_cpu(tmdHeader->title_id.lo));
d->metaData->addMetaData_string(Property::Title, s_title_id);
// Finished reading the metadata.
return static_cast<int>(d->metaData->count());
}
}

View File

@ -0,0 +1,19 @@
/***************************************************************************
* ROM Properties Page shell extension. (libromdata) *
* WiiTMD.hpp: Nintendo Wii (and Wii U) title metadata reader. *
* *
* Copyright (c) 2016-2024 by David Korth. *
* SPDX-License-Identifier: GPL-2.0-or-later *
***************************************************************************/
#pragma once
#include "librpbase/RomData.hpp"
namespace LibRomData {
ROMDATA_DECL_BEGIN(WiiTMD)
ROMDATA_DECL_METADATA()
ROMDATA_DECL_END()
}

View File

@ -159,7 +159,7 @@ ASSERT_STRUCT(RVL_TimeLimit, 2*sizeof(uint32_t));
*/
#pragma pack(1)
typedef struct PACKED _RVL_Ticket {
uint32_t signature_type; // [0x000] Always 0x10001 for RSA-2048.
uint32_t signature_type; // [0x000] Signature type
uint8_t signature[0x100]; // [0x004] Signature
uint8_t padding_sig[0x3C]; // [0x104] Padding (always 0)
@ -244,42 +244,48 @@ typedef struct PACKED _RVL_Ticket_V1 {
ASSERT_STRUCT(RVL_Ticket_V1, 0x350);
/**
* Wii TMD header.
* Reference: https://wiibrew.org/wiki/Tmd_file_structure
* Wii TMD header
* References:
* - https://wiibrew.org/wiki/Title_metadata
* - https://wiiubrew.org/wiki/Title_metadata
*
* All fields are big-endian.
*/
#pragma pack(1)
typedef struct PACKED _RVL_TMD_Header {
uint32_t signature_type; // [0x000] Always 0x10001 for RSA-2048.
uint8_t signature[0x100]; // [0x004] Signature.
uint8_t padding_sig[0x3C]; // [0x104] Padding. (always 0)
uint32_t signature_type; // [0x000] Signature type
uint8_t signature[0x100]; // [0x004] Signature
uint8_t padding_sig[0x3C]; // [0x104] Padding (always 0)
// The following fields are all covered by the above signature.
char signature_issuer[0x40]; // [0x140] Signature issuer.
uint8_t version; // [0x180] Version.
uint8_t ca_crl_version; // [0x181] CA CRL version.
uint8_t signer_crl_version; // [0x182] Signer CRL version.
char signature_issuer[0x40]; // [0x140] Signature issuer
uint8_t tmd_format_version; // [0x180] TMD format version (v0 for Wii; v1 for Wii U)
uint8_t ca_crl_version; // [0x181] CA CRL version
uint8_t signer_crl_version; // [0x182] Signer CRL version
uint8_t padding1; // [0x183]
Nintendo_TitleID_BE_t sys_version; // [0x184] System version. (IOS title ID)
Nintendo_TitleID_BE_t title_id; // [0x18C] Title ID.
uint32_t title_type; // [0x194] Title type.
uint16_t group_id; // [0x198] Group ID.
Nintendo_TitleID_BE_t sys_version; // [0x184] System version [IOS(U) title ID]
Nintendo_TitleID_BE_t title_id; // [0x18C] Title ID
uint32_t title_type; // [0x194] Title type
uint16_t group_id; // [0x198] Group ID
uint16_t reserved1; // [0x19A]
// region_code and ratings are NOT valid for discs.
// They're only valid for WiiWare.
uint16_t region_code; // [0x19C] Region code. (See GCN_Region_Code.)
uint8_t ratings[0x10]; // [0x19E] Country-specific age ratings.
uint16_t region_code; // [0x19C] Region code (See GCN_Region_Code)
uint8_t ratings[0x10]; // [0x19E] Country-specific age ratings
uint8_t reserved3[12]; // [0x1AE]
uint8_t ipc_mask[12]; // [0x1BA] IPC mask.
uint8_t ipc_mask[12]; // [0x1BA] IPC mask
uint8_t reserved4[18]; // [0x1C6]
uint32_t access_rights; // [0x1D8] Access rights. (See RVL_Access_Rights_e.)
uint16_t title_version; // [0x1DC] Title version.
uint16_t nbr_cont; // [0x1DE] Number of contents.
uint16_t boot_index; // [0x1E0] Boot index.
uint32_t access_rights; // [0x1D8] Access rights (See RVL_Access_Rights_e)
uint16_t title_version; // [0x1DC] Title version
uint16_t nbr_cont; // [0x1DE] Number of contents
uint16_t boot_index; // [0x1E0] Boot index
uint8_t padding2[2]; // [0x1E2]
// Following this header is a variable-length content table.
// Following this header is:
// - v0: Content table (length indicated by nbr_cont)
// - v1: CMD group header
} RVL_TMD_Header;
ASSERT_STRUCT(RVL_TMD_Header, 0x1E4);
#pragma pack()
@ -293,8 +299,10 @@ typedef enum {
} RVL_Access_Rights_e;
/**
* Wii content entry. (Stored after the TMD.)
* Wii content entry (Stored after the TMD) (v0)
* Reference: https://wiibrew.org/wiki/Title_metadata
*
* All fields are big-endian.
*/
#pragma pack(1)
typedef struct PACKED _RVL_Content_Entry {
@ -304,7 +312,7 @@ typedef struct PACKED _RVL_Content_Entry {
uint64_t size; // [0x008] Size
uint8_t sha1_hash[20]; // [0x010] SHA-1 hash of the content (installed) or H3 table (disc).
} RVL_Content_Entry;
ASSERT_STRUCT(RVL_Content_Entry, 0x24);
ASSERT_STRUCT(RVL_Content_Entry, 36);
#pragma pack()
/**

View File

@ -2,7 +2,7 @@
* ROM Properties Page shell extension. (libromdata) *
* wiiu_structs.h: Nintendo Wii U data structures. *
* *
* Copyright (c) 2016-2023 by David Korth. *
* Copyright (c) 2016-2024 by David Korth. *
* SPDX-License-Identifier: GPL-2.0-or-later *
***************************************************************************/
@ -50,6 +50,47 @@ ASSERT_STRUCT(WiiU_DiscHeader, 22);
// Secondary Wii U disc magic at 0x10000.
#define WIIU_SECONDARY_MAGIC 0xCC549EB9
/**
* Wii U CMD group entry (for v1 TMD)
*
* All fields are big-endian.
*/
typedef struct _WUP_CMD_GroupEntry {
uint16_t offset; // [0x000] Offset of the CMD group
uint16_t nbr_cont; // [0x002] Number of CMDs in the group
uint8_t sha256_hash[32]; // [0x004] SHA-256 hash of the CMDs in the group
} WUP_CMD_GroupEntry;
ASSERT_STRUCT(WUP_CMD_GroupEntry, 36);
/**
* Wii U CMD group header (for v1 TMD)
*
* All fields are big-endian.
*/
#pragma pack(1)
typedef struct _WUP_CMD_GroupHeader {
uint8_t sha256_hash[32]; // [0x000] SHA-256 hash of CMD groups
WUP_CMD_GroupEntry entries[64]; // [0x020] Up to 64 CMD group entries
} WUP_CMD_GroupHeader;
ASSERT_STRUCT(WUP_CMD_GroupHeader, 2336);
#pragma pack()
/**
* Wii U content entry (Stored after the TMD) (v1)
* Reference: https://wiibrew.org/wiki/Title_metadata
*
* All fields are big-endian.
*/
typedef struct _WUP_Content_Entry {
uint32_t content_id; // [0x000] Content ID
uint16_t index; // [0x004] Index
uint16_t type; // [0x006] Type (see RVL_Content_Type_e)
uint64_t size; // [0x008] Size
uint8_t sha1_hash[20]; // [0x010] SHA-1 hash of the content (installed) or H3 table (disc).
uint8_t unused[12]; // [0x024] Unused. (Maybe it was going to be used for SHA-256?)
} WUP_Content_Entry;
ASSERT_STRUCT(WUP_Content_Entry, 48);
#ifdef __cplusplus
}
#endif

View File

@ -53,6 +53,7 @@ using std::vector;
#include "Console/SufamiTurbo.hpp"
#include "Console/WiiSave.hpp"
#include "Console/WiiTicket.hpp"
#include "Console/WiiTMD.hpp"
#include "Console/WiiU.hpp"
#include "Console/WiiWAD.hpp"
#include "Console/WiiWIBN.hpp"
@ -391,6 +392,7 @@ const RomDataFactoryPrivate::RomDataFns RomDataFactoryPrivate::romDataFns_header
GetRomDataFns(SegaSaturn, ATTR_NONE | ATTR_HAS_METADATA | ATTR_SUPPORTS_DEVICES),
GetRomDataFns(WiiSave, ATTR_HAS_THUMBNAIL),
GetRomDataFns(WiiTicket, ATTR_HAS_METADATA),
GetRomDataFns(WiiTMD, ATTR_HAS_METADATA),
GetRomDataFns(WiiWAD, ATTR_HAS_THUMBNAIL | ATTR_HAS_METADATA),
// Handhelds

View File

@ -36,6 +36,7 @@ application/x-sms-rom # Sega8Bit
application/x-gamegear-rom # Sega8Bit
application/x-saturn-rom # SegaSaturn
application/x-nintendo-ticket # WiiTicket
application/x-nintendo-tmd # WiiTMD
# Handheld
application/x-atari-lynx-rom # Lynx

View File

@ -516,6 +516,13 @@
<glob pattern="*.tik"/>
</mime-type>
<!-- WiiTMD -->
<mime-type type="application/x-nintendo-tmd">
<comment>Nintendo Title Metadata</comment>
<generic-icon name="application-pkix-cert"/>
<glob pattern="*.tmd"/>
</mime-type>
<!-- WiiU -->
<mime-type type="application/x-wii-u-rom">
<comment>Wii U disc image</comment>