1
0
Fork 0
mirror of https://github.com/DarkMatterCore/nxdumptool.git synced 2024-11-22 18:26:39 +00:00
nxdumptool/source/core/nxdt_utils.c
Pablo Curiel f376eb6db4 keys: relax mkey requirements on older firmwares.
A hardcoded table with HOS version numbers and master key indexes is now used to determine the HOS key generation at runtime, whenever possible. This allows the application to more accurately determine the key generation that's actually required by the console it's running on.

Most parts of the code that relied on the Atmosphère key generation value have been updated to use the HOS key generation value instead. If the HOS version is too high/unknown, the code will fallback to the Atmosphère key generation value.

Furthermore, if the HOS key generation value is lower than our last known key generation, the code will now try to look for the highest available master key it can use to derive all lower master keys, beginning with the last known master key and ending with the master key that matches the HOS key generation value. Previous behavior only checked the availability of the master key that matched the Atmosphère key generation, which isn't completely reliable nor accurate.

If this process fails, current master key derivation will be carried out as a last resort, which wasn't being done either under this specific scenario.

Other changes include:

* keys: add keysGetHorizonOsKeyGeneration().
* keys: move current master key derivation logic into its own function, keysDeriveCurrentMasterKey(), which is now used if both Atmosphère and HOS and up-to-date, or if a lower master key is required (as a last resort method).
2024-10-12 20:38:33 +02:00

1578 lines
44 KiB
C

