nxdumptool/source/core/nxdt_utils.c

1439 lines
40 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 "nxdt_utils.h"
#include "keys.h"
#include "gamecard.h"
#include "services.h"
#include "nca.h"
#include "usb.h"
#include "title.h"
#include "bfttf.h"
#include "nxdt_bfsar.h"
#include "nxdt_devoptab.h"
#include "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_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 UtilsExosphereApiVersion g_exosphereApiVersion = {0};
/* Function prototypes. */
static void _utilsGetLaunchPath(void);
static bool utilsGetExosphereApiVersion(void);
static void _utilsGetCustomFirmwareType(void);
static bool _utilsGetProductModel(void);
static bool _utilsIsDevelopmentUnit(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. */
u32 hos_version = hosversionGet();
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));
LOG_MSG_INFO("Horizon OS version: %u.%u.%u.", HOSVER_MAJOR(hos_version), HOSVER_MINOR(hos_version), HOSVER_MICRO(hos_version));
#endif
/* Retrieve Exosphère API version. */
if (!utilsGetExosphereApiVersion())
{
LOG_MSG_ERROR("Failed to retrieve Exosphère API version!");
break;
}
/* Retrieve custom firmware type. */
_utilsGetCustomFirmwareType();
if (g_customFirmwareType != UtilsCustomFirmwareType_Unknown) LOG_MSG_INFO("Detected %s CFW.", (g_customFirmwareType == UtilsCustomFirmwareType_Atmosphere ? "Atmosphère" : \
(g_customFirmwareType == UtilsCustomFirmwareType_SXOS ? "SX OS" : "ReiNX")));
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);
/* Get product model. */
if (!_utilsGetProductModel()) break;
/* Get development unit flag. */
if (!_utilsIsDevelopmentUnit()) break;
/* Get applet type. */
g_programAppletType = appletGetAppletType();
LOG_MSG_INFO("Running under %s %s unit in %s mode.", g_isDevUnit ? "development" : "retail", utilsIsMarikoUnit() ? "Mariko" : "Erista", utilsIsAppletMode() ? "applet" : "title override");
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));
}
u8 utilsGetCustomFirmwareType(void)
{
return g_customFirmwareType;
}
bool utilsIsMarikoUnit(void)
{
return (g_productModel > SetSysProductModel_Copper);
}
bool utilsIsDevelopmentUnit(void)
{
return g_isDevUnit;
}
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';
}
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;
}
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 _utilsIsDevelopmentUnit(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 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';
}