mirror of
https://github.com/DarkMatterCore/nxdumptool.git
synced 2024-11-10 12:41:47 +00:00
974790944f
* Added NSP dumper PoC (SD card only atm, single-threaded). * Cert: replaced a wrong strcmp() with a proper strncmp(). * CNMT: added functions to update content info entries and generate/write Partition FS patches. * NCA: encrypt key area right after removing titlekey crypto. * NPDM/ProgramInfo: changed function names. * NPDM: check if the NCA has been modified before attempting to patch ACID data + calculate RSA-PSS signature *after* generating the PFS patch, not before. lol * PFS: restore name table size value before writing the header padding. * Tik: reworked the ticket lookup algorithm. Now uses information from ticket_list.bin to properly calculate the offset to the requested ticket in ticket.bin. * Title: changed title type strings used for filename generation. * Updated to-do list.
1855 lines
67 KiB
C
1855 lines
67 KiB
C
/*
|
|
* title.c
|
|
*
|
|
* Copyright (c) 2020, DarkMatterCore <pabloacurielz@gmail.com>.
|
|
*
|
|
* This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool).
|
|
*
|
|
* nxdumptool is free software; you can redistribute it and/or modify it
|
|
* under the terms and conditions of the GNU General Public License,
|
|
* version 2, as published by the Free Software Foundation.
|
|
*
|
|
* nxdumptool is distributed in the hope it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "utils.h"
|
|
#include "title.h"
|
|
#include "gamecard.h"
|
|
|
|
#define NS_APPLICATION_RECORD_LIMIT 4096
|
|
|
|
/* Type definitions. */
|
|
|
|
typedef struct {
|
|
u64 title_id;
|
|
char name[32];
|
|
} SystemTitleName;
|
|
|
|
/* Global variables. */
|
|
|
|
static Mutex g_titleMutex = 0;
|
|
static Thread g_titleGameCardInfoThread = {0};
|
|
static UEvent g_titleGameCardInfoThreadExitEvent = {0}, *g_titleGameCardStatusChangeUserEvent = NULL;
|
|
|
|
static bool g_titleInterfaceInit = false, g_titleGameCardInfoThreadCreated = false, g_titleGameCardAvailable = false, g_titleGameCardInfoUpdated = false;
|
|
|
|
static NsApplicationControlData *g_nsAppControlData = NULL;
|
|
|
|
static TitleApplicationMetadata *g_appMetadata = NULL;
|
|
static u32 g_appMetadataCount = 0;
|
|
|
|
static NcmContentMetaDatabase g_ncmDbGameCard = {0}, g_ncmDbEmmcSystem = {0}, g_ncmDbEmmcUser = {0}, g_ncmDbSdCard = {0};
|
|
static NcmContentStorage g_ncmStorageGameCard = {0}, g_ncmStorageEmmcSystem = {0}, g_ncmStorageEmmcUser = {0}, g_ncmStorageSdCard = {0};
|
|
|
|
static TitleInfo *g_titleInfo = NULL;
|
|
static u32 g_titleInfoCount = 0, g_titleInfoGameCardStartIndex = 0, g_titleInfoGameCardCount = 0, g_titleInfoOrphanCount = 0;
|
|
|
|
static const char *g_titleNcmContentTypeNames[] = {
|
|
[NcmContentType_Meta] = "Meta",
|
|
[NcmContentType_Program] = "Program",
|
|
[NcmContentType_Data] = "Data",
|
|
[NcmContentType_Control] = "Control",
|
|
[NcmContentType_HtmlDocument] = "HtmlDocument",
|
|
[NcmContentType_LegalInformation] = "LegalInformation",
|
|
[NcmContentType_DeltaFragment] = "DeltaFragment"
|
|
};
|
|
|
|
static const char *g_titleNcmContentMetaTypeNames[] = {
|
|
[NcmContentMetaType_Unknown] = "Unknown",
|
|
[NcmContentMetaType_SystemProgram] = "SystemProgram",
|
|
[NcmContentMetaType_SystemData] = "SystemData",
|
|
[NcmContentMetaType_SystemUpdate] = "SystemUpdate",
|
|
[NcmContentMetaType_BootImagePackage] = "BootImagePackage",
|
|
[NcmContentMetaType_BootImagePackageSafe] = "BootImagePackageSafe",
|
|
[NcmContentMetaType_Application - 0x7A] = "Application",
|
|
[NcmContentMetaType_Patch - 0x7A] = "Patch",
|
|
[NcmContentMetaType_AddOnContent - 0x7A] = "AddOnContent",
|
|
[NcmContentMetaType_Delta - 0x7A] = "Delta"
|
|
};
|
|
|
|
static const char *g_filenameTypeStrings[] = {
|
|
[NcmContentMetaType_Application - 0x80] = "BASE",
|
|
[NcmContentMetaType_Patch - 0x80] = "UPD",
|
|
[NcmContentMetaType_AddOnContent - 0x80] = "DLC",
|
|
[NcmContentMetaType_Delta - 0x80] = "DELTA"
|
|
};
|
|
|
|
/* Info retrieved from https://switchbrew.org/wiki/Title_list. */
|
|
/* Titles bundled with the kernel are excluded. */
|
|
static const SystemTitleName g_systemTitles[] = {
|
|
/* System modules. */
|
|
/* Meta + Program NCAs. */
|
|
{ 0x0100000000000006, "usb" },
|
|
{ 0x0100000000000007, "tma" },
|
|
{ 0x0100000000000008, "boot2" },
|
|
{ 0x0100000000000009, "settings" },
|
|
{ 0x010000000000000A, "bus" },
|
|
{ 0x010000000000000B, "bluetooth" },
|
|
{ 0x010000000000000C, "bcat" },
|
|
{ 0x010000000000000D, "dmnt" },
|
|
{ 0x010000000000000E, "friends" },
|
|
{ 0x010000000000000F, "nifm" },
|
|
{ 0x0100000000000010, "ptm" },
|
|
{ 0x0100000000000011, "shell" },
|
|
{ 0x0100000000000012, "bsdsockets" },
|
|
{ 0x0100000000000013, "hid" },
|
|
{ 0x0100000000000014, "audio" },
|
|
{ 0x0100000000000015, "LogManager" },
|
|
{ 0x0100000000000016, "wlan" },
|
|
{ 0x0100000000000017, "cs" },
|
|
{ 0x0100000000000018, "ldn" },
|
|
{ 0x0100000000000019, "nvservices" },
|
|
{ 0x010000000000001A, "pcv" },
|
|
{ 0x010000000000001B, "ppc" },
|
|
{ 0x010000000000001C, "nvnflinger" },
|
|
{ 0x010000000000001D, "pcie" },
|
|
{ 0x010000000000001E, "account" },
|
|
{ 0x010000000000001F, "ns" },
|
|
{ 0x0100000000000020, "nfc" },
|
|
{ 0x0100000000000021, "psc" },
|
|
{ 0x0100000000000022, "capsrv" },
|
|
{ 0x0100000000000023, "am" },
|
|
{ 0x0100000000000024, "ssl" },
|
|
{ 0x0100000000000025, "nim" },
|
|
{ 0x0100000000000026, "cec" },
|
|
{ 0x0100000000000027, "tspm" },
|
|
{ 0x0100000000000029, "lbl" },
|
|
{ 0x010000000000002A, "btm" },
|
|
{ 0x010000000000002B, "erpt" },
|
|
{ 0x010000000000002C, "time" },
|
|
{ 0x010000000000002D, "vi" },
|
|
{ 0x010000000000002E, "pctl" },
|
|
{ 0x010000000000002F, "npns" },
|
|
{ 0x0100000000000030, "eupld" },
|
|
{ 0x0100000000000031, "glue" },
|
|
{ 0x0100000000000032, "eclct" },
|
|
{ 0x0100000000000033, "es" },
|
|
{ 0x0100000000000034, "fatal" },
|
|
{ 0x0100000000000035, "grc" },
|
|
{ 0x0100000000000036, "creport" },
|
|
{ 0x0100000000000037, "ro" },
|
|
{ 0x0100000000000038, "profiler" },
|
|
{ 0x0100000000000039, "sdb" },
|
|
{ 0x010000000000003A, "migration" },
|
|
{ 0x010000000000003B, "jit" },
|
|
{ 0x010000000000003C, "jpegdec" },
|
|
{ 0x010000000000003D, "safemode" },
|
|
{ 0x010000000000003E, "olsc" },
|
|
{ 0x010000000000003F, "dt" },
|
|
{ 0x0100000000000040, "nd" },
|
|
{ 0x0100000000000041, "ngct" },
|
|
{ 0x0100000000000042, "pgl" },
|
|
|
|
/* System data archives. */
|
|
/* Meta + Data NCAs. */
|
|
{ 0x0100000000000800, "CertStore" },
|
|
{ 0x0100000000000801, "ErrorMessage" },
|
|
{ 0x0100000000000802, "MiiModel" },
|
|
{ 0x0100000000000803, "BrowserDll" },
|
|
{ 0x0100000000000804, "Help" },
|
|
{ 0x0100000000000805, "SharedFont" },
|
|
{ 0x0100000000000806, "NgWord" },
|
|
{ 0x0100000000000807, "SsidList" },
|
|
{ 0x0100000000000808, "Dictionary" },
|
|
{ 0x0100000000000809, "SystemVersion" },
|
|
{ 0x010000000000080A, "AvatarImage" },
|
|
{ 0x010000000000080B, "LocalNews" },
|
|
{ 0x010000000000080C, "Eula" },
|
|
{ 0x010000000000080D, "UrlBlackList" },
|
|
{ 0x010000000000080E, "TimeZoneBinary" },
|
|
{ 0x010000000000080F, "CertStoreCruiser" },
|
|
{ 0x0100000000000810, "FontNintendoExtension" },
|
|
{ 0x0100000000000811, "FontStandard" },
|
|
{ 0x0100000000000812, "FontKorean" },
|
|
{ 0x0100000000000813, "FontChineseTraditional" },
|
|
{ 0x0100000000000814, "FontChineseSimple" },
|
|
{ 0x0100000000000815, "FontBfcpx" },
|
|
{ 0x0100000000000816, "SystemUpdate" },
|
|
{ 0x0100000000000818, "FirmwareDebugSettings" },
|
|
{ 0x0100000000000819, "BootImagePackage" },
|
|
{ 0x010000000000081A, "BootImagePackageSafe" },
|
|
{ 0x010000000000081B, "BootImagePackageExFat" },
|
|
{ 0x010000000000081C, "BootImagePackageExFatSafe" },
|
|
{ 0x010000000000081D, "FatalMessage" },
|
|
{ 0x010000000000081E, "ControllerIcon" },
|
|
{ 0x010000000000081F, "PlatformConfigIcosa" },
|
|
{ 0x0100000000000820, "PlatformConfigCopper" },
|
|
{ 0x0100000000000821, "PlatformConfigHoag" },
|
|
{ 0x0100000000000822, "ControllerFirmware" },
|
|
{ 0x0100000000000823, "NgWord2" },
|
|
{ 0x0100000000000824, "PlatformConfigIcosaMariko" },
|
|
{ 0x0100000000000825, "ApplicationBlackList" },
|
|
{ 0x0100000000000826, "RebootlessSystemUpdateVersion" },
|
|
{ 0x0100000000000827, "ContentActionTable" },
|
|
{ 0x0100000000000828, "FunctionBlackList" },
|
|
{ 0x0100000000000830, "NgWordT" },
|
|
|
|
/* System applets. */
|
|
/* Meta + Program NCAs. */
|
|
{ 0x0100000000001000, "qlaunch" },
|
|
{ 0x0100000000001001, "auth" },
|
|
{ 0x0100000000001002, "cabinet" },
|
|
{ 0x0100000000001003, "controller" },
|
|
{ 0x0100000000001004, "dataErase" },
|
|
{ 0x0100000000001005, "error" },
|
|
{ 0x0100000000001006, "netConnect" },
|
|
{ 0x0100000000001007, "playerSelect" },
|
|
{ 0x0100000000001008, "swkbd" },
|
|
{ 0x0100000000001009, "miiEdit" },
|
|
{ 0x010000000000100A, "web" },
|
|
{ 0x010000000000100B, "shop" },
|
|
{ 0x010000000000100C, "overlayDisp" },
|
|
{ 0x010000000000100D, "photoViewer" },
|
|
{ 0x010000000000100E, "set" },
|
|
{ 0x010000000000100F, "offlineWeb" },
|
|
{ 0x0100000000001010, "loginShare" },
|
|
{ 0x0100000000001011, "wifiWebAuth" },
|
|
{ 0x0100000000001012, "starter" },
|
|
{ 0x0100000000001013, "myPage" },
|
|
{ 0x0100000000001014, "PlayReport" },
|
|
{ 0x0100000000001015, "MaintenanceMenu" },
|
|
{ 0x010000000000101A, "gift" },
|
|
{ 0x010000000000101B, "DummyECApplet" },
|
|
{ 0x010000000000101C, "userMigration" },
|
|
{ 0x010000000000101D, "EncounterSys" },
|
|
{ 0x0100000000001020, "story" },
|
|
{ 0x0100000000001023, "statistics" },
|
|
{ 0x0100000000001033, "promotion" },
|
|
{ 0x0100000000001038, "sample" },
|
|
{ 0x0100000000001FFF, "EndOceanProgramId" },
|
|
|
|
/* System debug applets. */
|
|
{ 0x0100000000002000, "A2BoardFunction" },
|
|
{ 0x0100000000002001, "A3Wireless" },
|
|
{ 0x0100000000002002, "C1LcdAndKey" },
|
|
{ 0x0100000000002003, "C2UsbHpmic" },
|
|
{ 0x0100000000002004, "C3Aging" },
|
|
{ 0x0100000000002005, "C4SixAxis" },
|
|
{ 0x0100000000002006, "C5Wireless" },
|
|
{ 0x0100000000002007, "C7FinalCheck" },
|
|
{ 0x010000000000203F, "AutoCapture" },
|
|
{ 0x0100000000002040, "DevMenuCommandSystem" },
|
|
{ 0x0100000000002041, "recovery" },
|
|
{ 0x0100000000002042, "DevMenuSystem" },
|
|
{ 0x0100000000002044, "HB-TBIntegrationTest" },
|
|
{ 0x010000000000204D, "BackupSaveData" },
|
|
{ 0x010000000000204E, "A4BoardCalWriti" },
|
|
{ 0x0100000000002054, "RepairSslCertificate" },
|
|
{ 0x0100000000002055, "GameCardWriter" },
|
|
{ 0x0100000000002056, "UsbPdTestTool" },
|
|
{ 0x0100000000002057, "RepairDeletePctl" },
|
|
{ 0x0100000000002058, "RepairBackup" },
|
|
{ 0x0100000000002059, "RepairRestore" },
|
|
{ 0x010000000000205A, "RepairAccountTransfer" },
|
|
{ 0x010000000000205B, "RepairAutoNetworkUpdater" },
|
|
{ 0x010000000000205C, "RefurbishReset" },
|
|
{ 0x010000000000205D, "RepairAssistCup" },
|
|
{ 0x010000000000205E, "RepairPairingCutter" },
|
|
{ 0x0100000000002064, "DevMenu" },
|
|
{ 0x0100000000002065, "DevMenuApp" },
|
|
{ 0x0100000000002066, "GetGameCardAsicInfo" },
|
|
{ 0x0100000000002068, "NfpDebugToolSystem" },
|
|
{ 0x0100000000002069, "AlbumSynchronizer" },
|
|
{ 0x0100000000002071, "SnapShotDumper" },
|
|
{ 0x0100000000002073, "DevMenuSystemApp" },
|
|
{ 0x0100000000002099, "DevOverlayDisp" },
|
|
{ 0x010000000000209A, "NandVerifier" },
|
|
{ 0x010000000000209B, "GpuCoreDumper" },
|
|
{ 0x010000000000209C, "TestApplication" },
|
|
{ 0x010000000000209E, "HelloWorld" },
|
|
{ 0x01000000000020A0, "XcieWriter" },
|
|
{ 0x01000000000020A1, "GpuOverrunNotifier" },
|
|
{ 0x01000000000020C8, "NfpDebugTool" },
|
|
{ 0x01000000000020CA, "NoftWriter" },
|
|
{ 0x01000000000020D0, "BcatSystemDebugTool" },
|
|
{ 0x01000000000020D1, "DevSafeModeUpdater" },
|
|
{ 0x01000000000020D3, "ControllerConnectionAnalyzer" },
|
|
{ 0x01000000000020D4, "DevKitUpdater" },
|
|
{ 0x01000000000020D6, "RepairTimeReviser" },
|
|
{ 0x01000000000020D7, "RepairReinitializeFuelGauge" },
|
|
{ 0x01000000000020DA, "RepairAbortMigration" },
|
|
{ 0x01000000000020DC, "RepairShowDeviceId" },
|
|
{ 0x01000000000020DD, "RepairSetCycleCountReliability" },
|
|
{ 0x01000000000020E0, "Interface" },
|
|
{ 0x01000000000020E1, "AlbumDownloader" },
|
|
{ 0x01000000000020E3, "FuelGaugeDumper" },
|
|
{ 0x01000000000020E4, "UnsafeExtract" },
|
|
{ 0x01000000000020E5, "UnsafeEngrave" },
|
|
{ 0x01000000000020EE, "BluetoothSettingTool" },
|
|
{ 0x01000000000020F0, "ApplicationInstallerRomfs" },
|
|
{ 0x0100000000002100, "DevMenuLotcheckDownloader" },
|
|
{ 0x0100000000002101, "DevMenuCommand" },
|
|
{ 0x0100000000002102, "ExportPartition" },
|
|
{ 0x0100000000002103, "SystemInitializer" },
|
|
{ 0x0100000000002104, "SystemUpdaterHostFs" },
|
|
{ 0x0100000000002105, "WriteToStorage" },
|
|
{ 0x0100000000002106, "CalWriter" },
|
|
{ 0x0100000000002107, "SettingsManager" },
|
|
{ 0x0100000000002109, "testBuildSystemIris" },
|
|
{ 0x010000000000210A, "SystemUpdater" },
|
|
{ 0x010000000000210B, "nvnflinger_util" },
|
|
{ 0x010000000000210C, "ControllerFirmwareUpdater" },
|
|
{ 0x010000000000210D, "testBuildSystemNintendoWare" },
|
|
{ 0x0100000000002110, "TestSaveDataCreator" },
|
|
{ 0x0100000000002111, "C9LcdSpker" },
|
|
{ 0x0100000000002114, "RankTurn" },
|
|
{ 0x0100000000002116, "BleTestTool" },
|
|
{ 0x010000000000211A, "PreinstallAppWriter" },
|
|
{ 0x010000000000211C, "ControllerSerialFlashTool" },
|
|
{ 0x010000000000211D, "ControllerFlashWriter" },
|
|
{ 0x0100000000002120, "ControllerTestApp" },
|
|
{ 0x0100000000002121, "HidInspectionTool" },
|
|
{ 0x0100000000002124, "BatteryCyclesEditor" },
|
|
{ 0x0100000000002125, "UsbFirmwareUpdater" },
|
|
{ 0x0100000000002126, "PalmaSerialCodeTool" },
|
|
{ 0x0100000000002127, "renderdoccmd" },
|
|
{ 0x0100000000002128, "HidInspectionToolProd" },
|
|
{ 0x010000000000212C, "ExhibitionMenu" },
|
|
{ 0x010000000000212F, "ExhibitionSaveData" },
|
|
{ 0x0100000000002130, "LuciaConverter" },
|
|
{ 0x0100000000002133, "CalDumper" },
|
|
{ 0x0100000000002134, "AnalogStickEvaluationTool" },
|
|
|
|
/* System debug modules. */
|
|
{ 0x0100000000003002, "DummyProcess" },
|
|
{ 0x0100000000003003, "DebugMonitor0" },
|
|
{ 0x0100000000003004, "SystemHelloWorld" },
|
|
|
|
/* Target tools. */
|
|
{ 0x1000000000000001, "SystemInitializer" },
|
|
{ 0x1000000000000004, "CalWriter" },
|
|
{ 0x1000000000000005, "DevMenuCommand" },
|
|
{ 0x1000000000000006, "SettingsManager" },
|
|
{ 0x1000000000000007, "ApplicationLauncer" },
|
|
{ 0x100000000000000B, "SnapShotDumper" },
|
|
{ 0x100000000000000C, "SystemUpdater" },
|
|
{ 0x100000000000000E, "ControllerFirmwareUpdater" },
|
|
|
|
/* Factory system modules. */
|
|
{ 0x010000000000B120, "nvdbgsvc" },
|
|
{ 0x010000000000B14A, "manu" },
|
|
{ 0x010000000000B14B, "ManuUsbLoopBack" },
|
|
{ 0x010000000000B1B8, "DevFwdbgHbPackage" },
|
|
{ 0x010000000000B1B9, "DevFwdbgUsbPackage" },
|
|
{ 0x010000000000B1BA, "ProdFwdbgPackage" },
|
|
{ 0x010000000000B22A, "scs" },
|
|
{ 0x010000000000B22B, "ControllerFirmwareDebug" },
|
|
{ 0x010000000000B240, "htc" },
|
|
{ 0x010000000000C600, "BdkSample01" },
|
|
{ 0x010000000000C601, "BdkSample02" },
|
|
{ 0x010000000000C602, "BdkSample03" },
|
|
{ 0x010000000000C603, "BdkSample04" },
|
|
{ 0x010000000000D609, "dmnt.gen2" },
|
|
|
|
/* System applications. */
|
|
{ 0x01008BB00013C000, "flog" },
|
|
{ 0x0100069000078000, "RetailInteractiveDisplayMenu" },
|
|
{ 0x010000B003486000, "AudioUsbMicDebugTool" },
|
|
{ 0x0100458001E04000, "BcatTestApp01" },
|
|
{ 0x0100F910020F8000, "BcatTestApp02" },
|
|
{ 0x0100B7D0020FC000, "BcatTestApp03" },
|
|
{ 0x0100132002100000, "BcatTestApp04" },
|
|
{ 0x0100935002116000, "BcatTestApp05" },
|
|
{ 0x0100DA4002130000, "BcatTestApp06" },
|
|
{ 0x0100B0F002104000, "BcatTestApp07" },
|
|
{ 0x010051E002132000, "BcatTestApp08" },
|
|
{ 0x01004CB0015C8000, "BcatTestApp09" },
|
|
{ 0x01009720015CA000, "BcatTestApp10" },
|
|
{ 0x01002F20015C6000, "BcatTestApp11" },
|
|
{ 0x0100204001F90000, "BcatTestApp12" },
|
|
{ 0x0100060001F92000, "BcatTestApp13" },
|
|
{ 0x0100C26001F94000, "BcatTestApp14" },
|
|
{ 0x0100462001F96000, "BcatTestApp15" },
|
|
{ 0x01005C6001F98000, "BcatTestApp16" },
|
|
{ 0x010070000E3C0000, "EncounterUsr" },
|
|
{ 0x010086000E49C000, "EncounterUsrDummy" },
|
|
{ 0x0100810002D5A000, "ShopMonitaringTool" },
|
|
{ 0x010023D002B98000, "DeltaStress" }
|
|
};
|
|
|
|
static const u32 g_systemTitlesCount = MAX_ELEMENTS(g_systemTitles);
|
|
|
|
/* Function prototypes. */
|
|
|
|
NX_INLINE void titleFreeApplicationMetadata(void);
|
|
NX_INLINE void titleFreeTitleInfo(void);
|
|
|
|
NX_INLINE TitleApplicationMetadata *titleFindApplicationMetadataByTitleId(u64 title_id);
|
|
|
|
static bool titleGenerateMetadataEntriesFromSystemTitles(void);
|
|
static bool titleGenerateMetadataEntriesFromNsRecords(void);
|
|
static bool titleRetrieveApplicationMetadataByTitleId(u64 title_id, TitleApplicationMetadata *out);
|
|
|
|
static bool titleOpenNcmDatabases(void);
|
|
static void titleCloseNcmDatabases(void);
|
|
|
|
static bool titleOpenNcmStorages(void);
|
|
static void titleCloseNcmStorages(void);
|
|
|
|
static bool titleOpenNcmDatabaseAndStorageFromGameCard(void);
|
|
static void titleCloseNcmDatabaseAndStorageFromGameCard(void);
|
|
|
|
static bool titleLoadPersistentStorageTitleInfo(void);
|
|
static bool titleRetrieveContentMetaKeysFromDatabase(u8 storage_id);
|
|
static bool titleGetContentInfosFromTitle(u8 storage_id, const NcmContentMetaKey *meta_key, NcmContentInfo **out_content_infos, u32 *out_content_count);
|
|
|
|
static bool titleCreateGameCardInfoThread(void);
|
|
static void titleDestroyGameCardInfoThread(void);
|
|
static void titleGameCardInfoThreadFunc(void *arg);
|
|
|
|
static bool titleRefreshGameCardTitleInfo(void);
|
|
static void titleRemoveGameCardTitleInfoEntries(void);
|
|
|
|
static bool titleIsUserApplicationContentAvailable(u64 app_id);
|
|
static TitleInfo *_titleGetInfoFromStorageByTitleId(u8 storage_id, u64 title_id, bool lock);
|
|
|
|
static int titleSystemTitleMetadataEntrySortFunction(const void *a, const void *b);
|
|
static int titleUserApplicationMetadataEntrySortFunction(const void *a, const void *b);
|
|
static int titleOrphanTitleInfoSortFunction(const void *a, const void *b);
|
|
|
|
bool titleInitialize(void)
|
|
{
|
|
mutexLock(&g_titleMutex);
|
|
|
|
bool ret = g_titleInterfaceInit;
|
|
if (ret) goto end;
|
|
|
|
/* Allocate memory for the ns application control data. */
|
|
/* This will be used each time we need to retrieve the metadata from an application. */
|
|
g_nsAppControlData = calloc(1, sizeof(NsApplicationControlData));
|
|
if (!g_nsAppControlData)
|
|
{
|
|
LOGFILE("Failed to allocate memory for the ns application control data!");
|
|
goto end;
|
|
}
|
|
|
|
/* Generate application metadata entries from hardcoded system titles, since we can't retrieve their names via ns. */
|
|
if (!titleGenerateMetadataEntriesFromSystemTitles())
|
|
{
|
|
LOGFILE("Failed to generate application metadata from hardcoded system titles!");
|
|
goto end;
|
|
}
|
|
|
|
/* Generate application metadata entries from ns records. */
|
|
/* Theoretically speaking, we should only need to do this once. */
|
|
/* However, if any new gamecard is inserted while the application is running, we *will* have to retrieve the metadata from its application(s). */
|
|
if (!titleGenerateMetadataEntriesFromNsRecords())
|
|
{
|
|
LOGFILE("Failed to generate application metadata from ns records!");
|
|
goto end;
|
|
}
|
|
|
|
/* Open eMMC System, eMMC User and SD card ncm databases. */
|
|
if (!titleOpenNcmDatabases())
|
|
{
|
|
LOGFILE("Failed to open ncm databases!");
|
|
goto end;
|
|
}
|
|
|
|
/* Open eMMC System, eMMC User and SD card ncm storages. */
|
|
if (!titleOpenNcmStorages())
|
|
{
|
|
LOGFILE("Failed to open ncm storages!");
|
|
goto end;
|
|
}
|
|
|
|
/* Load title info by retrieving content meta keys from available eMMC System, eMMC User and SD card titles. */
|
|
if (!titleLoadPersistentStorageTitleInfo())
|
|
{
|
|
LOGFILE("Failed to load persistent storage title info!");
|
|
goto end;
|
|
}
|
|
|
|
/* Create usermode exit event. */
|
|
ueventCreate(&g_titleGameCardInfoThreadExitEvent, true);
|
|
|
|
/* Retrieve gamecard status change user event. */
|
|
g_titleGameCardStatusChangeUserEvent = gamecardGetStatusChangeUserEvent();
|
|
if (!g_titleGameCardStatusChangeUserEvent)
|
|
{
|
|
LOGFILE("Failed to retrieve gamecard status change user event!");
|
|
goto end;
|
|
}
|
|
|
|
/* Create gamecard title info thread. */
|
|
if (!(g_titleGameCardInfoThreadCreated = titleCreateGameCardInfoThread())) goto end;
|
|
|
|
ret = g_titleInterfaceInit = true;
|
|
|
|
end:
|
|
mutexUnlock(&g_titleMutex);
|
|
|
|
return ret;
|
|
}
|
|
|
|
void titleExit(void)
|
|
{
|
|
mutexLock(&g_titleMutex);
|
|
|
|
/* Destroy gamecard detection thread. */
|
|
if (g_titleGameCardInfoThreadCreated)
|
|
{
|
|
titleDestroyGameCardInfoThread();
|
|
g_titleGameCardInfoThreadCreated = false;
|
|
}
|
|
|
|
/* Free title info. */
|
|
titleFreeTitleInfo();
|
|
|
|
/* Close gamecard ncm database and storage (if needed). */
|
|
titleCloseNcmDatabaseAndStorageFromGameCard();
|
|
|
|
/* Close eMMC System, eMMC User and SD card ncm storages. */
|
|
titleCloseNcmStorages();
|
|
|
|
/* Close eMMC System, eMMC User and SD card ncm databases. */
|
|
titleCloseNcmDatabases();
|
|
|
|
/* Free application metadata. */
|
|
titleFreeApplicationMetadata();
|
|
|
|
/* Free ns application control data. */
|
|
if (g_nsAppControlData) free(g_nsAppControlData);
|
|
|
|
g_titleInterfaceInit = false;
|
|
|
|
mutexUnlock(&g_titleMutex);
|
|
}
|
|
|
|
NcmContentMetaDatabase *titleGetNcmDatabaseByStorageId(u8 storage_id)
|
|
{
|
|
NcmContentMetaDatabase *ncm_db = NULL;
|
|
|
|
switch(storage_id)
|
|
{
|
|
case NcmStorageId_GameCard:
|
|
ncm_db = &g_ncmDbGameCard;
|
|
break;
|
|
case NcmStorageId_BuiltInSystem:
|
|
ncm_db = &g_ncmDbEmmcSystem;
|
|
break;
|
|
case NcmStorageId_BuiltInUser:
|
|
ncm_db = &g_ncmDbEmmcUser;
|
|
break;
|
|
case NcmStorageId_SdCard:
|
|
ncm_db = &g_ncmDbSdCard;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return ncm_db;
|
|
}
|
|
|
|
NcmContentStorage *titleGetNcmStorageByStorageId(u8 storage_id)
|
|
{
|
|
NcmContentStorage *ncm_storage = NULL;
|
|
|
|
switch(storage_id)
|
|
{
|
|
case NcmStorageId_GameCard:
|
|
ncm_storage = &g_ncmStorageGameCard;
|
|
break;
|
|
case NcmStorageId_BuiltInSystem:
|
|
ncm_storage = &g_ncmStorageEmmcSystem;
|
|
break;
|
|
case NcmStorageId_BuiltInUser:
|
|
ncm_storage = &g_ncmStorageEmmcUser;
|
|
break;
|
|
case NcmStorageId_SdCard:
|
|
ncm_storage = &g_ncmStorageSdCard;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return ncm_storage;
|
|
}
|
|
|
|
TitleApplicationMetadata **titleGetApplicationMetadataEntries(bool is_system, u32 *out_count)
|
|
{
|
|
mutexLock(&g_titleMutex);
|
|
|
|
u32 start_idx = (is_system ? 0 : g_systemTitlesCount);
|
|
u32 max_val = (is_system ? g_systemTitlesCount : g_appMetadataCount);
|
|
|
|
u32 app_count = 0;
|
|
TitleApplicationMetadata **app_metadata = NULL, **tmp_app_metadata = NULL;
|
|
|
|
if (!g_appMetadata || (is_system && g_appMetadataCount < g_systemTitlesCount) || (!is_system && g_appMetadataCount == g_systemTitlesCount) || !out_count)
|
|
{
|
|
LOGFILE("Invalid parameters!");
|
|
goto end;
|
|
}
|
|
|
|
for(u32 i = start_idx; i < max_val; i++)
|
|
{
|
|
TitleApplicationMetadata *cur_app_metadata = &(g_appMetadata[i]);
|
|
|
|
/* Skip current metadata entry if content data for this title isn't available. */
|
|
if ((is_system && !_titleGetInfoFromStorageByTitleId(NcmStorageId_BuiltInSystem, cur_app_metadata->title_id, false)) || \
|
|
(!is_system && !titleIsUserApplicationContentAvailable(cur_app_metadata->title_id))) continue;
|
|
|
|
/* Reallocate pointer buffer. */
|
|
tmp_app_metadata = realloc(app_metadata, (app_count + 1) * sizeof(TitleApplicationMetadata*));
|
|
if (!tmp_app_metadata)
|
|
{
|
|
LOGFILE("Failed to reallocate application metadata pointer buffer!");
|
|
if (app_metadata) free(app_metadata);
|
|
app_metadata = NULL;
|
|
goto end;
|
|
}
|
|
|
|
app_metadata = tmp_app_metadata;
|
|
tmp_app_metadata = NULL;
|
|
|
|
/* Set current pointer and increase counter. */
|
|
app_metadata[app_count++] = cur_app_metadata;
|
|
}
|
|
|
|
if (app_metadata && app_count)
|
|
{
|
|
/* Update output counter. */
|
|
*out_count = app_count;
|
|
} else {
|
|
LOGFILE("No content data found for %s!", is_system ? "system titles" : "user applications");
|
|
}
|
|
|
|
end:
|
|
mutexUnlock(&g_titleMutex);
|
|
|
|
return app_metadata;
|
|
}
|
|
|
|
TitleInfo *titleGetInfoFromStorageByTitleId(u8 storage_id, u64 title_id)
|
|
{
|
|
return _titleGetInfoFromStorageByTitleId(storage_id, title_id, true);
|
|
}
|
|
|
|
bool titleGetUserApplicationData(u64 app_id, TitleUserApplicationData *out)
|
|
{
|
|
mutexLock(&g_titleMutex);
|
|
|
|
bool success = false;
|
|
|
|
if (!out)
|
|
{
|
|
LOGFILE("Invalid parameters!");
|
|
goto end;
|
|
}
|
|
|
|
/* Clear output. */
|
|
memset(out, 0, sizeof(TitleUserApplicationData));
|
|
|
|
/* Get first user application title info. */
|
|
out->app_info = _titleGetInfoFromStorageByTitleId(NcmStorageId_Any, app_id, false);
|
|
|
|
/* Get first patch title info. */
|
|
out->patch_info = _titleGetInfoFromStorageByTitleId(NcmStorageId_Any, titleGetPatchIdByApplicationId(app_id), false);
|
|
|
|
/* Get first add-on content title info. */
|
|
for(u32 i = 0; i < g_titleInfoCount; i++)
|
|
{
|
|
TitleInfo *title_info = &(g_titleInfo[i]);
|
|
if (title_info->meta_key.type == NcmContentMetaType_AddOnContent && titleCheckIfAddOnContentIdBelongsToApplicationId(app_id, title_info->meta_key.id))
|
|
{
|
|
out->aoc_info = title_info;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Check retrieved title info. */
|
|
success = (out->app_info || out->patch_info || out->aoc_info);
|
|
if (!success)
|
|
{
|
|
LOGFILE("Failed to retrieve user application data for ID \"%016lX\"!", app_id);
|
|
goto end;
|
|
}
|
|
|
|
end:
|
|
mutexUnlock(&g_titleMutex);
|
|
|
|
return success;
|
|
}
|
|
|
|
bool titleAreOrphanTitlesAvailable(void)
|
|
{
|
|
mutexLock(&g_titleMutex);
|
|
bool ret = (g_titleInfoOrphanCount > 0);
|
|
mutexUnlock(&g_titleMutex);
|
|
return ret;
|
|
}
|
|
|
|
TitleInfo **titleGetInfoFromOrphanTitles(u32 *out_count)
|
|
{
|
|
mutexLock(&g_titleMutex);
|
|
|
|
TitleInfo **orphan_info = NULL;
|
|
|
|
if (!g_titleInfo || !g_titleInfoCount || !g_titleInfoOrphanCount || !out_count)
|
|
{
|
|
LOGFILE("Invalid parameters!");
|
|
goto end;
|
|
}
|
|
|
|
/* Allocate orphan title info buffer. */
|
|
orphan_info = calloc(g_titleInfoOrphanCount, sizeof(TitleInfo*));
|
|
if (!orphan_info)
|
|
{
|
|
LOGFILE("Failed to allocate memory for orphan title info buffer!");
|
|
goto end;
|
|
}
|
|
|
|
/* Get pointers to orphan title info entries. */
|
|
for(u32 i = 0, j = 0; i < g_titleInfoCount && j < g_titleInfoOrphanCount; i++)
|
|
{
|
|
TitleInfo *title_info = &(g_titleInfo[i]);
|
|
if ((title_info->meta_key.type == NcmContentMetaType_Patch || title_info->meta_key.type == NcmContentMetaType_AddOnContent) && !title_info->parent) orphan_info[j++] = title_info;
|
|
}
|
|
|
|
/* Sort orphan title info entries by title ID. */
|
|
if (g_titleInfoOrphanCount > 1) qsort(orphan_info, g_titleInfoOrphanCount, sizeof(TitleInfo*), &titleOrphanTitleInfoSortFunction);
|
|
|
|
/* Update output counter. */
|
|
*out_count = g_titleInfoOrphanCount;
|
|
|
|
end:
|
|
mutexUnlock(&g_titleMutex);
|
|
|
|
return orphan_info;
|
|
}
|
|
|
|
bool titleIsGameCardInfoUpdated(void)
|
|
{
|
|
mutexLock(&g_titleMutex);
|
|
bool ret = g_titleGameCardInfoUpdated;
|
|
if (ret) g_titleGameCardInfoUpdated = false; /* Update flag to avoid updating application metadata entries in the caller function if it's not needed. */
|
|
mutexUnlock(&g_titleMutex);
|
|
return ret;
|
|
}
|
|
|
|
char *titleGenerateFileName(const TitleInfo *title_info, u8 name_convention, u8 illegal_char_replace_type)
|
|
{
|
|
mutexLock(&g_titleMutex);
|
|
|
|
char *filename = NULL;
|
|
char title_name[0x400] = {0};
|
|
TitleApplicationMetadata *app_metadata = NULL;
|
|
|
|
if (!title_info || title_info->meta_key.type < NcmContentMetaType_Application || title_info->meta_key.type > NcmContentMetaType_Delta || name_convention > TitleFileNameConvention_IdAndVersionOnly || \
|
|
(name_convention == TitleFileNameConvention_Full && illegal_char_replace_type > TitleFileNameIllegalCharReplaceType_KeepAsciiCharsOnly))
|
|
{
|
|
LOGFILE("Invalid parameters!");
|
|
goto end;
|
|
}
|
|
|
|
u8 type = (title_info->meta_key.type - 0x80);
|
|
|
|
/* Retrieve application metadata. */
|
|
/* System titles and user applications: just retrieve the app_metadata pointer from the input TitleInfo. */
|
|
/* Patches and add-on contents: retrieve the app_metadata pointer from the parent TitleInfo if it's available. */
|
|
app_metadata = (title_info->meta_key.type <= NcmContentMetaType_Application ? title_info->app_metadata : (title_info->parent ? title_info->parent->app_metadata : NULL));
|
|
|
|
/* Generate filename for this title. */
|
|
if (name_convention == TitleFileNameConvention_Full)
|
|
{
|
|
if (app_metadata && *(app_metadata->lang_entry.name))
|
|
{
|
|
sprintf(title_name, "%s ", app_metadata->lang_entry.name);
|
|
if (illegal_char_replace_type) utilsReplaceIllegalCharacters(title_name, illegal_char_replace_type == TitleFileNameIllegalCharReplaceType_KeepAsciiCharsOnly);
|
|
}
|
|
|
|
sprintf(title_name + strlen(title_name), "[%016lX][v%u][%s]", title_info->meta_key.id, title_info->meta_key.version, g_filenameTypeStrings[type]);
|
|
} else
|
|
if (name_convention == TitleFileNameConvention_IdAndVersionOnly)
|
|
{
|
|
sprintf(title_name, "%016lX_v%u_%s", title_info->meta_key.id, title_info->meta_key.version, g_filenameTypeStrings[type]);
|
|
}
|
|
|
|
/* Duplicate generated filename. */
|
|
filename = strdup(title_name);
|
|
if (!filename) LOGFILE("Failed to duplicate generated filename!");
|
|
|
|
end:
|
|
mutexUnlock(&g_titleMutex);
|
|
|
|
return filename;
|
|
}
|
|
|
|
char *titleGenerateGameCardFileName(u8 name_convention, u8 illegal_char_replace_type)
|
|
{
|
|
mutexLock(&g_titleMutex);
|
|
|
|
size_t cur_filename_len = 0;
|
|
char *filename = NULL, *tmp_filename = NULL;
|
|
char app_name[0x400] = {0};
|
|
|
|
if (!g_titleGameCardAvailable || !g_titleInfo || !g_titleInfoCount || !g_titleInfoGameCardCount || g_titleInfoGameCardCount > g_titleInfoCount || \
|
|
g_titleInfoGameCardStartIndex != (g_titleInfoCount - g_titleInfoGameCardCount) || name_convention > TitleFileNameConvention_IdAndVersionOnly || \
|
|
(name_convention == TitleFileNameConvention_Full && illegal_char_replace_type > TitleFileNameIllegalCharReplaceType_KeepAsciiCharsOnly))
|
|
{
|
|
LOGFILE("Invalid parameters!");
|
|
goto end;
|
|
}
|
|
|
|
for(u32 i = g_titleInfoGameCardStartIndex; i < g_titleInfoCount; i++)
|
|
{
|
|
TitleInfo *app_info = &(g_titleInfo[i]);
|
|
u32 app_version = app_info->meta_key.version;
|
|
|
|
if (app_info->meta_key.type != NcmContentMetaType_Application) continue;
|
|
|
|
/* Check if the inserted gamecard holds any bundled patches for the current user application. */
|
|
/* If so, we'll use the highest patch version available as part of the filename. */
|
|
for(u32 j = g_titleInfoGameCardStartIndex; j < g_titleInfoCount; j++)
|
|
{
|
|
if (j == i) continue;
|
|
|
|
TitleInfo *patch_info = &(g_titleInfo[j]);
|
|
if (patch_info->meta_key.type != NcmContentMetaType_Patch || !titleCheckIfPatchIdBelongsToApplicationId(app_info->meta_key.id, patch_info->meta_key.id) || \
|
|
patch_info->meta_key.version <= app_version) continue;
|
|
|
|
app_version = patch_info->meta_key.version;
|
|
}
|
|
|
|
/* Generate current user application name. */
|
|
*app_name = '\0';
|
|
|
|
if (name_convention == TitleFileNameConvention_Full)
|
|
{
|
|
if (cur_filename_len) strcat(app_name, " + ");
|
|
|
|
if (app_info->app_metadata && *(app_info->app_metadata->lang_entry.name))
|
|
{
|
|
sprintf(app_name + strlen(app_name), "%s ", app_info->app_metadata->lang_entry.name);
|
|
if (illegal_char_replace_type) utilsReplaceIllegalCharacters(app_name, illegal_char_replace_type == TitleFileNameIllegalCharReplaceType_KeepAsciiCharsOnly);
|
|
}
|
|
|
|
sprintf(app_name + strlen(app_name), "[%016lX][v%u]", app_info->meta_key.id, app_version);
|
|
} else
|
|
if (name_convention == TitleFileNameConvention_IdAndVersionOnly)
|
|
{
|
|
if (cur_filename_len) strcat(app_name, "+");
|
|
sprintf(app_name + strlen(app_name), "%016lX_v%u", app_info->meta_key.id, app_version);
|
|
}
|
|
|
|
/* Reallocate output buffer. */
|
|
size_t app_name_len = strlen(app_name);
|
|
|
|
tmp_filename = realloc(filename, (cur_filename_len + app_name_len + 1) * sizeof(char));
|
|
if (!tmp_filename)
|
|
{
|
|
LOGFILE("Failed to reallocate filename buffer!");
|
|
if (filename) free(filename);
|
|
filename = NULL;
|
|
goto end;
|
|
}
|
|
|
|
filename = tmp_filename;
|
|
tmp_filename = NULL;
|
|
|
|
/* Concatenate current user application name. */
|
|
filename[cur_filename_len] = '\0';
|
|
strcat(filename, app_name);
|
|
cur_filename_len += app_name_len;
|
|
}
|
|
|
|
if (!filename) LOGFILE("Error: the inserted gamecard doesn't hold any user applications!");
|
|
|
|
end:
|
|
mutexUnlock(&g_titleMutex);
|
|
|
|
/* Fallback string if any errors occur. */
|
|
/* This function is guaranteed to fail with Kiosk / Quest gamecards, so that's why this is needed. */
|
|
if (!filename) filename = strdup("gamecard");
|
|
|
|
return filename;
|
|
}
|
|
|
|
const char *titleGetNcmContentTypeName(u8 content_type)
|
|
{
|
|
return (content_type <= NcmContentType_DeltaFragment ? g_titleNcmContentTypeNames[content_type] : NULL);
|
|
}
|
|
|
|
const char *titleGetNcmContentMetaTypeName(u8 content_meta_type)
|
|
{
|
|
if ((content_meta_type > NcmContentMetaType_BootImagePackageSafe && content_meta_type < NcmContentMetaType_Application) || content_meta_type > NcmContentMetaType_Delta) return NULL;
|
|
return (content_meta_type <= NcmContentMetaType_BootImagePackageSafe ? g_titleNcmContentMetaTypeNames[content_meta_type] : g_titleNcmContentMetaTypeNames[content_meta_type - 0x7A]);
|
|
}
|
|
|
|
NX_INLINE void titleFreeApplicationMetadata(void)
|
|
{
|
|
if (g_appMetadata)
|
|
{
|
|
for(u32 i = 0; i < g_appMetadataCount; i++)
|
|
{
|
|
if (g_appMetadata[i].icon) free(g_appMetadata[i].icon);
|
|
}
|
|
|
|
free(g_appMetadata);
|
|
g_appMetadata = NULL;
|
|
}
|
|
|
|
g_appMetadataCount = 0;
|
|
}
|
|
|
|
NX_INLINE void titleFreeTitleInfo(void)
|
|
{
|
|
if (g_titleInfo)
|
|
{
|
|
for(u32 i = 0; i < g_titleInfoCount; i++)
|
|
{
|
|
if (g_titleInfo[i].content_infos) free(g_titleInfo[i].content_infos);
|
|
}
|
|
|
|
free(g_titleInfo);
|
|
g_titleInfo = NULL;
|
|
}
|
|
|
|
g_titleInfoCount = g_titleInfoGameCardStartIndex = g_titleInfoGameCardCount = g_titleInfoOrphanCount = 0;
|
|
}
|
|
|
|
NX_INLINE TitleApplicationMetadata *titleFindApplicationMetadataByTitleId(u64 title_id)
|
|
{
|
|
if (!g_appMetadata || !g_appMetadataCount || !title_id) return NULL;
|
|
|
|
for(u32 i = 0; i < g_appMetadataCount; i++)
|
|
{
|
|
if (g_appMetadata[i].title_id == title_id) return &(g_appMetadata[i]);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static bool titleGenerateMetadataEntriesFromSystemTitles(void)
|
|
{
|
|
TitleApplicationMetadata *tmp_app_metadata = NULL;
|
|
|
|
/* Reallocate application metadata buffer. */
|
|
/* If g_appMetadata == NULL, realloc() will essentially act as a malloc(). */
|
|
tmp_app_metadata = realloc(g_appMetadata, (g_appMetadataCount + g_systemTitlesCount) * sizeof(TitleApplicationMetadata));
|
|
if (!tmp_app_metadata)
|
|
{
|
|
LOGFILE("Failed to reallocate application metadata buffer! (%u %s).", g_appMetadataCount + g_systemTitlesCount, (g_appMetadataCount + g_systemTitlesCount) > 1 ? "entries" : "entry");
|
|
return false;
|
|
}
|
|
|
|
g_appMetadata = tmp_app_metadata;
|
|
tmp_app_metadata = NULL;
|
|
|
|
/* Clear new application metadata buffer area. */
|
|
memset(g_appMetadata + g_appMetadataCount, 0, g_systemTitlesCount * sizeof(TitleApplicationMetadata));
|
|
|
|
/* Fill new application metadata entries. */
|
|
for(u32 i = 0; i < g_systemTitlesCount; i++)
|
|
{
|
|
TitleApplicationMetadata *app_metadata = &(g_appMetadata[g_appMetadataCount + i]);
|
|
const SystemTitleName *system_title = &(g_systemTitles[i]);
|
|
|
|
app_metadata->title_id = system_title->title_id;
|
|
sprintf(app_metadata->lang_entry.name, system_title->name);
|
|
}
|
|
|
|
/* Sort metadata entries by title ID. */
|
|
qsort(g_appMetadata + g_appMetadataCount, g_systemTitlesCount, sizeof(TitleApplicationMetadata), &titleSystemTitleMetadataEntrySortFunction);
|
|
|
|
/* Update application metadata count. */
|
|
g_appMetadataCount += g_systemTitlesCount;
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool titleGenerateMetadataEntriesFromNsRecords(void)
|
|
{
|
|
Result rc = 0;
|
|
|
|
NsApplicationRecord *app_records = NULL;
|
|
u32 app_records_count = 0;
|
|
|
|
u32 cur_app_count = g_appMetadataCount, new_app_count = 0;
|
|
TitleApplicationMetadata *tmp_app_metadata = NULL;
|
|
|
|
bool success = false;
|
|
|
|
/* Allocate memory for the ns application records. */
|
|
app_records = calloc(NS_APPLICATION_RECORD_LIMIT, sizeof(NsApplicationRecord));
|
|
if (!app_records)
|
|
{
|
|
LOGFILE("Failed to allocate memory for ns application records!");
|
|
goto end;
|
|
}
|
|
|
|
/* Retrieve ns application records. */
|
|
rc = nsListApplicationRecord(app_records, NS_APPLICATION_RECORD_LIMIT, 0, (s32*)&app_records_count);
|
|
if (R_FAILED(rc))
|
|
{
|
|
LOGFILE("nsListApplicationRecord failed! (0x%08X).", rc);
|
|
goto end;
|
|
}
|
|
|
|
/* Return right away if no records were retrieved. */
|
|
if (!app_records_count)
|
|
{
|
|
success = true;
|
|
goto end;
|
|
}
|
|
|
|
/* Reallocate application metadata buffer. */
|
|
tmp_app_metadata = realloc(g_appMetadata, (g_appMetadataCount + app_records_count) * sizeof(TitleApplicationMetadata));
|
|
if (!tmp_app_metadata)
|
|
{
|
|
LOGFILE("Failed to reallocate application metadata buffer! (%u %s).", g_appMetadataCount + app_records_count, (g_appMetadataCount + app_records_count) > 1 ? "entries" : "entry");
|
|
goto end;
|
|
}
|
|
|
|
g_appMetadata = tmp_app_metadata;
|
|
tmp_app_metadata = NULL;
|
|
|
|
/* Retrieve application metadata for each ns application record. */
|
|
for(u32 i = 0; i < app_records_count; i++)
|
|
{
|
|
if (!titleRetrieveApplicationMetadataByTitleId(app_records[i].application_id, &(g_appMetadata[g_appMetadataCount + new_app_count]))) continue;
|
|
new_app_count++;
|
|
}
|
|
|
|
/* Check retrieved application metadata count. */
|
|
if (!new_app_count)
|
|
{
|
|
LOGFILE("Unable to retrieve application metadata from ns application records! (%u %s).", app_records_count, app_records_count > 1 ? "entries" : "entry");
|
|
goto end;
|
|
}
|
|
|
|
/* Sort application metadata entries by name. */
|
|
if (new_app_count > 1) qsort(g_appMetadata + g_appMetadataCount, new_app_count, sizeof(TitleApplicationMetadata), &titleUserApplicationMetadataEntrySortFunction);
|
|
|
|
/* Update application metadata count. */
|
|
g_appMetadataCount += new_app_count;
|
|
|
|
success = true;
|
|
|
|
end:
|
|
if (app_records)
|
|
{
|
|
/* Decrease application metadata buffer size if needed. */
|
|
if (app_records_count && g_appMetadataCount < (cur_app_count + app_records_count))
|
|
{
|
|
TitleApplicationMetadata *tmp_app_metadata = realloc(g_appMetadata, g_appMetadataCount * sizeof(TitleApplicationMetadata));
|
|
if (tmp_app_metadata)
|
|
{
|
|
g_appMetadata = tmp_app_metadata;
|
|
tmp_app_metadata = NULL;
|
|
} else {
|
|
LOGFILE("Failed to reallocate application metadata buffer! (%u %s).", g_appMetadataCount, g_appMetadataCount > 1 ? "entries" : "entry");
|
|
}
|
|
}
|
|
|
|
free(app_records);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
static bool titleRetrieveApplicationMetadataByTitleId(u64 title_id, TitleApplicationMetadata *out)
|
|
{
|
|
if (!g_nsAppControlData || !title_id || !out)
|
|
{
|
|
LOGFILE("Invalid parameters!");
|
|
return false;
|
|
}
|
|
|
|
Result rc = 0;
|
|
u64 write_size = 0;
|
|
NacpLanguageEntry *lang_entry = NULL;
|
|
|
|
u32 icon_size = 0;
|
|
u8 *icon = NULL;
|
|
|
|
/* Retrieve ns application control data. */
|
|
rc = nsGetApplicationControlData(NsApplicationControlSource_Storage, title_id, g_nsAppControlData, sizeof(NsApplicationControlData), &write_size);
|
|
if (R_FAILED(rc))
|
|
{
|
|
LOGFILE("nsGetApplicationControlData failed for title ID \"%016lX\"! (0x%08X).", rc, title_id);
|
|
return false;
|
|
}
|
|
|
|
if (write_size < sizeof(NacpStruct))
|
|
{
|
|
LOGFILE("Retrieved application control data buffer is too small! (0x%lX).", write_size);
|
|
return false;
|
|
}
|
|
|
|
/* Get language entry. */
|
|
rc = nacpGetLanguageEntry(&(g_nsAppControlData->nacp), &lang_entry);
|
|
if (R_FAILED(rc))
|
|
{
|
|
LOGFILE("nacpGetLanguageEntry failed! (0x%08X).", rc);
|
|
return false;
|
|
}
|
|
|
|
/* Get icon. */
|
|
icon_size = (u32)(write_size - sizeof(NacpStruct));
|
|
if (icon_size)
|
|
{
|
|
icon = malloc(icon_size);
|
|
if (!icon)
|
|
{
|
|
LOGFILE("Error allocating memory for the icon buffer! (0x%X).", icon_size);
|
|
return false;
|
|
}
|
|
|
|
memcpy(icon, g_nsAppControlData->icon, icon_size);
|
|
}
|
|
|
|
/* Copy data. */
|
|
out->title_id = title_id;
|
|
|
|
memcpy(&(out->lang_entry), lang_entry, sizeof(NacpLanguageEntry));
|
|
utilsTrimString(out->lang_entry.name);
|
|
utilsTrimString(out->lang_entry.author);
|
|
|
|
out->icon_size = icon_size;
|
|
out->icon = icon;
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool titleOpenNcmDatabases(void)
|
|
{
|
|
Result rc = 0;
|
|
NcmContentMetaDatabase *ncm_db = NULL;
|
|
|
|
for(u8 i = NcmStorageId_BuiltInSystem; i <= NcmStorageId_SdCard; i++)
|
|
{
|
|
/* Retrieve ncm database pointer. */
|
|
ncm_db = titleGetNcmDatabaseByStorageId(i);
|
|
if (!ncm_db)
|
|
{
|
|
LOGFILE("Failed to retrieve ncm database pointer for storage ID %u!", i);
|
|
return false;
|
|
}
|
|
|
|
/* Check if the ncm database handle has already been retrieved. */
|
|
if (serviceIsActive(&(ncm_db->s))) continue;
|
|
|
|
/* Open ncm database. */
|
|
rc = ncmOpenContentMetaDatabase(ncm_db, i);
|
|
if (R_FAILED(rc))
|
|
{
|
|
/* If the SD card is mounted, but it isn't currently being used by HOS, 0x21005 will be returned, so we'll just filter this particular error and continue. */
|
|
/* This can occur when using the "Nintendo" directory from a different console, or when the "sdmc:/Nintendo/Contents/private" file is corrupted. */
|
|
LOGFILE("ncmOpenContentMetaDatabase failed for storage ID %u! (0x%08X).", i, rc);
|
|
if (i == NcmStorageId_SdCard && rc == 0x21005) continue;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void titleCloseNcmDatabases(void)
|
|
{
|
|
NcmContentMetaDatabase *ncm_db = NULL;
|
|
|
|
for(u8 i = NcmStorageId_BuiltInSystem; i <= NcmStorageId_SdCard; i++)
|
|
{
|
|
/* Retrieve ncm database pointer. */
|
|
ncm_db = titleGetNcmDatabaseByStorageId(i);
|
|
if (!ncm_db) continue;
|
|
|
|
/* Check if the ncm database handle has already been retrieved. */
|
|
if (serviceIsActive(&(ncm_db->s))) ncmContentMetaDatabaseClose(ncm_db);
|
|
}
|
|
}
|
|
|
|
static bool titleOpenNcmStorages(void)
|
|
{
|
|
Result rc = 0;
|
|
NcmContentStorage *ncm_storage = NULL;
|
|
|
|
for(u8 i = NcmStorageId_BuiltInSystem; i <= NcmStorageId_SdCard; i++)
|
|
{
|
|
/* Retrieve ncm storage pointer. */
|
|
ncm_storage = titleGetNcmStorageByStorageId(i);
|
|
if (!ncm_storage)
|
|
{
|
|
LOGFILE("Failed to retrieve ncm storage pointer for storage ID %u!", i);
|
|
return false;
|
|
}
|
|
|
|
/* Check if the ncm storage handle has already been retrieved. */
|
|
if (serviceIsActive(&(ncm_storage->s))) continue;
|
|
|
|
/* Open ncm storage. */
|
|
rc = ncmOpenContentStorage(ncm_storage, i);
|
|
if (R_FAILED(rc))
|
|
{
|
|
/* If the SD card is mounted, but it isn't currently being used by HOS, 0x21005 will be returned, so we'll just filter this particular error and continue. */
|
|
/* This can occur when using the "Nintendo" directory from a different console, or when the "sdmc:/Nintendo/Contents/private" file is corrupted. */
|
|
LOGFILE("ncmOpenContentStorage failed for storage ID %u! (0x%08X).", i, rc);
|
|
if (i == NcmStorageId_SdCard && rc == 0x21005) continue;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void titleCloseNcmStorages(void)
|
|
{
|
|
NcmContentStorage *ncm_storage = NULL;
|
|
|
|
for(u8 i = NcmStorageId_BuiltInSystem; i <= NcmStorageId_SdCard; i++)
|
|
{
|
|
/* Retrieve ncm storage pointer. */
|
|
ncm_storage = titleGetNcmStorageByStorageId(i);
|
|
if (!ncm_storage) continue;
|
|
|
|
/* Check if the ncm storage handle has already been retrieved. */
|
|
if (serviceIsActive(&(ncm_storage->s))) ncmContentStorageClose(ncm_storage);
|
|
}
|
|
}
|
|
|
|
static bool titleOpenNcmDatabaseAndStorageFromGameCard(void)
|
|
{
|
|
Result rc = 0;
|
|
NcmContentMetaDatabase *ncm_db = &g_ncmDbGameCard;
|
|
NcmContentStorage *ncm_storage = &g_ncmStorageGameCard;
|
|
|
|
/* Open ncm database. */
|
|
rc = ncmOpenContentMetaDatabase(ncm_db, NcmStorageId_GameCard);
|
|
if (R_FAILED(rc))
|
|
{
|
|
LOGFILE("ncmOpenContentMetaDatabase failed! (0x%08X).", rc);
|
|
goto end;
|
|
}
|
|
|
|
/* Open ncm storage. */
|
|
rc = ncmOpenContentStorage(ncm_storage, NcmStorageId_GameCard);
|
|
if (R_FAILED(rc))
|
|
{
|
|
LOGFILE("ncmOpenContentStorage failed! (0x%08X).", rc);
|
|
goto end;
|
|
}
|
|
|
|
end:
|
|
return R_SUCCEEDED(rc);
|
|
}
|
|
|
|
static void titleCloseNcmDatabaseAndStorageFromGameCard(void)
|
|
{
|
|
NcmContentMetaDatabase *ncm_db = &g_ncmDbGameCard;
|
|
NcmContentStorage *ncm_storage = &g_ncmStorageGameCard;
|
|
|
|
/* Check if the ncm database handle has already been retrieved. */
|
|
if (serviceIsActive(&(ncm_db->s))) ncmContentMetaDatabaseClose(ncm_db);
|
|
|
|
/* Check if the ncm storage handle has already been retrieved. */
|
|
if (serviceIsActive(&(ncm_storage->s))) ncmContentStorageClose(ncm_storage);
|
|
}
|
|
|
|
static bool titleLoadPersistentStorageTitleInfo(void)
|
|
{
|
|
/* Return right away if title info has already been retrieved. */
|
|
if (g_titleInfo || g_titleInfoCount) return true;
|
|
|
|
g_titleInfoCount = 0;
|
|
|
|
for(u8 i = NcmStorageId_BuiltInSystem; i <= NcmStorageId_SdCard; i++)
|
|
{
|
|
/* Retrieve content meta keys from the current storage. */
|
|
if (!titleRetrieveContentMetaKeysFromDatabase(i))
|
|
{
|
|
LOGFILE("Failed to retrieve content meta keys from storage ID %u!", i);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool titleRetrieveContentMetaKeysFromDatabase(u8 storage_id)
|
|
{
|
|
NcmContentMetaDatabase *ncm_db = NULL;
|
|
|
|
if (!(ncm_db = titleGetNcmDatabaseByStorageId(storage_id)) || !serviceIsActive(&(ncm_db->s)))
|
|
{
|
|
LOGFILE("Invalid parameters!");
|
|
return false;
|
|
}
|
|
|
|
Result rc = 0;
|
|
|
|
u32 written = 0, total = 0;
|
|
NcmContentMetaKey *meta_keys = NULL, *meta_keys_tmp = NULL;
|
|
size_t meta_keys_size = sizeof(NcmContentMetaKey);
|
|
|
|
TitleInfo *tmp_title_info = NULL;
|
|
|
|
bool success = false;
|
|
|
|
/* Allocate memory for the ncm application content meta keys. */
|
|
meta_keys = calloc(1, meta_keys_size);
|
|
if (!meta_keys)
|
|
{
|
|
LOGFILE("Unable to allocate memory for the ncm application meta keys!");
|
|
goto end;
|
|
}
|
|
|
|
/* Get a full list of all titles available in this storage. */
|
|
/* Meta type '0' means all title types will be retrieved. */
|
|
rc = ncmContentMetaDatabaseList(ncm_db, (s32*)&total, (s32*)&written, meta_keys, 1, 0, 0, 0, UINT64_MAX, NcmContentInstallType_Full);
|
|
if (R_FAILED(rc))
|
|
{
|
|
LOGFILE("ncmContentMetaDatabaseList failed! (0x%08X) (first entry).", rc);
|
|
goto end;
|
|
}
|
|
|
|
/* Check if our application meta keys buffer was actually filled. */
|
|
/* If it wasn't, odds are there are no titles in this storage. */
|
|
if (!written || !total)
|
|
{
|
|
success = true;
|
|
goto end;
|
|
}
|
|
|
|
/* Check if we need to resize our application meta keys buffer. */
|
|
if (total > written)
|
|
{
|
|
/* Update application meta keys buffer size. */
|
|
meta_keys_size *= total;
|
|
|
|
/* Reallocate application meta keys buffer. */
|
|
meta_keys_tmp = realloc(meta_keys, meta_keys_size);
|
|
if (!meta_keys_tmp)
|
|
{
|
|
LOGFILE("Unable to reallocate application meta keys buffer! (%u entries).", total);
|
|
goto end;
|
|
}
|
|
|
|
meta_keys = meta_keys_tmp;
|
|
meta_keys_tmp = NULL;
|
|
|
|
/* Issue call again. */
|
|
rc = ncmContentMetaDatabaseList(ncm_db, (s32*)&total, (s32*)&written, meta_keys, (s32)total, 0, 0, 0, UINT64_MAX, NcmContentInstallType_Full);
|
|
if (R_FAILED(rc))
|
|
{
|
|
LOGFILE("ncmContentMetaDatabaseList failed! (0x%08X) (%u %s).", rc, total, total > 1 ? "entries" : "entry");
|
|
goto end;
|
|
}
|
|
|
|
/* Safety check. */
|
|
if (written != total)
|
|
{
|
|
LOGFILE("Application meta key count mismatch! (%u != %u).", written, total);
|
|
goto end;
|
|
}
|
|
}
|
|
|
|
/* Reallocate title info buffer. */
|
|
/* If g_titleInfo == NULL, realloc() will essentially act as a malloc(). */
|
|
tmp_title_info = realloc(g_titleInfo, (g_titleInfoCount + total) * sizeof(TitleInfo));
|
|
if (!tmp_title_info)
|
|
{
|
|
LOGFILE("Unable to reallocate title info buffer! (%u %s).", g_titleInfoCount + total, (g_titleInfoCount + total) > 1 ? "entries" : "entry");
|
|
goto end;
|
|
}
|
|
|
|
g_titleInfo = tmp_title_info;
|
|
tmp_title_info = NULL;
|
|
|
|
/* Clear new title info buffer area. */
|
|
memset(g_titleInfo + g_titleInfoCount, 0, total * sizeof(TitleInfo));
|
|
|
|
/* Fill new title info entries. */
|
|
for(u32 i = 0; i < total; i++)
|
|
{
|
|
TitleInfo *cur_title_info = &(g_titleInfo[g_titleInfoCount + i]);
|
|
|
|
/* Fill information. */
|
|
cur_title_info->storage_id = storage_id;
|
|
memcpy(&(cur_title_info->meta_key), &(meta_keys[i]), sizeof(NcmContentMetaKey));
|
|
cur_title_info->version.value = cur_title_info->meta_key.version;
|
|
|
|
if (cur_title_info->meta_key.type <= NcmContentMetaType_Application) cur_title_info->app_metadata = titleFindApplicationMetadataByTitleId(cur_title_info->meta_key.id);
|
|
|
|
/* Retrieve content infos. */
|
|
if (titleGetContentInfosFromTitle(storage_id, &(cur_title_info->meta_key), &(cur_title_info->content_infos), &(cur_title_info->content_count)))
|
|
{
|
|
/* Calculate title size. */
|
|
u64 tmp_size = 0;
|
|
for(u32 j = 0; j < cur_title_info->content_count; j++)
|
|
{
|
|
titleConvertNcmContentSizeToU64(cur_title_info->content_infos[j].size, &tmp_size);
|
|
cur_title_info->size += tmp_size;
|
|
}
|
|
}
|
|
|
|
/* Generate formatted title size string. */
|
|
utilsGenerateFormattedSizeString(cur_title_info->size, cur_title_info->size_str, sizeof(cur_title_info->size_str));
|
|
}
|
|
|
|
/* Update title info count. */
|
|
g_titleInfoCount += total;
|
|
|
|
/* Update linked lists pointers for user applications, patches and add-on contents. */
|
|
g_titleInfoOrphanCount = 0;
|
|
for(u32 i = 0; i < g_titleInfoCount; i++)
|
|
{
|
|
TitleInfo *child_info = &(g_titleInfo[i]);
|
|
child_info->parent = child_info->previous = child_info->next = NULL;
|
|
|
|
if (child_info->meta_key.type != NcmContentMetaType_Application && child_info->meta_key.type != NcmContentMetaType_Patch && \
|
|
child_info->meta_key.type != NcmContentMetaType_AddOnContent) continue;
|
|
|
|
if (child_info->meta_key.type == NcmContentMetaType_Patch || child_info->meta_key.type == NcmContentMetaType_AddOnContent)
|
|
{
|
|
/* Retrieve pointer to parent user application entry for patches and add-on contents. */
|
|
/* Since gamecard title info entries are always appended to the end of the buffer, this guarantees we will first retrieve an eMMC / SD card entry (if available). */
|
|
for(u32 j = 0; j < g_titleInfoCount; j++)
|
|
{
|
|
TitleInfo *parent_info = &(g_titleInfo[j]);
|
|
|
|
if (parent_info->meta_key.type == NcmContentMetaType_Application && \
|
|
((child_info->meta_key.type == NcmContentMetaType_Patch && titleCheckIfPatchIdBelongsToApplicationId(parent_info->meta_key.id, child_info->meta_key.id)) || \
|
|
(child_info->meta_key.type == NcmContentMetaType_AddOnContent && titleCheckIfAddOnContentIdBelongsToApplicationId(parent_info->meta_key.id, child_info->meta_key.id))))
|
|
{
|
|
child_info->parent = parent_info;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Increase orphan title count if we couldn't find the parent user application. */
|
|
if (!child_info->parent)
|
|
{
|
|
g_titleInfoOrphanCount++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
/* Locate previous user application, patch or add-on content entry. */
|
|
/* If it's found, we will update both its next pointer and the previous pointer from the current entry. */
|
|
for(u32 j = i; j > 0; j--)
|
|
{
|
|
TitleInfo *previous_info = &(g_titleInfo[j - 1]);
|
|
|
|
if (previous_info->meta_key.type == child_info->meta_key.type && (((child_info->meta_key.type == NcmContentMetaType_Application || child_info->meta_key.type == NcmContentMetaType_Patch) && \
|
|
previous_info->meta_key.id == child_info->meta_key.id) || (child_info->meta_key.type == NcmContentMetaType_AddOnContent && \
|
|
titleCheckIfAddOnContentIdsAreSiblings(previous_info->meta_key.id, child_info->meta_key.id))))
|
|
{
|
|
previous_info->next = child_info;
|
|
child_info->previous = previous_info;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
success = true;
|
|
|
|
end:
|
|
if (meta_keys) free(meta_keys);
|
|
|
|
return success;
|
|
}
|
|
|
|
static bool titleGetContentInfosFromTitle(u8 storage_id, const NcmContentMetaKey *meta_key, NcmContentInfo **out_content_infos, u32 *out_content_count)
|
|
{
|
|
NcmContentMetaDatabase *ncm_db = NULL;
|
|
|
|
if (!(ncm_db = titleGetNcmDatabaseByStorageId(storage_id)) || !serviceIsActive(&(ncm_db->s)) || !meta_key || !out_content_infos || !out_content_count)
|
|
{
|
|
LOGFILE("Invalid parameters!");
|
|
return false;
|
|
}
|
|
|
|
Result rc = 0;
|
|
|
|
NcmContentMetaHeader content_meta_header = {0};
|
|
u64 content_meta_header_read_size = 0;
|
|
|
|
NcmContentInfo *content_infos = NULL;
|
|
u32 content_count = 0, written = 0;
|
|
|
|
bool success = false;
|
|
|
|
/* Retrieve content meta header. */
|
|
rc = ncmContentMetaDatabaseGet(ncm_db, meta_key, &content_meta_header_read_size, &content_meta_header, sizeof(NcmContentMetaHeader));
|
|
if (R_FAILED(rc))
|
|
{
|
|
LOGFILE("ncmContentMetaDatabaseGet failed! (0x%08X).", rc);
|
|
goto end;
|
|
}
|
|
|
|
if (content_meta_header_read_size != sizeof(NcmContentMetaHeader))
|
|
{
|
|
LOGFILE("Content meta header size mismatch! (0x%lX != 0x%lX).", rc, content_meta_header_read_size, sizeof(NcmContentMetaHeader));
|
|
goto end;
|
|
}
|
|
|
|
/* Get content count. */
|
|
content_count = (u32)content_meta_header.content_count;
|
|
if (!content_count)
|
|
{
|
|
LOGFILE("Content count is zero!");
|
|
goto end;
|
|
}
|
|
|
|
/* Allocate memory for the content infos. */
|
|
content_infos = calloc(content_count, sizeof(NcmContentInfo));
|
|
if (!content_infos)
|
|
{
|
|
LOGFILE("Unable to allocate memory for the content infos buffer! (%u content[s]).", content_count);
|
|
goto end;
|
|
}
|
|
|
|
/* Retrieve content infos. */
|
|
rc = ncmContentMetaDatabaseListContentInfo(ncm_db, (s32*)&written, content_infos, (s32)content_count, meta_key, 0);
|
|
if (R_FAILED(rc))
|
|
{
|
|
LOGFILE("ncmContentMetaDatabaseListContentInfo failed! (0x%08X).", rc);
|
|
goto end;
|
|
}
|
|
|
|
if (written != content_count)
|
|
{
|
|
LOGFILE("Content count mismatch! (%u != %u).", written, content_count);
|
|
goto end;
|
|
}
|
|
|
|
/* Update output. */
|
|
*out_content_infos = content_infos;
|
|
*out_content_count = content_count;
|
|
|
|
success = true;
|
|
|
|
end:
|
|
if (!success && content_infos) free(content_infos);
|
|
|
|
return success;
|
|
}
|
|
|
|
static bool titleCreateGameCardInfoThread(void)
|
|
{
|
|
if (!utilsCreateThread(&g_titleGameCardInfoThread, titleGameCardInfoThreadFunc, NULL, 1))
|
|
{
|
|
LOGFILE("Failed to create gamecard title info thread!");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void titleDestroyGameCardInfoThread(void)
|
|
{
|
|
/* Signal the exit event to terminate the gamecard title info thread. */
|
|
ueventSignal(&g_titleGameCardInfoThreadExitEvent);
|
|
|
|
/* Wait for the gamecard title info thread to exit. */
|
|
utilsJoinThread(&g_titleGameCardInfoThread);
|
|
}
|
|
|
|
static void titleGameCardInfoThreadFunc(void *arg)
|
|
{
|
|
(void)arg;
|
|
|
|
Result rc = 0;
|
|
int idx = 0;
|
|
|
|
Waiter gamecard_status_event_waiter = waiterForUEvent(g_titleGameCardStatusChangeUserEvent);
|
|
Waiter exit_event_waiter = waiterForUEvent(&g_titleGameCardInfoThreadExitEvent);
|
|
|
|
/* Initial gamecard title info retrieval. */
|
|
mutexLock(&g_titleMutex);
|
|
titleRefreshGameCardTitleInfo();
|
|
mutexUnlock(&g_titleMutex);
|
|
|
|
while(true)
|
|
{
|
|
/* Wait until an event is triggered. */
|
|
rc = waitMulti(&idx, -1, gamecard_status_event_waiter, exit_event_waiter);
|
|
if (R_FAILED(rc)) continue;
|
|
|
|
/* Exit event triggered. */
|
|
if (idx == 1) break;
|
|
|
|
/* Update gamecard title info. */
|
|
mutexLock(&g_titleMutex);
|
|
g_titleGameCardInfoUpdated = titleRefreshGameCardTitleInfo();
|
|
mutexUnlock(&g_titleMutex);
|
|
}
|
|
|
|
/* Update gamecard flags. */
|
|
g_titleGameCardAvailable = g_titleGameCardInfoUpdated = false;
|
|
|
|
threadExit();
|
|
}
|
|
|
|
static bool titleRefreshGameCardTitleInfo(void)
|
|
{
|
|
TitleApplicationMetadata *tmp_app_metadata = NULL;
|
|
u32 orig_app_count = g_appMetadataCount, cur_app_count = g_appMetadataCount, gamecard_app_count = 0, gamecard_metadata_count = 0;
|
|
bool status = false, success = false, cleanup = true;
|
|
|
|
/* Retrieve current gamecard status. */
|
|
status = (gamecardGetStatus() == GameCardStatus_InsertedAndInfoLoaded);
|
|
if (status == g_titleGameCardAvailable || !status)
|
|
{
|
|
success = cleanup = (status != g_titleGameCardAvailable);
|
|
goto end;
|
|
}
|
|
|
|
/* Open gamecard ncm database and storage handles. */
|
|
if (!titleOpenNcmDatabaseAndStorageFromGameCard())
|
|
{
|
|
LOGFILE("Failed to open gamecard ncm database and storage handles.");
|
|
goto end;
|
|
}
|
|
|
|
/* Update start index for the gamecard title info entries. */
|
|
g_titleInfoGameCardStartIndex = g_titleInfoCount;
|
|
|
|
/* Retrieve content meta keys from the gamecard ncm database. */
|
|
if (!titleRetrieveContentMetaKeysFromDatabase(NcmStorageId_GameCard))
|
|
{
|
|
LOGFILE("Failed to retrieve content meta keys from gamecard!");
|
|
goto end;
|
|
}
|
|
|
|
/* Update gamecard title info count. */
|
|
g_titleInfoGameCardCount = (g_titleInfoCount - g_titleInfoGameCardStartIndex);
|
|
if (!g_titleInfoGameCardCount)
|
|
{
|
|
LOGFILE("Empty content meta key count from gamecard!");
|
|
goto end;
|
|
}
|
|
|
|
/* Retrieve gamecard application metadata. */
|
|
for(u32 i = g_titleInfoGameCardStartIndex; i < g_titleInfoCount; i++)
|
|
{
|
|
TitleInfo *cur_title_info = &(g_titleInfo[i]);
|
|
|
|
/* Skip current title if it's not an application. */
|
|
if (cur_title_info->meta_key.type != NcmContentMetaType_Application) continue;
|
|
gamecard_app_count++;
|
|
|
|
/* Check if we already have an application metadata entry for this title ID. */
|
|
if ((cur_title_info->app_metadata = titleFindApplicationMetadataByTitleId(cur_title_info->meta_key.id)) != NULL)
|
|
{
|
|
gamecard_metadata_count++;
|
|
continue;
|
|
}
|
|
|
|
/* Reallocate application metadata buffer (if needed). */
|
|
if (cur_app_count < (g_appMetadataCount + 1))
|
|
{
|
|
tmp_app_metadata = realloc(g_appMetadata, (g_appMetadataCount + 1) * sizeof(TitleApplicationMetadata));
|
|
if (!tmp_app_metadata)
|
|
{
|
|
LOGFILE("Failed to reallocate application metadata buffer! (additional entry).");
|
|
goto end;
|
|
}
|
|
|
|
g_appMetadata = tmp_app_metadata;
|
|
tmp_app_metadata = NULL;
|
|
|
|
cur_app_count++;
|
|
}
|
|
|
|
/* Retrieve application metadata. */
|
|
if (!titleRetrieveApplicationMetadataByTitleId(cur_title_info->meta_key.id, &(g_appMetadata[g_appMetadataCount]))) continue;
|
|
|
|
cur_title_info->app_metadata = &(g_appMetadata[g_appMetadataCount++]);
|
|
gamecard_metadata_count++;
|
|
}
|
|
|
|
/* Check gamecard application count. */
|
|
if (!gamecard_app_count)
|
|
{
|
|
LOGFILE("Gamecard application count is zero!");
|
|
goto end;
|
|
}
|
|
|
|
/* Check retrieved application metadata count. */
|
|
if (!gamecard_metadata_count)
|
|
{
|
|
LOGFILE("Unable to retrieve application metadata from gamecard! (%u %s).", gamecard_app_count, gamecard_app_count > 1 ? "entries" : "entry");
|
|
goto end;
|
|
}
|
|
|
|
/* Sort application metadata entries by name. */
|
|
u32 new_app_count = (g_appMetadataCount - g_systemTitlesCount);
|
|
if (g_appMetadataCount > orig_app_count && new_app_count > 1) qsort(g_appMetadata + g_systemTitlesCount, new_app_count, sizeof(TitleApplicationMetadata), \
|
|
&titleUserApplicationMetadataEntrySortFunction);
|
|
|
|
success = true;
|
|
cleanup = false;
|
|
|
|
end:
|
|
/* Update gamecard status. */
|
|
g_titleGameCardAvailable = status;
|
|
|
|
/* Decrease application metadata buffer size (if needed). */
|
|
if ((success && g_appMetadataCount < cur_app_count) || (!success && g_appMetadataCount > orig_app_count))
|
|
{
|
|
if (!success) g_appMetadataCount = orig_app_count;
|
|
|
|
tmp_app_metadata = realloc(g_appMetadata, g_appMetadataCount * sizeof(TitleApplicationMetadata));
|
|
if (tmp_app_metadata)
|
|
{
|
|
g_appMetadata = tmp_app_metadata;
|
|
tmp_app_metadata = NULL;
|
|
}
|
|
}
|
|
|
|
/* Remove gamecard title info entries and close its ncm database and storage handles (if needed). */
|
|
if (cleanup)
|
|
{
|
|
titleRemoveGameCardTitleInfoEntries();
|
|
titleCloseNcmDatabaseAndStorageFromGameCard();
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
static void titleRemoveGameCardTitleInfoEntries(void)
|
|
{
|
|
if (!g_titleInfo || !g_titleInfoCount || !g_titleInfoGameCardCount || g_titleInfoGameCardCount > g_titleInfoCount || \
|
|
g_titleInfoGameCardStartIndex != (g_titleInfoCount - g_titleInfoGameCardCount)) return;
|
|
|
|
if (g_titleInfoGameCardCount == g_titleInfoCount)
|
|
{
|
|
/* Free all title info entries. */
|
|
titleFreeTitleInfo();
|
|
} else {
|
|
/* Update parent, previous and next title info pointers from user application, patch and add-on content entries. */
|
|
for(u32 i = 0; i < g_titleInfoGameCardStartIndex; i++)
|
|
{
|
|
TitleInfo *cur_title_info = &(g_titleInfo[i]);
|
|
|
|
if (cur_title_info->meta_key.type != NcmContentMetaType_Application && cur_title_info->meta_key.type != NcmContentMetaType_Patch && \
|
|
cur_title_info->meta_key.type != NcmContentMetaType_AddOnContent) continue;
|
|
|
|
if (cur_title_info->parent && cur_title_info->parent->storage_id == NcmStorageId_GameCard) cur_title_info->parent = NULL;
|
|
if (cur_title_info->previous && cur_title_info->previous->storage_id == NcmStorageId_GameCard) cur_title_info->previous = NULL;
|
|
if (cur_title_info->next && cur_title_info->next->storage_id == NcmStorageId_GameCard) cur_title_info->next = NULL;
|
|
}
|
|
|
|
/* Free content infos from gamecard title info entries. */
|
|
for(u32 i = g_titleInfoGameCardStartIndex; i < g_titleInfoCount; i++)
|
|
{
|
|
TitleInfo *cur_title_info = &(g_titleInfo[i]);
|
|
if (cur_title_info->content_infos) free(cur_title_info->content_infos);
|
|
}
|
|
|
|
/* Reallocate title info buffer. */
|
|
TitleInfo *tmp_title_info = realloc(g_titleInfo, g_titleInfoGameCardStartIndex * sizeof(TitleInfo));
|
|
if (tmp_title_info)
|
|
{
|
|
g_titleInfo = tmp_title_info;
|
|
tmp_title_info = NULL;
|
|
}
|
|
|
|
/* Update counters. */
|
|
g_titleInfoCount = g_titleInfoGameCardStartIndex;
|
|
g_titleInfoGameCardStartIndex = g_titleInfoGameCardCount = 0;
|
|
}
|
|
}
|
|
|
|
static bool titleIsUserApplicationContentAvailable(u64 app_id)
|
|
{
|
|
if (!g_titleInfo || !g_titleInfoCount || !app_id) return false;
|
|
|
|
for(u32 i = 0; i < g_titleInfoCount; i++)
|
|
{
|
|
TitleInfo *cur_title_info = &(g_titleInfo[i]);
|
|
|
|
if ((cur_title_info->meta_key.type == NcmContentMetaType_Application && cur_title_info->meta_key.id == app_id) || \
|
|
(cur_title_info->meta_key.type == NcmContentMetaType_Patch && titleCheckIfPatchIdBelongsToApplicationId(app_id, cur_title_info->meta_key.id)) || \
|
|
(cur_title_info->meta_key.type == NcmContentMetaType_AddOnContent && titleCheckIfAddOnContentIdBelongsToApplicationId(app_id, cur_title_info->meta_key.id))) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static TitleInfo *_titleGetInfoFromStorageByTitleId(u8 storage_id, u64 title_id, bool lock)
|
|
{
|
|
if (lock) mutexLock(&g_titleMutex);
|
|
|
|
TitleInfo *info = NULL;
|
|
|
|
if (!g_titleInfo || !g_titleInfoCount || storage_id < NcmStorageId_GameCard || storage_id > NcmStorageId_Any || (storage_id == NcmStorageId_GameCard && (!g_titleInfoGameCardCount || \
|
|
g_titleInfoGameCardCount > g_titleInfoCount || g_titleInfoGameCardStartIndex != (g_titleInfoCount - g_titleInfoGameCardCount))) || !title_id)
|
|
{
|
|
LOGFILE("Invalid parameters!");
|
|
goto end;
|
|
}
|
|
|
|
/* Speed up gamecard lookups. */
|
|
u32 start_idx = (storage_id == NcmStorageId_GameCard ? g_titleInfoGameCardStartIndex : 0);
|
|
u32 max_val = ((storage_id == NcmStorageId_GameCard || storage_id == NcmStorageId_Any) ? g_titleInfoCount : g_titleInfoGameCardStartIndex);
|
|
|
|
for(u32 i = start_idx; i < max_val; i++)
|
|
{
|
|
TitleInfo *title_info = &(g_titleInfo[i]);
|
|
|
|
if (title_info->meta_key.id == title_id && (storage_id == NcmStorageId_Any || (storage_id != NcmStorageId_Any && title_info->storage_id == storage_id)))
|
|
{
|
|
info = title_info;
|
|
break;
|
|
}
|
|
}
|
|
|
|
//if (!info) LOGFILE("Unable to find TitleInfo entry with ID \"%016lX\"! (storage ID %u).", title_id, storage_id);
|
|
|
|
end:
|
|
if (lock) mutexUnlock(&g_titleMutex);
|
|
|
|
return info;
|
|
}
|
|
|
|
static int titleSystemTitleMetadataEntrySortFunction(const void *a, const void *b)
|
|
{
|
|
const TitleApplicationMetadata *app_metadata_1 = (const TitleApplicationMetadata*)a;
|
|
const TitleApplicationMetadata *app_metadata_2 = (const TitleApplicationMetadata*)b;
|
|
|
|
if (app_metadata_1->title_id < app_metadata_2->title_id)
|
|
{
|
|
return -1;
|
|
} else
|
|
if (app_metadata_1->title_id > app_metadata_2->title_id)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int titleUserApplicationMetadataEntrySortFunction(const void *a, const void *b)
|
|
{
|
|
const TitleApplicationMetadata *app_metadata_1 = (const TitleApplicationMetadata*)a;
|
|
const TitleApplicationMetadata *app_metadata_2 = (const TitleApplicationMetadata*)b;
|
|
|
|
return strcasecmp(app_metadata_1->lang_entry.name, app_metadata_2->lang_entry.name);
|
|
}
|
|
|
|
static int titleOrphanTitleInfoSortFunction(const void *a, const void *b)
|
|
{
|
|
const TitleInfo *title_info_1 = *((const TitleInfo**)a);
|
|
const TitleInfo *title_info_2 = *((const TitleInfo**)b);
|
|
|
|
if (title_info_1->meta_key.id < title_info_2->meta_key.id)
|
|
{
|
|
return -1;
|
|
} else
|
|
if (title_info_1->meta_key.id > title_info_2->meta_key.id)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|