Fix install destination detection for CXI-based game files (#953)

Fixces #952
* Fix install destination detection
* fix NCCH extheader reading logic
* check for empty mountpath on title uninstallation
This commit is contained in:
ZeroSkill 2026-05-24 14:40:59 +03:00 committed by GitHub
parent f477da087c
commit 4058c9c5f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1046,6 +1046,143 @@ u32 VerifyTicketFile(const char* path) {
return res;
}
u32 LoadNcchFromTmdFile(const char *path, NcchHeader *ncch, NcchExtHeader *extheader) {
char pathstr[UTF_BUFFER_BYTESIZE(32)];
TruncateString(pathstr, path, 32, 8);
// content path
char path_content[256];
char *name_content; // points to content file name (%08lx)
strncpy(path_content, path, 256);
path_content[255] = '\0';
name_content = strrchr(path_content, '/');
if (!name_content) return 1;
name_content++;
TitleMetaData *tmd = (TitleMetaData*) malloc(TMD_SIZE_MAX);
TmdContentChunk *content_list = (TmdContentChunk*) (tmd + 1);
if ((LoadTmdFile(tmd, path) != 0) || (VerifyTmd(tmd) != 0)) {
ShowPrompt(false, "%s\n%s", pathstr, STR_ERROR_TMD_PROBABLY_CORRUPTED);
free(tmd);
return 1;
}
u8 titlekey[0x10] = { 0xFF };
Ticket* ticket = NULL;
if (!((LoadCdnTicketFile(&ticket, path) == 0) ||
((ticket = (Ticket*)malloc(TICKET_COMMON_SIZE), ticket != NULL) &&
(BuildFakeTicket(ticket, tmd->title_id) == 0) &&
(FindTitleKey(ticket, tmd->title_id) == 0))) ||
(GetTitleKey(titlekey, ticket) != 0)) {
ShowPrompt(false, "%s\n%s", pathstr, STR_ERROR_CDN_TITLEKEY_NOT_FOUND);
free(ticket);
free(tmd);
return 1;
}
free(ticket);
u32 content_count = getbe16(tmd->content_count);
u32 res = 0;
bool found = false;
TmdContentChunk chunk = { 0 };
for (u32 i = 0; !res && i < content_count && (i < TMD_MAX_CONTENTS); i++) {
TmdContentChunk *tmp = &(content_list[i]);
if (getbe16(tmp->index) == 0) {
found = true;
memcpy(&chunk, tmp, sizeof(TmdContentChunk));
break;
}
}
free(tmd);
if (!found) {
return 1;
}
// path to content with index 0
snprintf(name_content, 256 - (name_content - path_content), "%08lx", getbe32(chunk.id));
bool encrypted = getbe16(chunk.type) & 0x1;
u8 ctr[16];
GetTmdCtr(ctr, &chunk);
if (fvx_qread(path_content, ncch, 0, sizeof(NcchHeader), NULL) != FR_OK ||
(extheader && (fvx_qread(path_content, extheader, NCCH_EXTHDR_OFFSET, sizeof(NcchExtHeader), NULL) != FR_OK)))
return 1;
if (encrypted) {
DecryptCiaContentSequential(ncch, sizeof(NcchHeader), ctr, titlekey);
if (extheader)
DecryptCiaContentSequential(extheader, sizeof(NcchExtHeader), ctr, titlekey);
}
if (ValidateNcchHeader(ncch) != 0 ||
(extheader && (DecryptNcch(extheader, NCCH_EXTHDR_OFFSET, sizeof(NcchExtHeader), ncch, NULL) != 0)))
return 1;
return 0;
}
u32 LoadNcchFromGameFile(const char* path, NcchHeader* ncch, NcchExtHeader* extheader) {
u64 filetype = IdentifyFileType(path);
if (filetype & GAME_NCCH) {
if ((fvx_qread(path, ncch, 0, sizeof(NcchHeader), NULL) == FR_OK) &&
(ValidateNcchHeader(ncch) == 0) &&
(!extheader || ((fvx_qread(path, extheader, NCCH_EXTHDR_OFFSET, sizeof(NcchExtHeader), NULL) == FR_OK) &&
DecryptNcch(extheader, NCCH_EXTHDR_OFFSET, sizeof(NcchExtHeader), ncch, NULL) == 0))) return 0;
} else if (filetype & GAME_NCSD) {
if ((fvx_qread(path, ncch, NCSD_CNT0_OFFSET, sizeof(NcchHeader), NULL) == FR_OK) &&
(ValidateNcchHeader(ncch) == 0) &&
(!extheader || ((fvx_qread(path, extheader, NCSD_CINFO_OFFSET + NCCH_EXTHDR_OFFSET, sizeof(NcchExtHeader), NULL) == 0) &&
DecryptNcch(extheader, NCCH_EXTHDR_OFFSET, sizeof(NcchExtHeader), ncch, NULL) == 0))) return 0;
} else if (filetype & GAME_CDNTMD) {
return LoadNcchFromTmdFile(path, ncch, extheader);
} else if (filetype & GAME_CIA) {
CiaStub* cia = (CiaStub*) malloc(sizeof(CiaStub));
CiaInfo info;
// load CIA stub from path
if ((LoadCiaStub(cia, path) != 0) ||
(GetCiaInfo(&info, &(cia->header)) != 0)) {
free(cia);
return 1;
}
// decrypt / load NCCH header from first CIA content
u32 ret = 1;
if (getbe16(cia->tmd.content_count)) {
TmdContentChunk* chunk = cia->content_list;
if ((getbe64(chunk->size) < sizeof(NcchHeader)) ||
(fvx_qread(path, ncch, info.offset_content, sizeof(NcchHeader), NULL) != FR_OK) ||
(extheader && (fvx_qread(path, extheader, info.offset_content + NCCH_EXTHDR_OFFSET, sizeof(NcchExtHeader), NULL) != FR_OK))) {
free(cia);
return 1;
}
if (getbe16(chunk->type) & 0x1) { // decrypt first content header
u8 titlekey[16];
u8 ctr[16];
GetTmdCtr(ctr, chunk);
if (GetTitleKey(titlekey, (Ticket*)&(cia->ticket)) != 0) {
free(cia);
return 1;
}
DecryptCiaContentSequential((void*) ncch, sizeof(NcchHeader), ctr, titlekey);
if (extheader)
DecryptCiaContentSequential((void*) extheader, sizeof(NcchExtHeader), ctr, titlekey);
}
if (ValidateNcchHeader(ncch) == 0 &&
(!extheader || DecryptNcch(extheader, NCCH_EXTHDR_OFFSET, sizeof(NcchExtHeader), ncch, NULL) == 0)) ret = 0;
}
free(cia);
return ret;
}
return 1;
}
u32 VerifyGameFile(const char* path, bool sig_check) {
u64 filetype = IdentifyFileType(path);
if (filetype & GAME_CIA)
@ -1509,7 +1646,28 @@ u32 CryptGameFile(const char* path, bool inplace, bool encrypt, bool restore) {
return ret;
}
u32 GetInstallDataDrive(char* drv, u64 tid64, bool to_emunand) {
u32 GetTwlInstallDataDrive(char *drv, bool to_emunand) {
drv[0] = to_emunand ? '5' : '2';
drv[1] = ':';
drv[2] = '\0';
return 0;
}
u32 GetSdInstallDataDrive(char *drv, bool to_emunand) {
drv[0] = to_emunand ? 'B' : 'A';
drv[1] = ':';
drv[2] = '\0';
return 0;
}
u32 GetNandInstallDataDrive(char *drv, bool to_emunand) {
drv[0] = to_emunand ? '4' : '1';
drv[1] = ':';
drv[2] = '\0';
return 0;
}
u32 GetInstallDataDriveGeneric(char* drv, u64 tid64, bool to_emunand) {
// check the title id
bool to_twl = ((tid64 >> 32) & 0x8000);
bool to_sd = (!to_twl && !((tid64 >> 32) & 0x10));
@ -1517,14 +1675,8 @@ u32 GetInstallDataDrive(char* drv, u64 tid64, bool to_emunand) {
// sanity
if (!tid64) return 1;
// determine the correct drive
drv[0] = to_emunand ?
(to_twl ? '5' : to_sd ? 'B' : '4') :
(to_twl ? '2' : to_sd ? 'A' : '1');
drv[1] = ':';
drv[2] = '\0';
return 0;
return to_twl ? GetTwlInstallDataDrive(drv, to_emunand) :
to_sd ? GetSdInstallDataDrive(drv, to_emunand) : GetNandInstallDataDrive(drv, to_emunand);
}
u32 GetInstallDbsPath(char* path, const char* drv, const char* str) {
@ -1628,14 +1780,9 @@ u32 CreateSaveData(const char* drv, u64 tid64, const char* name, u32 save_size,
return 0;
}
u32 UninstallGameData(u64 tid64, bool remove_tie, bool remove_ticket, bool remove_save, bool from_emunand) {
char drv[3];
u32 UninstallGameData(const char *drv, u64 tid64, bool remove_tie, bool remove_ticket, bool remove_save) {
// check permissions for SysNAND (this includes everything we need)
if (!CheckWritePermissions(from_emunand ? "4:" : "1:")) return 1;
// determine the drive
if (GetInstallDataDrive(drv, tid64, from_emunand) != 0) return 1;
if (!CheckWritePermissions(drv)) return 1;
// remove data path
char path_data[256];
@ -1694,7 +1841,7 @@ u32 UninstallGameDataTie(const char* path, bool remove_tie, bool remove_ticket,
u64 tid64;
const char* mntpath = GetMountPath();
if (!mntpath) return 1;
if (!mntpath || !*mntpath) return 1;
// title.db from emunand?
if ((strncasecmp(mntpath, "B:/dbs/title.db", 16) == 0) ||
@ -1705,7 +1852,18 @@ u32 UninstallGameDataTie(const char* path, bool remove_tie, bool remove_ticket,
if (sscanf(path, "T:/%016llx", &tid64) != 1)
return 1;
return UninstallGameData(tid64, remove_tie, remove_ticket, remove_save, from_emunand);
// tid here is in the 00048000... format if it's a TWL title
bool is_twl = ((tid64 >> 32) & 0x8000) == 0x8000;
char drv[3] = { 0xFF, ':', '\0' };
// in all cases except for TWL titles, the uninstall drive is the same as the drive
// containing the .db file that is mounted.
if (is_twl)
drv[0] = from_emunand ? '5' : '2';
else
drv[0] = mntpath[0];
return UninstallGameData(drv, tid64, remove_tie, remove_ticket, remove_save);
}
u32 LoadEncryptedIconFromCiaTmd(const char* path, void* output, void* hdr, bool cia_meta) {
@ -2869,14 +3027,51 @@ u32 BuildCiaFromGameFile(const char* path, bool force_legit) {
return ret;
}
u32 GetInstallDataDriveForGameFile(const char *path, u64 filetype, u64 titleid, char *drv, bool to_emunand) {
// CTR filetypes
if (filetype & (GAME_CIA | GAME_NCSD | GAME_NCCH | GAME_CDNTMD)) {
if ((filetype & GAME_CIA) && (titleid >> 32) & 0x8000) {
// only CIAs may be either CTR or TWL titles
GetTwlInstallDataDrive(drv, to_emunand);
} else if ((titleid >> 32) == 0x0004008C || (filetype & GAME_NCSD)) {
// DLC, although it does not contain a CXI, must be installed to SD always
// gamecarts, and backups thereof, are also always installed to SD
GetSdInstallDataDrive(drv, to_emunand);
} else {
// definitely a CTR title
NcchHeader ncch;
NcchExtHeader extheader;
if (LoadNcchFromGameFile(path, &ncch, &extheader) != 0)
return 1;
if (!NCCH_IS_CXI(&ncch)) {
// for CFA, we don't have much to go off of. but CFAs can just be separated by the system title bit,
// as most CFAs are either part of system titles (NAND) or instruction manuals (SD).
if (GetInstallDataDriveGeneric(drv, titleid, to_emunand) != 0)
return 1;
} else {
// for files that are CXIs or files whose first content is a CXI, we can check the exheader for the SD bit
if (extheader.flag & BIT(1))
GetSdInstallDataDrive(drv, to_emunand);
else
GetNandInstallDataDrive(drv, to_emunand);
}
}
} else { // TWL file types (GAME_NDS, GAME_TWLTMD)
GetTwlInstallDataDrive(drv, to_emunand);
}
return 0;
}
u32 InstallGameFile(const char* path, bool to_emunand) {
char drv[3];
u64 filetype = IdentifyFileType(path);
u64 tid64 = GetGameFileTitleId(path);
u32 ret = 0;
// find out the destination
u64 tid64 = GetGameFileTitleId(path);
if (GetInstallDataDrive(drv, tid64, to_emunand) != 0)
if (GetInstallDataDriveForGameFile(path, filetype, tid64, drv, to_emunand) != 0)
return 1;
// check dbs
@ -2893,7 +3088,7 @@ u32 InstallGameFile(const char* path, bool to_emunand) {
// cleanup content folder before starting install
ShowProgress(0, 0, path);
UninstallGameData(tid64, false, false, false, to_emunand);
UninstallGameData(drv, tid64, false, false, false);
// install game file
if (filetype & GAME_CIA)
@ -2909,7 +3104,7 @@ u32 InstallGameFile(const char* path, bool to_emunand) {
else ret = 1;
// cleanup on failed installs, but leave ticket and save untouched
if (ret != 0) UninstallGameData(tid64, true, false, false, to_emunand);
if (ret != 0) UninstallGameData(drv, tid64, true, false, false);
return ret;
}
@ -4008,54 +4203,6 @@ u32 BuildSeedInfo(const char* path, bool dump) {
return 0;
}
u32 LoadNcchFromGameFile(const char* path, NcchHeader* ncch) {
u64 filetype = IdentifyFileType(path);
if (filetype & GAME_NCCH) {
if ((fvx_qread(path, ncch, 0, sizeof(NcchHeader), NULL) == FR_OK) &&
(ValidateNcchHeader(ncch) == 0)) return 0;
} else if (filetype & GAME_NCSD) {
if ((fvx_qread(path, ncch, NCSD_CNT0_OFFSET, sizeof(NcchHeader), NULL) == FR_OK) &&
(ValidateNcchHeader(ncch) == 0)) return 0;
} else if (filetype & GAME_CIA) {
CiaStub* cia = (CiaStub*) malloc(sizeof(CiaStub));
CiaInfo info;
// load CIA stub from path
if ((LoadCiaStub(cia, path) != 0) ||
(GetCiaInfo(&info, &(cia->header)) != 0)) {
free(cia);
return 1;
}
// decrypt / load NCCH header from first CIA content
u32 ret = 1;
if (getbe16(cia->tmd.content_count)) {
TmdContentChunk* chunk = cia->content_list;
if ((getbe64(chunk->size) < sizeof(NcchHeader)) ||
(fvx_qread(path, ncch, info.offset_content, sizeof(NcchHeader), NULL) != FR_OK)) {
free(cia);
return 1;
}
if (getbe16(chunk->type) & 0x1) { // decrypt first content header
u8 titlekey[16];
u8 ctr[16];
GetTmdCtr(ctr, chunk);
if (GetTitleKey(titlekey, (Ticket*)&(cia->ticket)) != 0) {
free(cia);
return 1;
}
DecryptCiaContentSequential((void*) ncch, sizeof(NcchHeader), ctr, titlekey);
}
if (ValidateNcchHeader(ncch) == 0) ret = 0;
}
free(cia);
return ret;
}
return 1;
}
u32 GetGoodName(char* name, const char* path, bool quick) {
// name should be 128+1 byte
@ -4117,7 +4264,7 @@ u32 GetGoodName(char* name, const char* path, bool quick) {
if (LoadTwlMetaData(path_donor, (TwlHeader*) header,
quick ? NULL : (TwlIconData*) icon) != 0) type_donor = 0;
} else if (type_donor & (GAME_NCSD|GAME_NCCH)) { // CTR (data from NCCH)
if (LoadNcchFromGameFile(path_donor, (NcchHeader*) header) != 0) type_donor = 0;
if (LoadNcchFromGameFile(path_donor, (NcchHeader*) header, NULL) != 0) type_donor = 0;
if (!quick && (LoadSmdhFromGameFile(path_donor, (Smdh*) icon) != 0)) quick = true;
} else if (type_donor & (GAME_CIA|GAME_CDNTMD|GAME_TWLTMD)) { // encrypted
type_donor = LoadEncryptedIconFromCiaTmd(path_donor, icon, header, false);