rom-properties/src/libromdata/Console/WiiUPackage_xml.cpp
David Korth 1aad707f88
Some checks are pending
Codecov / run (push) Waiting to run
CodeQL / Analyze (cpp) (push) Waiting to run
[libromdata] WiiUPackagePrivate::addFields_System_XMLs(): Use C strings instead of C++ strings for the XML keys.
Reduces memory usage slightly and probably improves performance a bit,
since we don't have to manage buffer sizes.

Code size differences: (64-bit Gentoo Linux, gcc-15.1.0, release build, no LTO)

   text    data     bss     dec     hex filename
  15953     288       0   16241    3f71 WiiUPackage_xml.cpp.o [before]
  13376     288       0   13664    3560 WiiUPackage_xml.cpp.o [after]
  -2577       0       0   -2577    -a11 Difference
2025-06-07 15:25:51 -04:00

674 lines
20 KiB
C++

/***************************************************************************
* ROM Properties Page shell extension. (libromdata) *
* WiiUPackage_xml.cpp: Wii U NUS Package reader. (XML parsing) *
* *
* Copyright (c) 2016-2025 by David Korth. *
* SPDX-License-Identifier: GPL-2.0-or-later *
***************************************************************************/
#include "stdafx.h"
#include "WiiUPackage_p.hpp"
#ifndef ENABLE_XML
# error Cannot compile EXE_manifest.cpp without XML support.
#endif
// for Wii U application types
#include "data/WiiUData.hpp"
// Other rom-properties libraries
using namespace LibRpBase;
using namespace LibRpFile;
using namespace LibRpText;
// for the system language code
#include "SystemRegion.hpp"
// PugiXML
#include <pugixml.hpp>
using namespace pugi;
// C++ STL classes
using std::array;
using std::string;
using std::unique_ptr;
using std::vector;
namespace LibRomData {
#if defined(_MSC_VER) && defined(XML_IS_DLL)
/**
* Check if PugiXML can be delay-loaded.
* @return 0 on success; negative POSIX error code on error.
*/
extern int DelayLoad_test_PugiXML(void);
#endif /* defined(_MSC_VER) && defined(XML_IS_DLL) */
/** PugiXML macros **/
/**
* Load a Wii U system XML file.
*
* The XML is loaded and parsed using the specified
* PugiXML document.
*
* NOTE: DelayLoad must be checked by the caller, since it's
* passing an xml_document reference to this function.
*
* @param doc [in/out] XML document
* @param filename [in] XML filename
* @param rootNode [in] Root node for verification
* @return 0 on success; negative POSIX error code on error.
*/
int WiiUPackagePrivate::loadSystemXml(xml_document &doc, const char *filename, const char *rootNode)
{
assert(this->isValid);
assert(rootNode != nullptr); // not checking in release builds
if (!this->isValid) {
// Can't load the XML file.
return -EIO;
}
IRpFilePtr f_xml = this->open(filename);
if (!f_xml) {
// Icon not found?
return -ENOENT;
}
// Read the entire resource into memory.
// Assuming a limit of 64 KB for Wii U system XML files.
const size_t xml_size = static_cast<size_t>(f_xml->size());
if (xml_size > 65536) {
// Manifest is too big.
// (Or, it's negative, and wraps around due to unsigned.)
return -ENOMEM;
}
// PugiXML memory allocation functions
allocation_function xml_alloc = get_memory_allocation_function();
deallocation_function xml_dealloc = get_memory_deallocation_function();
char *const xml_data = static_cast<char*>(xml_alloc(xml_size));
if (!xml_data) {
// malloc() failure!
return -ENOMEM;
}
size_t size = f_xml->read(xml_data, xml_size);
if (size != xml_size) {
// Read error.
xml_dealloc(xml_data);
int err = f_xml->lastError();
if (err == 0) {
err = EIO;
}
return -err;
}
f_xml.reset();
// Parse the XML.
doc.reset();
xml_parse_result result = doc.load_buffer_inplace_own(xml_data, xml_size, parse_default, encoding_utf8);
if (!result) {
// Error parsing the manifest XML.
// TODO: Better error code.
doc.reset();
return -EIO;
}
// Verify the root node.
xml_node theRootNode = doc.child(rootNode);
if (!theRootNode) {
// Root node not found.
// TODO: Better error code.
doc.reset();
return -EIO;
}
// Verify assembly attributes.
// Wii U System XMLs always have 'type' and 'access' attributes.
// 'type' should be "complex".
// 'access' might not necessarily be "777", so not checking it.
xml_attribute attr_type = theRootNode.attribute("type");
xml_attribute attr_access = theRootNode.attribute("access");
if (!attr_type || !attr_access || strcmp(attr_type.value(), "complex") != 0) {
// Incorrect attributes.
// TODO: Better error code.
doc.reset();
return -EIO;
}
// XML document loaded.
return 0;
}
/**
* Parse an "unsignedInt" element.
* @param rootNode [in] Root node
* @param name [in] Node name
* @param defval [in] Default value to return if the node isn't found
* @return unsignedInt data (returns 0 on error)
*/
unsigned int WiiUPackagePrivate::parseUnsignedInt(xml_node rootNode, const char *name, unsigned int defval)
{
if (!rootNode) {
return defval;
}
xml_node elem = rootNode.child(name);
if (!elem) {
return defval;
}
xml_attribute attr = elem.attribute("type");
if (!attr || strcmp(attr.value(), "unsignedInt") != 0) {
return defval;
}
attr = elem.attribute("length");
assert(attr && strcmp(attr.value(), "4") == 0);
if (!attr || strcmp(attr.value(), "4") != 0) {
return defval;
}
xml_text text = elem.text();
if (!text) {
return defval;
}
// Parse the value as an unsigned int.
return text.as_uint(defval);
}
/**
* Parse a "hexBinary" element.
* NOTE: Some fields are 64-bit hexBinary, so we'll return a 64-bit value.
* @param rootNode [in] Root node
* @param name [in] Node name
* @return hexBinary data (returns 0 on error)
*/
uint64_t WiiUPackagePrivate::parseHexBinary(xml_node rootNode, const char *name)
{
if (!rootNode) {
return 0;
}
xml_node elem = rootNode.child(name);
if (!elem) {
return 0;
}
xml_attribute attr = elem.attribute("type");
if (!attr || strcmp(attr.value(), "hexBinary") != 0) {
return 0;
}
attr = elem.attribute("length");
const char *const attr_value = attr.value();
assert(attr && (strcmp(attr_value, "4") == 0 || strcmp(attr_value, "8") == 0));
if (!attr || (strcmp(attr_value, "4") != 0 && strcmp(attr_value, "8") != 0)) {
return 0;
}
xml_text text = elem.text();
if (!text) {
return 0;
}
// Parse the value as a uint64_t.
// NOTE: PugiXML's as_*int*() functions expect decimal, not hex.
// Use strtoull() instead.
char *endptr;
uint64_t val = strtoull(text.get(), &endptr, 16);
return (*endptr == '\0') ? val : 0;
}
#define ADD_TEXT(rootNode, name, desc) do { \
xml_text text = rootNode.child(name).text(); \
if (text) { \
fields.addField_string((desc), text.get()); \
} \
} while (0)
/**
* Add fields from the Wii U System XML files.
* @return 0 on success; negative POSIX error code on error.
*/
int WiiUPackagePrivate::addFields_System_XMLs(void)
{
#if defined(_MSC_VER) && defined(XML_IS_DLL)
// Delay load verification.
int ret_dl = DelayLoad_test_PugiXML();
if (ret_dl != 0) {
// Delay load failed.
return ret_dl;
}
#endif /* defined(_MSC_VER) && defined(XML_IS_DLL) */
// Load the three XML files.
xml_document appXml, cosXml, metaXml;
{
int retSys = loadSystemXml(appXml, "/code/app.xml", "app");
int retCos = loadSystemXml(cosXml, "/code/cos.xml", "app");
int retMeta = loadSystemXml(metaXml, "/meta/meta.xml", "menu");
if (retSys != 0 && retCos != 0 && retMeta != 0) {
// Unable to load any of the XMLs.
return retSys;
}
}
// NOTE: Not creating a separate tab.
// app.xml root node: "app"
xml_node appRootNode = appXml.child("app");
// cos.xml root node: "app"
xml_node cosRootNode = cosXml.child("app");
// meta.xml root node: "menu"
xml_node metaRootNode = metaXml.child("menu");
if (!appRootNode && !cosRootNode && !metaRootNode) {
// Missing root elements from all three XMLs.
// TODO: Better error code.
return -EIO;
}
// Title (shortname), full title (longname), publisher
// TODO: Wii U language codes? The XMLs are strings, so we'll
// just use character-based codes for now.
// TODO: Do we need both shortname and longname?
// Language code map is required.
// Most Wii U language codes match standard codes,
// except for "zht" ('hant') and "zhs" ('hans').
// Ordering matches Wii U meta.xml, which is likely the internal ordering.
static constexpr size_t WiiU_LC_COUNT = 12U;
struct xml_lc_map_t {
char xml_lc[4]; // LC in the XML file
uint32_t lc; // Our LC
};
static const array<xml_lc_map_t, WiiU_LC_COUNT> xml_lc_map = {{
{"ja", 'ja'},
{"en", 'en'},
{"fr", 'fr'},
{"de", 'de'},
{"it", 'it'},
{"es", 'es'},
{"zhs", 'hans'},
{"ko", 'ko'},
{"nl", 'nl'},
{"pt", 'pt'},
{"ru", 'ru'},
{"zht", 'hant'},
}};
if (metaRootNode) {
array<const char*, WiiU_LC_COUNT> longnames;
array<const char*, WiiU_LC_COUNT> shortnames;
array<const char*, WiiU_LC_COUNT> publishers;
char longname_key[16] = "longname_";
char shortname_key[16] = "shortname_";
char publisher_key[16] = "publisher_";
for (size_t i = 0; i < xml_lc_map.size(); i++) {
// NOTE: xml_lc_map[i].xml_lc cannot be more than 3 letters.
strcpy(&longname_key[9], xml_lc_map[i].xml_lc);
strcpy(&shortname_key[10], xml_lc_map[i].xml_lc);
strcpy(&publisher_key[10], xml_lc_map[i].xml_lc);
longnames[i] = metaRootNode.child(longname_key).text().as_string(nullptr);
shortnames[i] = metaRootNode.child(shortname_key).text().as_string(nullptr);
publishers[i] = metaRootNode.child(publisher_key).text().as_string(nullptr);
}
// If English is valid, we'll deduplicate titles.
// TODO: Constants for Wii U languages. (Extension of Wii languages?)
const bool dedupe_titles = (longnames[1] && longnames[1][0] != '\0');
RomFields::StringMultiMap_t *const pMap_longname = new RomFields::StringMultiMap_t();
RomFields::StringMultiMap_t *const pMap_shortname = new RomFields::StringMultiMap_t();
RomFields::StringMultiMap_t *const pMap_publisher = new RomFields::StringMultiMap_t();
for (int langID = 0; langID < static_cast<int>(xml_lc_map.size()); langID++) {
// Check for empty strings first.
if (( !longnames[langID] || longnames[langID][0] == '\0') &&
(!shortnames[langID] || shortnames[langID][0] == '\0') &&
(!publishers[langID] || publishers[langID][0] == '\0'))
{
// Strings are empty.
continue;
}
if (dedupe_titles && langID != 1 /* English */) {
// Check if the title matches English.
if ( longnames[langID] && longnames[1] && !strcmp( longnames[langID], longnames[1]) &&
shortnames[langID] && shortnames[1] && !strcmp(shortnames[langID], shortnames[1]) &&
publishers[langID] && publishers[1] && !strcmp(publishers[langID], publishers[1]))
{
// All three title fields match English.
continue;
}
}
const uint32_t lc = xml_lc_map[langID].lc;
assert(lc != 0);
if (lc == 0)
continue;
if (longnames[langID] && longnames[langID][0] != '\0') {
pMap_longname->emplace(lc, longnames[langID]);
}
if (shortnames[langID] && shortnames[langID][0] != '\0') {
pMap_shortname->emplace(lc, shortnames[langID]);
}
if (publishers[langID] && publishers[langID][0] != '\0') {
pMap_publisher->emplace(lc, publishers[langID]);
}
}
// NOTE: Using the same descriptions as Nintendo3DS.
const char *const s_title_title = C_("Nintendo", "Title");
const char *const s_full_title_title = C_("Nintendo", "Full Title");
const char *const s_publisher_title = C_("RomData", "Publisher");
const char *const s_unknown = C_("RomData", "Unknown");
// Get the system language code and see if we have a matching title.
uint32_t def_lc = SystemRegion::getLanguageCode();
if (pMap_longname->find(def_lc) == pMap_longname->end()) {
// Not valid. Check English.
if (pMap_longname->find('en') != pMap_longname->end()) {
// English is valid.
def_lc = 'en';
} else {
// Not valid. Check Japanese.
if (pMap_longname->find('jp') != pMap_longname->end()) {
// Japanese is valid.
def_lc = 'jp';
} else {
// Not valid...
// Default to English anyway.
def_lc = 'en';
}
}
}
if (!pMap_shortname->empty()) {
fields.addField_string_multi(s_title_title, pMap_shortname, def_lc);
} else {
delete pMap_shortname;
fields.addField_string(s_title_title, s_unknown);
}
if (!pMap_longname->empty()) {
fields.addField_string_multi(s_full_title_title, pMap_longname, def_lc);
} else {
delete pMap_longname;
fields.addField_string(s_full_title_title, s_unknown);
}
if (!pMap_publisher->empty()) {
fields.addField_string_multi(s_publisher_title, pMap_publisher, def_lc);
} else {
delete pMap_publisher;
fields.addField_string(s_publisher_title, s_unknown);
}
}
// Product code
ADD_TEXT(metaRootNode, "product_code", C_("Nintendo", "Product Code"));
// SDK version
if (appRootNode) {
const unsigned int sdk_version = parseUnsignedInt(appRootNode, "sdk_version");
if (sdk_version != 0) {
fields.addField_string(C_("WiiU", "SDK Version"),
fmt::format(FSTR("{:d}.{:0>2d}.{:0>2d}"),
sdk_version / 10000, (sdk_version / 100) % 100, sdk_version % 100));
}
}
// argstr (TODO: Better title)
ADD_TEXT(cosRootNode, "argstr", "argstr");
// app_type (TODO: Decode this!)
if (appRootNode) {
const unsigned int app_type = parseHexBinary32(appRootNode, "app_type");
if (app_type != 0) {
const char *const s_app_type_title = C_("RomData", "Type");
const char *const s_app_type = WiiUData::lookup_application_type(app_type);
if (s_app_type) {
fields.addField_string(s_app_type_title, s_app_type);
} else {
fields.addField_string_numeric(s_app_type_title, app_type,
RomFields::Base::Hex, 8, RomFields::STRF_MONOSPACE);
}
}
}
if (metaRootNode) {
// Region code
// Maps directly to the region field.
const uint32_t region_code = parseHexBinary32(metaRootNode, "region");
static const array<const char*, 7> wiiu_region_bitfield_names = {{
NOP_C_("Region", "Japan"),
NOP_C_("Region", "USA"),
NOP_C_("Region", "Europe"),
nullptr, //NOP_C_("Region", "Australia"), // NOTE: Not actually used?
NOP_C_("Region", "China"),
NOP_C_("Region", "South Korea"),
NOP_C_("Region", "Taiwan"),
}};
vector<string> *const v_wiiu_region_bitfield_names = RomFields::strArrayToVector_i18n(
"Region", wiiu_region_bitfield_names);
fields.addField_bitfield(C_("RomData", "Region Code"),
v_wiiu_region_bitfield_names, 3, region_code);
// Age rating(s)
// The fields match other Nintendo products, but it's in XML
// instead of a binary field.
// TODO: Exclude ratings that aren't actually used?
RomFields::age_ratings_t age_ratings;
// Valid ratings: 0-1, 3-4, 6-11 (excludes old BBFC and Finland/MEKU)
static constexpr uint16_t valid_ratings = 0xFDB;
static const array<const char*, 12> age_rating_nodes = {{
"pc_cero", "pc_esrb", "pc_bbfc", "pc_usk",
"pc_pegi_gen", "pc_pegi_fin", "pc_pegi_prt", "pc_pegi_bbfc",
"pc_cob", "pc_grb", "pc_cgsrr", "pc_oflc",
/*"pc_reserved0", "pc_reserved1", "pc_reserved2", "pc_reserved3",*/
}};
static_assert(age_rating_nodes.size() == static_cast<size_t>(RomFields::AgeRatingsCountry::MaxAllocated), "age_rating_nodes is out of sync with age_ratings_t");
for (int i = static_cast<int>(age_ratings.size())-1; i >= 0; i--) {
if (!(valid_ratings & (1U << i))) {
// Rating is not applicable for Wii U.
age_ratings[i] = 0;
continue;
}
// Wii U ratings field:
// - 0x00-0x1F: Age rating
// - 0x80: No rating
// - 0xC0: Rating pending
unsigned int val = parseUnsignedInt(metaRootNode, age_rating_nodes[i], ~0U);
if (val == ~0U) {
// Not found...
age_ratings[i] = 0;
continue;
}
if (val == 0x80) {
// Rating is unused
age_ratings[i] = 0;
} else if (val == 0xC0) {
// Rating pending
age_ratings[i] = RomFields::AGEBF_ACTIVE | RomFields::AGEBF_PENDING;
} /*else if (val == 0) {
// No age restriction
// FIXME: Can be confused with some other ratings, so disabled for now.
// Maybe Wii U has the same 0x20 bit as 3DS?
age_ratings[i] = RomFields::AGEBF_ACTIVE | RomFields::AGEBF_NO_RESTRICTION;
}*/ else {
// Set active | age value.
age_ratings[i] = RomFields::AGEBF_ACTIVE | (val & RomFields::AGEBF_MIN_AGE_MASK);
}
}
fields.addField_ageRatings(C_("RomData", "Age Ratings"), age_ratings);
// Controller support
uint32_t controllers = 0;
static const array<const char*, 6> controller_nodes = {{
"ext_dev_nunchaku",
"ext_dev_classic",
"ext_dev_urcc",
"ext_dev_board",
"ext_dev_usb_keyboard",
//"ext_dev_etc", // TODO
//"ext_dev_etc_name", // TODO
"drc_use",
}};
for (size_t i = 0; i < controller_nodes.size(); i++) {
unsigned int val = parseUnsignedInt(metaRootNode, controller_nodes[i]);
if (val > 0) {
// This controller is supported.
controllers |= (1U << i);
}
}
static const array<const char*, 6> controllers_bitfield_names = {{
NOP_C_("WiiU|Controller", "Nunchuk"),
NOP_C_("WiiU|Controller", "Classic"),
NOP_C_("WiiU|Controller", "Pro"),
NOP_C_("WiiU|Controller", "Balance Board"),
NOP_C_("WiiU|Controller", "USB Keyboard"),
NOP_C_("WiiU|Controller", "Gamepad"),
}};
vector<string> *const v_controllers_bitfield_names = RomFields::strArrayToVector_i18n(
"WiiU|Controller", controllers_bitfield_names);
fields.addField_bitfield(C_("WiiU", "Controllers"),
v_controllers_bitfield_names, 3, controllers);
}
// System XML files read successfully.
return 0;
}
/**
* Add metadata from the Wii U System XML files.
* @return 0 on success; negative POSIX error code on error.
*/
int WiiUPackagePrivate::addMetaData_System_XMLs(void)
{
#if defined(_MSC_VER) && defined(XML_IS_DLL)
// Delay load verification.
int ret_dl = DelayLoad_test_PugiXML();
if (ret_dl != 0) {
// Delay load failed.
return ret_dl;
}
#endif /* defined(_MSC_VER) && defined(XML_IS_DLL) */
// Load meta.xml.
xml_document metaXml;
int ret = loadSystemXml(metaXml, "/meta/meta.xml", "menu");
if (ret != 0) {
return ret;
}
// meta.xml root node: "menu"
xml_node metaRootNode = metaXml.child("menu");
if (!metaRootNode) {
// No "menu" element.
// TODO: Better error code.
return -EIO;
}
// Get the system language code and see if we have a matching title.
// NOTE: Using the same LC for all fields once we find a matching title.
string s_def_lc = SystemRegion::lcToString(SystemRegion::getLanguageCode());
string nodeName = fmt::format(FSTR("shortname_{:s}"), s_def_lc);
xml_text shortname = metaRootNode.child(nodeName.c_str()).text();
if (!shortname) {
// Not valid. Check English.
shortname = metaRootNode.child("shortname_en").text();
if (shortname) {
// English is valid.
s_def_lc = "en";
} else {
// Not valid. Check Japanese.
shortname = metaRootNode.child("shortname_jp");
if (shortname) {
// Japanese is valid.
s_def_lc = "jp";
} else {
// Not valid...
// Default to English anyway.
s_def_lc = "en";
}
}
}
// Title
// TODO: Shortname vs. longname?
if (shortname) {
metaData.addMetaData_string(Property::Title, shortname.get());
}
// Publisher
nodeName = fmt::format(FSTR("publisher_{:s}"), s_def_lc);
xml_text publisher = metaRootNode.child(nodeName.c_str()).text();
if (publisher) {
metaData.addMetaData_string(Property::Publisher, publisher.get());
}
// System XML files read successfully.
return 0;
}
/**
* Get the product code from meta.xml, and application type from app.xml.
* @param pApplType [out] Pointer to uint32_t for application type
* @return Product code, or empty string on error.
*/
string WiiUPackagePrivate::getProductCodeAndApplType_xml(uint32_t *pApplType)
{
#if defined(_MSC_VER) && defined(XML_IS_DLL)
// Delay load verification.
int ret_dl = DelayLoad_test_PugiXML();
if (ret_dl != 0) {
// Delay load failed.
return {};
}
#endif /* defined(_MSC_VER) && defined(XML_IS_DLL) */
xml_document metaXml;
int retMeta = loadSystemXml(metaXml, "/meta/meta.xml", "menu");
if (retMeta != 0) {
// Unable to load meta.xml.
return {};
}
xml_node metaRootNode = metaXml.child("menu");
if (!metaRootNode) {
// No root node.
return {};
}
const char *const product_code = metaRootNode.child("product_code").text().as_string(nullptr);
if (pApplType) {
// Get the application type.
xml_document appXml;
int retApp = loadSystemXml(appXml, "/code/app.xml", "app");
if (retApp == 0) {
xml_node appRootNode = appXml.child("app");
if (appRootNode) {
*pApplType = parseHexBinary32(appRootNode, "app_type");
}
} else {
// Unable to load app.xml.
*pApplType = 0;
}
}
return (product_code) ? product_code : string();
}
}