/*
* nxdt_utils.c
*
* Copyright (c) 2020-2024, 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 of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nxdumptool is distributed in the hope that 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 <https://www.gnu.org/licenses/>.
*/
#include <sys/statvfs.h>
#include <core/nxdt_utils.h>
#include <core/keys.h>
#include <core/gamecard.h>
#include <core/services.h>
#include <core/nca.h>
#include <core/usb.h>
#include <core/title.h>
#include <core/bfttf.h>
#include <core/nxdt_bfsar.h>
#include <core/devoptab/nxdt_devoptab.h>
#include <core/fatfs/ff.h>
/* Type definitions. */
/* Reference: https://github.com/Atmosphere-NX/Atmosphere/blob/master/exosphere/program/source/smc/secmon_smc_info.hpp. */
typedef struct {
SdkAddOnVersion target_firmware;
u8 key_generation;
u8 ams_ver_micro;
u8 ams_ver_minor;
u8 ams_ver_major;
} UtilsExosphereApiVersion;
NXDT_ASSERT(UtilsExosphereApiVersion, 0x8);
typedef struct {
u32 major;
u32 minor;
u32 micro;
} UtilsApplicationVersion;
/* Global variables. */
extern int __system_argc;
extern char **__system_argv;
static bool g_resourcesInit = false;
static Mutex g_resourcesMutex = 0;
static const char *g_appLaunchPath = NULL;
static FsFileSystem *g_sdCardFileSystem = NULL;
static int g_nxLinkSocketFd = -1;
static u8 g_customFirmwareType = UtilsCustomFirmwareType_Unknown;
static u8 g_productModel = SetSysProductModel_Invalid;
static bool g_isTerraUnit = false, g_isDevUnit = false;
static AppletType g_programAppletType = AppletType_None;
static FsStorage g_emmcBisSystemPartitionStorage = {0};
static FATFS *g_emmcBisSystemPartitionFatFsObj = NULL;
static AppletHookCookie g_systemOverclockCookie = {0};
static bool g_longRunningProcess = false;
static const char *g_sizeSuffixes[] = { "B", "KiB", "MiB", "GiB", "TiB" };
static const u32 g_sizeSuffixesCount = MAX_ELEMENTS(g_sizeSuffixes);
static const char g_illegalFileSystemChars[] = "\\/:*?\"<>|";
static const size_t g_illegalFileSystemCharsLength = (MAX_ELEMENTS(g_illegalFileSystemChars) - 1);
static bool g_appUpdated = false;
static const SplConfigItem SplConfigItem_ExosphereApiVersion = (SplConfigItem)65000;
static const SplConfigItem SplConfigItem_ExosphereEmummcType = (SplConfigItem)65007;
static UtilsExosphereApiVersion g_exosphereApiVersion = {0};
static bool g_exosphereIsEmummc = false;
/* Function prototypes. */
static void _utilsGetLaunchPath(void);
static bool utilsGetExosphereApiVersion(void);
static bool utilsGetExosphereEmummcType(void);
static void _utilsGetCustomFirmwareType(void);
static bool _utilsGetProductModel(void);
static bool utilsGetDevelopmentUnitFlag(void);
static bool utilsGetTerraUnitFlag(void);
static bool utilsMountEmmcBisSystemPartitionStorage(void);
static void utilsUnmountEmmcBisSystemPartitionStorage(void);
static void utilsOverclockSystem(bool overclock);
static void utilsOverclockSystemAppletHook(AppletHookType hook, void *param);
static void utilsChangeHomeButtonBlockStatus(bool block);
static size_t utilsGetUtf8StringLimit(const char *str, size_t str_size, size_t byte_limit);
static char utilsConvertHexDigitToBinary(char c);
bool utilsInitializeResources(void)
{
Result rc = 0;
bool ret = false;
SCOPED_LOCK(&g_resourcesMutex)
{
ret = g_resourcesInit;
if (ret) break;
/* Lock applet exit. */
appletLockExit();
/* Retrieve pointer to the application launch path. */
_utilsGetLaunchPath();
/* Retrieve pointer to the SD card FsFileSystem element. */
if (!(g_sdCardFileSystem = fsdevGetDeviceFileSystem(DEVOPTAB_SDMC_DEVICE)))
{
LOG_MSG_ERROR("Failed to retrieve FsFileSystem object for the SD card!");
break;
}
/*FsFileSystemAttribute fs_attr = {0};
if (R_SUCCEEDED(fsFsGetFileSystemAttribute(g_sdCardFileSystem, &fs_attr)))
{
LOG_DATA_INFO(&fs_attr, sizeof(FsFileSystemAttribute), "SD card FS attributes:");
}*/
/* Initialize needed services. */
if (!servicesInitialize()) break;
/* Check if a valid nxlink host IP address was set by libnx. */
/* If so, initialize nxlink connection without redirecting stdout and/or stderr. */
if (__nxlink_host.s_addr != 0 && __nxlink_host.s_addr != INADDR_NONE) g_nxLinkSocketFd = nxlinkConnectToHost(false, false);
#if LOG_LEVEL <= LOG_LEVEL_INFO
/* Log info messages. */
LOG_MSG_INFO(APP_TITLE " v" APP_VERSION " starting (" GIT_REV "). Built on " BUILD_TIMESTAMP ".");
if (g_nxLinkSocketFd >= 0) LOG_MSG_INFO("nxlink enabled! Host IP address: %s.", inet_ntoa(__nxlink_host));
#endif
/* Retrieve Exosphère API version. */
if (!utilsGetExosphereApiVersion())
{
LOG_MSG_ERROR("Failed to retrieve Exosphère API version!");
break;
}
/* Retrieve Exosphère emuMMC status. */
if (!utilsGetExosphereEmummcType())
{
LOG_MSG_ERROR("Failed to retrieve Exosphère emuMMC status!");
break;
}
/* Retrieve custom firmware type. */
_utilsGetCustomFirmwareType();
/* Get product model. */
if (!_utilsGetProductModel()) break;
/* Get development unit flag. */
if (!utilsGetDevelopmentUnitFlag()) break;
/* Get Terra unit flag. */
if (!utilsGetTerraUnitFlag()) break;
/* Get applet type. */
g_programAppletType = appletGetAppletType();
#if LOG_LEVEL <= LOG_LEVEL_INFO
/* Log info messages. */
u32 hos_version = hosversionGet();
LOG_MSG_INFO("Console info:\r\n" \
"- Horizon OS version: %u.%u.%u.\r\n" \
"- CFW: %s.\r\n" \
"- eMMC type: %s.\r\n" \
"- SoC type: %s.\r\n" \
"- Development unit: %s.\r\n" \
"- Terra flag: %s.\r\n" \
"- Execution mode: %s.", \
HOSVER_MAJOR(hos_version), HOSVER_MINOR(hos_version), HOSVER_MICRO(hos_version), \
(g_customFirmwareType == UtilsCustomFirmwareType_Atmosphere ? "Atmosphère" : (g_customFirmwareType == UtilsCustomFirmwareType_SXOS ? "SX OS" : g_customFirmwareType == UtilsCustomFirmwareType_ReiNX ? "ReiNX" : "Unknown")), \
g_exosphereIsEmummc ? "emuMMC" : "sysMMC", \
utilsIsMarikoUnit() ? "Mariko" : "Erista", \
g_isDevUnit ? "yes" : "no", \
g_isTerraUnit ? "yes" : "no", \
utilsIsAppletMode() ? "applet" : "title override");
LOG_MSG_INFO("Exosphère API version info:\r\n" \
"- Release version: %u.%u.%u.\r\n" \
"- PKG1 key generation: %u (0x%02X).\r\n" \
"- Target firmware: %u.%u.%u.", \
g_exosphereApiVersion.ams_ver_major, g_exosphereApiVersion.ams_ver_minor, g_exosphereApiVersion.ams_ver_micro, \
g_exosphereApiVersion.key_generation, !g_exosphereApiVersion.key_generation ? g_exosphereApiVersion.key_generation : (g_exosphereApiVersion.key_generation + 1), \
g_exosphereApiVersion.target_firmware.major, g_exosphereApiVersion.target_firmware.minor, g_exosphereApiVersion.target_firmware.micro);
#endif
if (g_appLaunchPath)
{
LOG_MSG_INFO("Launch path: \"%s\".", g_appLaunchPath);
/* Move NRO if the launch path isn't the right one, then return. */
/* TODO: uncomment this block whenever we are ready for a release. */
/*if (strcmp(g_appLaunchPath, NRO_PATH) != 0)
{
utilsCreateDirectoryTree(NRO_PATH, false);
remove(NRO_PATH);
rename(g_appLaunchPath, NRO_PATH);
LOG_MSG_INFO("Moved NRO to \"%s\". Please reload the application.", NRO_PATH);
break;
}*/
}
/* Initialize HTTP interface. */
/* cURL must be initialized before starting any other threads. */
if (!httpInitialize()) break;
/* Initialize USB interface. */
if (!usbInitialize()) break;
/* Initialize USB Mass Storage interface. */
if (!umsInitialize()) break;
/* Load keyset. */
if (!keysLoadKeyset()) break;
/* Allocate NCA crypto buffer. */
if (!ncaAllocateCryptoBuffer())
{
LOG_MSG_ERROR("Unable to allocate memory for NCA crypto buffer!");
break;
}
/* Initialize gamecard interface. */
if (!gamecardInitialize()) break;
/* Initialize title interface. */
if (!titleInitialize()) break;
/* Initialize BFTTF interface. */
if (!bfttfInitialize()) break;
/* Initialize BFSAR interface. */
//if (!bfsarInitialize()) break;
/* Mount eMMC BIS System partition. */
if (!utilsMountEmmcBisSystemPartitionStorage()) break;
/* Mount application RomFS. */
rc = romfsInit();
if (R_FAILED(rc))
{
LOG_MSG_ERROR("Failed to mount " APP_TITLE "'s RomFS container!");
break;
}
/* Initialize configuration interface. */
if (!configInitialize()) break;
/* Setup an applet hook to change the hardware clocks after a system mode change (docked <-> undocked). */
appletHook(&g_systemOverclockCookie, utilsOverclockSystemAppletHook, NULL);
/* Enable video recording if we're running under title override mode. */
if (!utilsIsAppletMode())
{
bool flag = false;
rc = appletIsGamePlayRecordingSupported(&flag);
if (R_SUCCEEDED(rc) && flag)
{
rc = appletInitializeGamePlayRecording();
if (R_FAILED(rc)) LOG_MSG_ERROR("appletInitializeGamePlayRecording failed! (0x%X).", rc);
} else {
LOG_MSG_ERROR("appletIsGamePlayRecordingSupported returned [0x%X, %u].", rc, flag);
}
}
/* Update flags. */
ret = g_resourcesInit = true;
}
if (!ret)
{
char *msg = NULL;
size_t msg_size = 0;
/* Generate error message. */
utilsAppendFormattedStringToBuffer(&msg, &msg_size, "An error occurred while initializing resources.");
#if LOG_LEVEL <= LOG_LEVEL_ERROR
/* Get last log message. */
char *log_msg = logGetLastMessage();
if (log_msg)
{
utilsAppendFormattedStringToBuffer(&msg, &msg_size, "\n\n%s", log_msg);
free(log_msg);
}
#endif
/* Print error message. */
utilsPrintConsoleError(msg);
/* Free error message. */
if (msg) free(msg);
}
return ret;
}
void utilsCloseResources(void)
{
SCOPED_LOCK(&g_resourcesMutex)
{
LOG_MSG_INFO("Shutting down...");
/* Unmount all custom devoptab devices. */
devoptabUnmountAllDevices();
/* Unset long running process state. */
utilsSetLongRunningProcessState(false);
/* Unset our overclock applet hook. */
appletUnhook(&g_systemOverclockCookie);
/* Close configuration interface. */
configExit();
/* Unmount application RomFS. */
romfsExit();
/* Unmount eMMC BIS System partition. */
utilsUnmountEmmcBisSystemPartitionStorage();
/* Deinitialize BFSAR interface. */
//bfsarExit();
/* Deinitialize BFTTF interface. */
bfttfExit();
/* Deinitialize title interface. */
titleExit();
/* Deinitialize gamecard interface. */
gamecardExit();
/* Free NCA crypto buffer. */
ncaFreeCryptoBuffer();
/* Close USB Mass Storage interface. */
umsExit();
/* Close USB interface. */
usbExit();
/* Close HTTP interface. */
httpExit();
/* Close nxlink socket. */
if (g_nxLinkSocketFd >= 0)
{
close(g_nxLinkSocketFd);
g_nxLinkSocketFd = -1;
}
/* Close initialized services. */
servicesClose();
/* Replace application NRO (if needed). */
/* TODO: uncomment this block whenever we're ready for a release. */
/*if (g_appUpdated)
{
remove(NRO_PATH);
rename(NRO_TMP_PATH, NRO_PATH);
}*/
#if LOG_LEVEL <= LOG_LEVEL_ERROR
/* Close logfile. */
logCloseLogFile();
#endif
/* Unlock applet exit. */
appletUnlockExit();
g_resourcesInit = false;
}
}
const char *utilsGetLaunchPath(void)
{
return g_appLaunchPath;
}
int utilsGetNxLinkFileDescriptor(void)
{
return g_nxLinkSocketFd;
}
FsFileSystem *utilsGetSdCardFileSystemObject(void)
{
return g_sdCardFileSystem;
}
bool utilsCommitSdCardFileSystemChanges(void)
{
return (g_sdCardFileSystem ? R_SUCCEEDED(fsFsCommit(g_sdCardFileSystem)) : false);
}
u32 utilsGetAtmosphereVersion(void)
{
return MAKEHOSVERSION(g_exosphereApiVersion.ams_ver_major, g_exosphereApiVersion.ams_ver_minor, g_exosphereApiVersion.ams_ver_micro);
}
u8 utilsGetAtmosphereKeyGeneration(void)
{
return g_exosphereApiVersion.key_generation;
}
void utilsGetAtmosphereTargetFirmware(SdkAddOnVersion *out)
{
memcpy(out, &(g_exosphereApiVersion.target_firmware), sizeof(SdkAddOnVersion));
}
bool utilsGetAtmosphereEmummcStatus(void)
{
return g_exosphereIsEmummc;
}
u8 utilsGetCustomFirmwareType(void)
{
return g_customFirmwareType;
}
bool utilsIsMarikoUnit(void)
{
return (g_productModel > SetSysProductModel_Copper);
}
bool utilsIsDevelopmentUnit(void)
{
return g_isDevUnit;
}
bool utilsIsTerraUnit(void)
{
return g_isTerraUnit;
}
bool utilsIsAppletMode(void)
{
return (g_programAppletType > AppletType_Application && g_programAppletType < AppletType_SystemApplication);
}
FsStorage *utilsGetEmmcBisSystemPartitionStorage(void)
{
return &g_emmcBisSystemPartitionStorage;
}
void utilsSetLongRunningProcessState(bool state)
{
SCOPED_LOCK(&g_resourcesMutex)
{
/* Don't proceed if resources haven't been initialized, or if the requested state matches the current one. */
if (!g_resourcesInit || state == g_longRunningProcess) break;
/* Change HOME button block status. */
utilsChangeHomeButtonBlockStatus(state);
/* Enable/disable screen dimming and auto sleep. */
appletSetMediaPlaybackState(state);
/* Enable/disable system overclock. */
utilsOverclockSystem(configGetBoolean("overclock") & state);
/* Update flag. */
g_longRunningProcess = state;
}
}
bool utilsCreateThread(Thread *out_thread, ThreadFunc func, void *arg, int cpu_id)
{
/* Core 3 is reserved for HOS, so we can only use cores 0, 1 and 2. */
/* -2 can be provided to use the default process core. */
if (!out_thread || !func || (cpu_id < 0 && cpu_id != -2) || cpu_id > 2)
{
LOG_MSG_ERROR("Invalid parameters!");
return false;
}
Result rc = 0;
u64 core_mask = 0;
size_t stack_size = 0x20000; /* Same value as libnx's newlib. */
bool success = false;
memset(out_thread, 0, sizeof(Thread));
/* Get process core mask. */
rc = svcGetInfo(&core_mask, InfoType_CoreMask, CUR_PROCESS_HANDLE, 0);
if (R_FAILED(rc))
{
LOG_MSG_ERROR("svcGetInfo failed! (0x%X).", rc);
goto end;
}
/* Create thread. */
/* Enable preemptive multithreading by using priority 0x3B. */
rc = threadCreate(out_thread, func, arg, NULL, stack_size, 0x3B, cpu_id);
if (R_FAILED(rc))
{
LOG_MSG_ERROR("threadCreate failed! (0x%X).", rc);
goto end;
}
/* Set thread core mask. */
rc = svcSetThreadCoreMask(out_thread->handle, cpu_id == -2 ? -1 : cpu_id, core_mask);
if (R_FAILED(rc))
{
LOG_MSG_ERROR("svcSetThreadCoreMask failed! (0x%X).", rc);
goto end;
}
/* Start thread. */
rc = threadStart(out_thread);
if (R_FAILED(rc))
{
LOG_MSG_ERROR("threadStart failed! (0x%X).", rc);
goto end;
}
success = true;
end:
if (!success && out_thread->handle != INVALID_HANDLE) threadClose(out_thread);
return success;
}
void utilsJoinThread(Thread *thread)
{
if (!thread || thread->handle == INVALID_HANDLE)
{
LOG_MSG_ERROR("Invalid parameters!");
return;
}
Result rc = threadWaitForExit(thread);
if (R_FAILED(rc))
{
LOG_MSG_ERROR("threadWaitForExit failed! (0x%X).", rc);
return;
}
threadClose(thread);
memset(thread, 0, sizeof(Thread));
}
__attribute__((format(printf, 3, 4))) bool utilsAppendFormattedStringToBuffer(char **dst, size_t *dst_size, const char *fmt, ...)
{
bool use_log = false;
SCOPED_LOCK(&g_resourcesMutex) use_log = g_resourcesInit;
if (!dst || !dst_size || !fmt || !*fmt)
{
if (use_log) LOG_MSG_ERROR("Invalid parameters!");
return false;
}
va_list args;
int formatted_str_len = 0;
size_t formatted_str_len_cast = 0;
char *dst_ptr = *dst, *tmp_str = NULL;
size_t dst_cur_size = *dst_size, dst_str_len = (dst_ptr ? strlen(dst_ptr) : 0);
bool success = false;
/* Sanity check. */
if (dst_cur_size && dst_str_len >= dst_cur_size)
{
if (use_log) LOG_MSG_ERROR("String length is equal to or greater than the provided buffer size! (0x%lX >= 0x%lX).", dst_str_len, dst_cur_size);
return false;
}
va_start(args, fmt);
/* Get formatted string length. */
formatted_str_len = vsnprintf(NULL, 0, fmt, args);
if (formatted_str_len <= 0)
{
if (use_log) LOG_MSG_ERROR("Failed to retrieve formatted string length!");
goto end;
}
formatted_str_len_cast = (size_t)(formatted_str_len + 1);
if (!dst_ptr || !dst_cur_size || formatted_str_len_cast > (dst_cur_size - dst_str_len))
{
/* Update buffer size. */
dst_cur_size = (dst_str_len + formatted_str_len_cast);
/* Reallocate buffer. */
tmp_str = realloc(dst_ptr, dst_cur_size);
if (!tmp_str)
{
if (use_log) LOG_MSG_ERROR("Failed to resize buffer to 0x%lX byte(s).", dst_cur_size);
goto end;
}
dst_ptr = tmp_str;
tmp_str = NULL;
/* Clear allocated area. */
memset(dst_ptr + dst_str_len, 0, formatted_str_len_cast);
/* Update pointers. */
*dst = dst_ptr;
*dst_size = dst_cur_size;
}
/* Generate formatted string. */
vsprintf(dst_ptr + dst_str_len, fmt, args);
/* Update output flag. */
success = true;
end:
va_end(args);
return success;
}
void utilsReplaceIllegalCharacters(char *str, bool ascii_only)
{
size_t str_size = 0, cur_pos = 0;
if (!str || !(str_size = strlen(str))) return;
u8 *ptr1 = (u8*)str, *ptr2 = ptr1;
ssize_t units = 0;
u32 code = 0;
bool repl = false;
while(cur_pos < str_size)
{
units = decode_utf8(&code, ptr1);
if (units < 0) break;
if (memchr(g_illegalFileSystemChars, (int)code, g_illegalFileSystemCharsLength) || code < 0x20 || (!ascii_only && code == 0x7F) || (ascii_only && code >= 0x7F))
{
if (!repl)
{
*ptr2++ = '_';
repl = true;
}
} else {
if (ptr2 != ptr1) memmove(ptr2, ptr1, (size_t)units);
ptr2 += units;
repl = false;
}
ptr1 += units;
cur_pos += (size_t)units;
}
*ptr2 = '\0';
}
char *utilsEscapeCharacters(const char *str, const char *chars_to_escape, const char escape_char)
{
size_t str_size = 0, chars_to_escape_size = 0;
if (!str || !(str_size = strlen(str)) || !chars_to_escape || !(chars_to_escape_size = strlen(chars_to_escape)) || \
escape_char < 0x20 || escape_char >= 0x7F || memchr(chars_to_escape, (int)escape_char, chars_to_escape_size))
{
LOG_MSG_ERROR("Invalid parameters!");
return NULL;
}
ssize_t units = 0;
u32 code = 0, escape_cnt = 0;
const u8 *ptr = (const u8*)str;
size_t cur_pos = 0, escaped_str_size = 0;
char *ret = NULL;
/* Determine the number of character we need to escape. */
while(cur_pos < str_size)
{
units = decode_utf8(&code, ptr);
if (units < 0) break;
if (memchr(chars_to_escape, (int)code, chars_to_escape_size)) escape_cnt++;
ptr += units;
cur_pos += (size_t)units;
}
/* Short-circuit: check if we don't have to escape anything. */
/* If so, we'll just duplicate the provided string and call it a day. */
if (!escape_cnt)
{
ret = strdup(str);
goto end;
}
/* Calculate escaped string size. */
escaped_str_size = (str_size + escape_cnt);
/* Allocate memory for the output string. */
ret = calloc(sizeof(char), escaped_str_size + 1);
if (!ret)
{
LOG_MSG_ERROR("Failed to allocate memory for the output string! (0x%lX).", escaped_str_size + 1);
goto end;
}
/* Reset current position. */
ptr = (const u8*)str;
cur_pos = 0;
/* Copy characters and deal with the ones that need to be escaped. */
while(cur_pos < escaped_str_size)
{
units = decode_utf8(&code, ptr);
if (units < 0) break;
if (memchr(chars_to_escape, (int)code, chars_to_escape_size)) ret[cur_pos++] = escape_char;
for(ssize_t i = 0; i < units; i++) ret[cur_pos + (size_t)i] = ptr[i];
ptr += units;
cur_pos += (size_t)units;
}
end:
return ret;
}
void utilsTrimString(char *str)
{
size_t strsize = 0;
char *start = NULL, *end = NULL;
if (!str || !(strsize = strlen(str))) return;
start = str;
end = (start + strsize);
while(--end >= start)
{
if (!isspace((unsigned char)*end)) break;
}
*(++end) = '\0';
while(isspace((unsigned char)*start)) start++;
if (start != str) memmove(str, start, end - start + 1);
}
void utilsGenerateHexString(char *dst, size_t dst_size, const void *src, size_t src_size, bool uppercase)
{
if (!src || !src_size || !dst || dst_size < ((src_size * 2) + 1)) return;
size_t i, j;
const u8 *src_u8 = (const u8*)src;
for(i = 0, j = 0; i < src_size; i++)
{
char h_nib = ((src_u8[i] >> 4) & 0xF);
char l_nib = (src_u8[i] & 0xF);
dst[j++] = (h_nib + (h_nib < 0xA ? 0x30 : (uppercase ? 0x37 : 0x57)));
dst[j++] = (l_nib + (l_nib < 0xA ? 0x30 : (uppercase ? 0x37 : 0x57)));
}
dst[j] = '\0';
}
bool utilsParseHexString(void *dst, size_t dst_size, const char *src, size_t src_size)
{
u8 *dst_u8 = (u8*)dst;
bool success = true;
if (!dst || !dst_size || !src || !*src || (!src_size && !(src_size = strlen(src))) || (src_size % 2) != 0 || dst_size < (src_size / 2))
{
LOG_MSG_ERROR("Invalid parameters!");
return false;
}
memset(dst, 0, dst_size);
for(size_t i = 0; i < src_size; i++)
{
char val = utilsConvertHexDigitToBinary(src[i]);
if (val == 'z')
{
LOG_MSG_ERROR("Invalid hex character in string \"%s\" at position %lu!", src, i);
success = false;
break;
}
if ((i & 1) == 0) val <<= 4;
dst_u8[i >> 1] |= val;
}
return success;
}
void utilsGenerateFormattedSizeString(double size, char *dst, size_t dst_size)
{
if (!dst || dst_size < 2) return;
size = fabs(size);
for(u32 i = 0; i < g_sizeSuffixesCount; i++)
{
if (size >= pow(1024.0, i + 1) && (i + 1) < g_sizeSuffixesCount) continue;
size /= pow(1024.0, i);
/* Don't display decimal places if we're dealing with plain bytes. */
snprintf(dst, dst_size, "%.*f %s", i == 0 ? 0 : 2, size, g_sizeSuffixes[i]);
break;
}
}
bool utilsGetFileSystemStatsByPath(const char *path, u64 *out_total, u64 *out_free)
{
char *name_end = NULL, stat_path[32] = {0};
struct statvfs info = {0};
int ret = -1;
if (!path || !*path || !(name_end = strchr(path, ':')) || *(name_end + 1) != '/' || (!out_total && !out_free))
{
LOG_MSG_ERROR("Invalid parameters!");
return false;
}
name_end += 2;
snprintf(stat_path, MAX_ELEMENTS(stat_path), "%.*s", (int)(name_end - path), path);
if ((ret = statvfs(stat_path, &info)) != 0)
{
LOG_MSG_ERROR("statvfs failed for \"%s\"! (%d) (errno: %d).", stat_path, ret, errno);
return false;
}
if (out_total) *out_total = ((u64)info.f_blocks * (u64)info.f_frsize);
if (out_free) *out_free = ((u64)info.f_bfree * (u64)info.f_frsize);
return true;
}
bool utilsCheckIfFileExists(const char *path)
{
if (!path || !*path) return false;
FILE *chkfile = fopen(path, "rb");
if (chkfile)
{
fclose(chkfile);
return true;
}
return false;
}
void utilsRemoveConcatenationFile(const char *path)
{
if (!path || !*path) return;
remove(path);
fsdevDeleteDirectoryRecursively(path);
}
bool utilsCreateConcatenationFile(const char *path)
{
if (!path || !*path)
{
LOG_MSG_ERROR("Invalid parameters!");
return false;
}
/* Safety measure: remove any existant file/directory at the destination path. */
utilsRemoveConcatenationFile(path);
/* Create ConcatenationFile. */
/* If the call succeeds, the caller function will be able to operate on this file using stdio calls. */
Result rc = fsdevCreateFile(path, 0, FsCreateOption_BigFile);
if (R_FAILED(rc)) LOG_MSG_ERROR("fsdevCreateFile failed for \"%s\"! (0x%X).", path, rc);
return R_SUCCEEDED(rc);
}
void utilsCreateDirectoryTree(const char *path, bool create_last_element)
{
char *ptr = NULL, *tmp = NULL;
size_t path_len = 0;
if (!path || !(path_len = strlen(path))) return;
tmp = calloc(path_len + 1, sizeof(char));
if (!tmp) return;
ptr = strchr(path, '/');
while(ptr)
{
sprintf(tmp, "%.*s", (int)(ptr - path), path);
mkdir(tmp, 0777);
ptr = strchr(++ptr, '/');
}
if (create_last_element) mkdir(path, 0777);
free(tmp);
}
bool utilsGetDirectorySize(const char *path, u64 *out_size)
{
u64 total_size = 0;
char *name_end = NULL, *entry_path = NULL;
DIR *dir = NULL;
struct dirent *entry = NULL;
struct stat st = {0};
bool success = false;
/* Sanity checks. */
if (!path || !*path || !(name_end = strchr(path, ':')) || *(name_end + 1) != '/' || !*(name_end + 2) || !out_size)
{
LOG_MSG_ERROR("Invalid parameters!");
return false;
}
if (!(dir = opendir(path)))
{
LOG_MSG_ERROR("Failed to open directory \"%s\"! (%d).", path, errno);
goto end;
}
if (!(entry_path = calloc(1, FS_MAX_PATH)))
{
LOG_MSG_ERROR("Failed to allocate memory for path buffer!");
goto end;
}
/* Read directory entries. */
while((entry = readdir(dir)))
{
/* Skip current directory and parent directory entries. */
if (!strcmp(".", entry->d_name) || !strcmp("..", entry->d_name)) continue;
/* Generate path to the current entry. */
snprintf(entry_path, FS_MAX_PATH, "%s/%s", path, entry->d_name);
if (entry->d_type == DT_DIR)
{
/* Get directory size. */
u64 dir_size = 0;
if (!utilsGetDirectorySize(entry_path, &dir_size)) goto end;
/* Update size. */
total_size += dir_size;
} else {
/* Get file properties. */
if (stat(entry_path, &st) != 0)
{
LOG_MSG_ERROR("Failed to stat file \"%s\"! (%d).", entry_path, errno);
goto end;
}
/* Update size. */
total_size += st.st_size;
}
}
/* Update output pointer. */
*out_size = total_size;
/* Update return value. */
success = true;
end:
if (entry_path) free(entry_path);
if (dir) closedir(dir);
return success;
}
bool utilsDeleteDirectoryRecursively(const char *path)
{
char *name_end = NULL, *entry_path = NULL;
DIR *dir = NULL;
struct dirent *entry = NULL;
bool success = false;
/* Sanity checks. */
if (!path || !*path || !(name_end = strchr(path, ':')) || *(name_end + 1) != '/' || !*(name_end + 2))
{
LOG_MSG_ERROR("Invalid parameters!");
return false;
}
if (!(dir = opendir(path)))
{
LOG_MSG_ERROR("Failed to open directory \"%s\"! (%d).", path, errno);
goto end;
}
if (!(entry_path = calloc(1, FS_MAX_PATH)))
{
LOG_MSG_ERROR("Failed to allocate memory for path buffer!");
goto end;
}
/* Read directory entries. */
while((entry = readdir(dir)))
{
int status = 0;
/* Skip current directory and parent directory entries. */
if (!strcmp(".", entry->d_name) || !strcmp("..", entry->d_name)) continue;
/* Generate path to the current entry. */
snprintf(entry_path, FS_MAX_PATH, "%s/%s", path, entry->d_name);
if (entry->d_type == DT_DIR)
{
/* Delete directory contents. */
if (!utilsDeleteDirectoryRecursively(entry_path)) goto end;
/* Delete directory. */
status = rmdir(entry_path);
} else {
/* Delete file. */
status = unlink(entry_path);
}
if (status != 0)
{
LOG_MSG_ERROR("Failed to delete %s \"%s\"! (%d).", entry->d_type == DT_DIR ? "directory" : "file", entry_path, errno);
goto end;
}
}
/* Close topmost directory so we can delete it. */
closedir(dir);
dir = NULL;
success = (rmdir(path) == 0);
if (!success) LOG_MSG_ERROR("Failed to delete topmost directory \"%s\"! (%d).", path, errno);
end:
if (entry_path) free(entry_path);
if (dir) closedir(dir);
return success;
}
char *utilsGeneratePath(const char *prefix, const char *filename, const char *extension)
{
if (!filename || !*filename)
{
LOG_MSG_ERROR("Invalid parameters!");
return NULL;
}
bool use_prefix = (prefix && *prefix);
size_t prefix_len = (use_prefix ? strlen(prefix) : 0);
bool append_path_sep = (use_prefix && prefix[prefix_len - 1] != '/' && *filename != '/');
bool use_extension = (extension && *extension);
size_t extension_len = (use_extension ? strlen(extension) : 0);
size_t path_len = (prefix_len + strlen(filename) + extension_len);
if (append_path_sep) path_len++;
const size_t max_filename_len = ((use_prefix && !strncmp(prefix, DEVOPTAB_SDMC_DEVICE, strlen(DEVOPTAB_SDMC_DEVICE))) ? SDMC_MAX_FILENAME_LENGTH : FS_MAX_FILENAME_LENGTH);
char *path = NULL, *ptr1 = NULL, *ptr2 = NULL;
bool filename_only = false, success = false;
/* Allocate memory for the output path. */
if (!(path = calloc(path_len + 1, sizeof(char))))
{
LOG_MSG_ERROR("Failed to allocate 0x%lX bytes for output path!", path_len);
goto end;
}
/* Generate output path. */
if (use_prefix) strcat(path, prefix);
if (append_path_sep) strcat(path, "/");
strcat(path, filename);
if (use_extension) strcat(path, extension);
/* Retrieve pointer to the first path separator. */
ptr1 = strchr(path, '/');
if (!ptr1)
{
filename_only = true;
ptr1 = path;
}
/* Make sure each path element doesn't exceed our max filename length. */
while(ptr1)
{
if (!filename_only)
{
/* End loop if we find a NULL terminator. */
if (!*ptr1++) break;
/* Get pointer to next path separator. */
ptr2 = strchr(ptr1, '/');
}
/* Get current path element size. */
size_t element_size = (ptr2 ? (size_t)(ptr2 - ptr1) : (path_len - (size_t)(ptr1 - path)));
/* Get UTF-8 string limit. */
/* Use our max filename length as the byte count limit. */
size_t last_cp_pos = utilsGetUtf8StringLimit(ptr1, element_size, max_filename_len);
if (last_cp_pos < element_size)
{
if (ptr2)
{
/* Truncate current element by moving the rest of the path to the current position. */
memmove(ptr1 + last_cp_pos, ptr2, path_len - (size_t)(ptr2 - path));
/* Update pointer. */
ptr2 -= (element_size - last_cp_pos);
} else
if (use_extension)
{
/* Truncate last element. Make sure to preserve the provided file extension. */
if (extension_len >= last_cp_pos)
{
LOG_MSG_ERROR("File extension length is >= truncated filename length! (0x%lX >= 0x%lX).", extension_len, last_cp_pos);
goto end;
}
memmove(ptr1 + last_cp_pos - extension_len, ptr1 + element_size - extension_len, extension_len);
}
path_len -= (element_size - last_cp_pos);
path[path_len] = '\0';
}
ptr1 = ptr2;
}
/* Check if the full length for the generated path is >= FS_MAX_PATH. */
if (path_len >= FS_MAX_PATH)
{
LOG_MSG_ERROR("Generated path length is >= FS_MAX_PATH! (0x%lX).", path_len);
goto end;
}
/* Update flag. */
success = true;
end:
if (!success && path)
{
free(path);
path = NULL;
}
return path;
}
void utilsPrintConsoleError(const char *msg)
{
PadState pad = {0};
/* Don't consider stick movement as button inputs. */
u64 flag = ~(HidNpadButton_StickLLeft | HidNpadButton_StickLRight | HidNpadButton_StickLUp | HidNpadButton_StickLDown | HidNpadButton_StickRLeft | HidNpadButton_StickRRight | \
HidNpadButton_StickRUp | HidNpadButton_StickRDown);
/* Configure input. */
/* Up to 8 different, full controller inputs. */
/* Individual Joy-Cons not supported. */
padConfigureInput(8, HidNpadStyleSet_NpadFullCtrl);
padInitializeWithMask(&pad, 0x1000000FFUL);
/* Initialize console output. */
consoleInit(NULL);
/* Print message. */
if (msg && *msg)
{
printf("%s", msg);
} else {
printf("An error occurred.");
}
#if LOG_LEVEL < LOG_LEVEL_NONE
printf("\n\nFor more information, please check the logfile. Press any button to exit.");
#else
printf("\n\nPress any button to exit.");
#endif
consoleUpdate(NULL);
/* Wait until the user presses a button. */
while(appletMainLoop())
{
padUpdate(&pad);
if (padGetButtonsDown(&pad) & flag) break;
utilsAppletLoopDelay();
}
/* Deinitialize console output. */
consoleExit(NULL);
}
bool utilsGetApplicationUpdatedState(void)
{
bool ret = false;
SCOPED_LOCK(&g_resourcesMutex) ret = g_appUpdated;
return ret;
}
void utilsSetApplicationUpdatedState(void)
{
SCOPED_LOCK(&g_resourcesMutex) g_appUpdated = true;
}
bool utilsParseGitHubReleaseJsonData(const char *json_buf, size_t json_buf_size, UtilsGitHubReleaseJsonData *out)
{
if (!json_buf || !json_buf_size || !out)
{
LOG_MSG_ERROR("Invalid parameters!");
return false;
}
bool ret = false;
const char *published_at = NULL;
struct json_object *assets = NULL;
/* Free output buffer beforehand. */
utilsFreeGitHubReleaseJsonData(out);
/* Parse JSON object. */
out->obj = jsonParseFromString(json_buf, json_buf_size);
if (!out->obj)
{
LOG_MSG_ERROR("Failed to parse JSON object!");
return false;
}
/* Get required JSON elements. */
out->version = jsonGetString(out->obj, "tag_name");
out->commit_hash = jsonGetString(out->obj, "target_commitish");
published_at = jsonGetString(out->obj, "published_at");
out->changelog = jsonGetString(out->obj, "body");
assets = jsonGetArray(out->obj, "assets");
if (!out->version || !out->commit_hash || !published_at || !out->changelog || !assets)
{
LOG_MSG_ERROR("Failed to retrieve required elements from the provided JSON!");
goto end;
}
/* Parse release date. */
if (!strptime(published_at, "%Y-%m-%dT%H:%M:%SZ", &(out->date)))
{
LOG_MSG_ERROR("Failed to parse release date \"%s\"!", published_at);
goto end;
}
/* Loop through the assets array until we find the NRO. */
size_t assets_len = json_object_array_length(assets);
for(size_t i = 0; i < assets_len; i++)
{
struct json_object *cur_asset = NULL;
const char *asset_name = NULL;
/* Get current asset object. */
cur_asset = json_object_array_get_idx(assets, i);
if (!cur_asset) continue;
/* Get current asset name. */
asset_name = jsonGetString(cur_asset, "name");
if (!asset_name || strcmp(asset_name, NRO_NAME) != 0) continue;
/* Jackpot. Get the download URL. */
out->download_url = jsonGetString(cur_asset, "browser_download_url");
break;
}
if (!out->download_url)
{
LOG_MSG_ERROR("Failed to retrieve required elements from the provided JSON!");
goto end;
}
/* Update return value. */
ret = true;
end:
if (!ret) utilsFreeGitHubReleaseJsonData(out);
return ret;
}
bool utilsIsApplicationUpdatable(const char *version, const char *commit_hash)
{
if (!version || !*version || *version != 'v' || !commit_hash || !*commit_hash)
{
LOG_MSG_ERROR("Invalid parameters!");
return false;
}
bool ret = false;
UtilsApplicationVersion cur_version = { VERSION_MAJOR, VERSION_MINOR, VERSION_MICRO }, new_version = {0};
/* Parse version string. */
sscanf(version, "v%u.%u.%u", &(new_version.major), &(new_version.minor), &(new_version.micro));
/* Compare versions. */
if (cur_version.major == new_version.major)
{
if (cur_version.minor == new_version.minor)
{
if (cur_version.micro == new_version.micro)
{
/* Versions are equal. Let's compare the commit hashes and return true if they're different. */
ret = (strncasecmp(commit_hash, GIT_COMMIT, 7) != 0);
} else
if (cur_version.micro < new_version.micro)
{
ret = true;
}
} else
if (cur_version.minor < new_version.minor)
{
ret = true;
}
} else
if (cur_version.major < new_version.major)
{
ret = true;
}
return ret;
}
static void _utilsGetLaunchPath(void)
{
if (__system_argc <= 0 || !__system_argv) return;
for(int i = 0; i < __system_argc; i++)
{
if (__system_argv[i] && !strncmp(__system_argv[i], DEVOPTAB_SDMC_DEVICE "/", strlen(DEVOPTAB_SDMC_DEVICE)))
{
g_appLaunchPath = __system_argv[i];
break;
}
}
}
/* SMC config item available in Atmosphère and Atmosphère-based CFWs. */
static bool utilsGetExosphereApiVersion(void)
{
Result rc = splGetConfig(SplConfigItem_ExosphereApiVersion, (u64*)&g_exosphereApiVersion);
bool ret = R_SUCCEEDED(rc);
if (!ret) LOG_MSG_ERROR("splGetConfig failed! (0x%X).", rc);
return ret;
}
/* SMC config item available in Atmosphère and Atmosphère-based CFWs. */
static bool utilsGetExosphereEmummcType(void)
{
u64 is_emummc = 0;
Result rc = splGetConfig(SplConfigItem_ExosphereEmummcType, &is_emummc);
bool ret = R_SUCCEEDED(rc);
g_exosphereIsEmummc = (ret && is_emummc);
if (!ret) LOG_MSG_ERROR("splGetConfig failed! (0x%X).", rc);
return ret;
}
static void _utilsGetCustomFirmwareType(void)
{
bool tx_srv = servicesCheckRunningServiceByName("tx");
bool rnx_srv = servicesCheckRunningServiceByName("rnx");
g_customFirmwareType = (rnx_srv ? UtilsCustomFirmwareType_ReiNX : (tx_srv ? UtilsCustomFirmwareType_SXOS : UtilsCustomFirmwareType_Atmosphere));
}
static bool _utilsGetProductModel(void)
{
Result rc = 0;
bool ret = false;
SetSysProductModel model = SetSysProductModel_Invalid;
rc = setsysGetProductModel(&model);
if (R_SUCCEEDED(rc) && model != SetSysProductModel_Invalid)
{
g_productModel = model;
ret = true;
} else {
LOG_MSG_ERROR("setsysGetProductModel failed! (0x%X) (%d).", rc, model);
}
return ret;
}
static bool utilsGetDevelopmentUnitFlag(void)
{
Result rc = 0;
bool tmp = false;
rc = splIsDevelopment(&tmp);
if (R_SUCCEEDED(rc))
{
g_isDevUnit = tmp;
} else {
LOG_MSG_ERROR("splIsDevelopment failed! (0x%X).", rc);
}
return R_SUCCEEDED(rc);
}
static bool utilsGetTerraUnitFlag(void)
{
/* Return right away if we're running under a HOS version that's too low. */
if (hosversionBefore(8, 0, 0)) return true;
Result rc = 0;
bool tmp = false;
rc = setsysGetT(&tmp);
if (R_SUCCEEDED(rc))
{
g_isTerraUnit = tmp;
} else {
LOG_MSG_ERROR("setsysGetT failed! (0x%X).", rc);
}
return R_SUCCEEDED(rc);
}
static bool utilsMountEmmcBisSystemPartitionStorage(void)
{
Result rc = 0;
FRESULT fr = FR_OK;
rc = fsOpenBisStorage(&g_emmcBisSystemPartitionStorage, FsBisPartitionId_System);
if (R_FAILED(rc))
{
LOG_MSG_ERROR("Failed to open eMMC BIS System partition storage! (0x%X).", rc);
return false;
}
g_emmcBisSystemPartitionFatFsObj = calloc(1, sizeof(FATFS));
if (!g_emmcBisSystemPartitionFatFsObj)
{
LOG_MSG_ERROR("Unable to allocate memory for FatFs element!");
return false;
}
fr = f_mount(g_emmcBisSystemPartitionFatFsObj, BIS_SYSTEM_PARTITION_MOUNT_NAME, 1);
if (fr != FR_OK)
{
LOG_MSG_ERROR("Failed to mount eMMC BIS System partition! (%u).", fr);
return false;
}
return true;
}
static void utilsUnmountEmmcBisSystemPartitionStorage(void)
{
if (g_emmcBisSystemPartitionFatFsObj)
{
f_unmount(BIS_SYSTEM_PARTITION_MOUNT_NAME);
free(g_emmcBisSystemPartitionFatFsObj);
g_emmcBisSystemPartitionFatFsObj = NULL;
}
if (serviceIsActive(&(g_emmcBisSystemPartitionStorage.s)))
{
fsStorageClose(&g_emmcBisSystemPartitionStorage);
memset(&g_emmcBisSystemPartitionStorage, 0, sizeof(FsStorage));
}
}
static void utilsOverclockSystem(bool overclock)
{
u32 cpu_rate = ((overclock ? CPU_CLKRT_OVERCLOCKED : CPU_CLKRT_NORMAL) * 1000000);
u32 mem_rate = ((overclock ? MEM_CLKRT_OVERCLOCKED : MEM_CLKRT_NORMAL) * 1000000);
servicesChangeHardwareClockRates(cpu_rate, mem_rate);
}
static void utilsOverclockSystemAppletHook(AppletHookType hook, void *param)
{
NX_IGNORE_ARG(param);
/* Don't proceed if we're not dealing with a desired hook type. */
if (hook != AppletHookType_OnOperationMode && hook != AppletHookType_OnPerformanceMode) return;
/* Overclock the system based on the overclock setting and the current long running state value. */
SCOPED_LOCK(&g_resourcesMutex) utilsOverclockSystem(configGetBoolean("overclock") & g_longRunningProcess);
}
static void utilsChangeHomeButtonBlockStatus(bool block)
{
/* Only change HOME button blocking status if we're running as a regular application or a system application. */
if (utilsIsAppletMode()) return;
if (block)
{
appletBeginBlockingHomeButtonShortAndLongPressed(0);
} else {
appletEndBlockingHomeButtonShortAndLongPressed();
}
}
static size_t utilsGetUtf8StringLimit(const char *str, size_t str_size, size_t byte_limit)
{
if (!str || !*str || !str_size || !byte_limit) return 0;
if (byte_limit > str_size) return str_size;
u32 code = 0;
ssize_t units = 0;
size_t cur_pos = 0, last_cp_pos = 0;
const u8 *str_u8 = (const u8*)str;
while(cur_pos < str_size && cur_pos < byte_limit)
{
units = decode_utf8(&code, str_u8 + cur_pos);
size_t new_pos = (cur_pos + (size_t)units);
if (units < 0 || !code || new_pos > str_size) break;
cur_pos = new_pos;
if (cur_pos < byte_limit) last_cp_pos = cur_pos;
}
return last_cp_pos;
}
static char utilsConvertHexDigitToBinary(char c)
{
if ('a' <= c && c <= 'f') return (c - 'a' + 0xA);
if ('A' <= c && c <= 'F') return (c - 'A' + 0xA);
if ('0' <= c && c <= '9') return (c - '0');
return 'z';
}