diff --git a/Makefile b/Makefile index 8c88e2a..05efb1d 100644 --- a/Makefile +++ b/Makefile @@ -81,7 +81,7 @@ CFLAGS := -g -gdwarf-4 -Wall -Werror -O2 -ffunction-sections $(ARCH) $(DEFINES) CFLAGS += -DVERSION_MAJOR=${VERSION_MAJOR} -DVERSION_MINOR=${VERSION_MINOR} -DVERSION_MICRO=${VERSION_MICRO} CFLAGS += -DAPP_TITLE=\"${APP_TITLE}\" -DAPP_AUTHOR=\"${APP_AUTHOR}\" -DAPP_VERSION=\"${APP_VERSION}\" CFLAGS += -DGIT_BRANCH=\"${GIT_BRANCH}\" -DGIT_COMMIT=\"${GIT_COMMIT}\" -DGIT_REV=\"${GIT_REV}\" -CFLAGS += -DBOREALIS_RESOURCES="\"${BOREALIS_RESOURCES}\"" +CFLAGS += -DBOREALIS_RESOURCES="\"${BOREALIS_RESOURCES}\"" -D_GNU_SOURCE CXXFLAGS := $(CFLAGS) -std=c++20 -Wno-volatile -Wno-unused-parameter diff --git a/include/core/config.h b/include/core/config.h index 747e342..80da4d5 100644 --- a/include/core/config.h +++ b/include/core/config.h @@ -24,7 +24,7 @@ #ifndef __CONFIG_H__ #define __CONFIG_H__ -#include +#include "nxdt_json.h" #ifdef __cplusplus extern "C" { diff --git a/include/core/nxdt_json.h b/include/core/nxdt_json.h new file mode 100644 index 0000000..79df47c --- /dev/null +++ b/include/core/nxdt_json.h @@ -0,0 +1,97 @@ +/* + * nxdt_json.h + * + * Copyright (c) 2020-2021, DarkMatterCore . + * + * 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 . + */ + +#pragma once + +#ifndef __NXDT_JSON_H__ +#define __NXDT_JSON_H__ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/// Parses a JSON object using the provided string. +/// If 'size' is zero, strlen() is used to retrieve the input string length. +/// json_object_put() must be used to free the returned JSON object. +/// Returns NULL if an error occurs. +struct json_object *jsonParseFromString(const char *str, size_t size); + +/// Retrieves a JSON object from another object using a path. +/// Path elements must be separated using forward slashes. +/// If 'out_last_element' is provided, the parent JSON object from the last path element will be returned instead. +/// Furthermore, a string duplication of the last path element will be stored in 'out_last_element', which must be freed by the user. +/// Returns NULL if an error occurs. +struct json_object *jsonGetObjectByPath(const struct json_object *obj, const char *path, char **out_last_element); + +/// Logs the last JSON error, if available. +void jsonLogLastError(void); + +/// Getters and setters for various data types. +/// Path elements must be separated using forward slashes. + +bool jsonGetBoolean(const struct json_object *obj, const char *path); +bool jsonSetBoolean(const struct json_object *obj, const char *path, bool value); + +int jsonGetInteger(const struct json_object *obj, const char *path); +bool jsonSetInteger(const struct json_object *obj, const char *path, int value); + +const char *jsonGetString(const struct json_object *obj, const char *path); +bool jsonSetString(const struct json_object *obj, const char *path, const char *value); + +struct json_object *jsonGetArray(const struct json_object *obj, const char *path); +bool jsonSetArray(const struct json_object *obj, const char *path, struct json_object *value); + +/// Helper functions to validate specific JSON object types. + +NX_INLINE bool jsonValidateBoolean(const struct json_object *obj) +{ + return (obj != NULL && json_object_is_type(obj, json_type_boolean)); +} + +NX_INLINE bool jsonValidateInteger(const struct json_object *obj, int lower_boundary, int upper_boundary) +{ + if (!obj || !json_object_is_type(obj, json_type_int) || lower_boundary > upper_boundary) return false; + int val = json_object_get_int(obj); + return (val >= lower_boundary && val <= upper_boundary); +} + +NX_INLINE bool jsonValidateString(const struct json_object *obj) +{ + return (obj != NULL && json_object_is_type(obj, json_type_string) && json_object_get_string_len(obj) > 0); +} + +NX_INLINE bool jsonValidateArray(const struct json_object *obj) +{ + return (obj != NULL && json_object_is_type(obj, json_type_array) && json_object_array_length(obj) > 0); +} + +NX_INLINE bool jsonValidateObject(const struct json_object *obj) +{ + return (obj != NULL && json_object_is_type(obj, json_type_object) && json_object_object_length(obj) > 0); +} + +#ifdef __cplusplus +} +#endif + +#endif /* __NXDT_JSON_H__ */ diff --git a/include/core/nxdt_utils.h b/include/core/nxdt_utils.h index 3856e16..5d75e77 100644 --- a/include/core/nxdt_utils.h +++ b/include/core/nxdt_utils.h @@ -52,6 +52,17 @@ typedef enum { UtilsCustomFirmwareType_ReiNX = 3 } UtilsCustomFirmwareType; +/// Used to handle parsed data from a GitHub release JSON. +/// All strings are dynamically allocated. +typedef struct { + struct json_object *obj; ///< JSON object. Must be freed using json_object_put(). + const char *version; ///< Pointer to the version string, referenced by obj. + const char *commit_hash; ///< Pointer to the commit hash string, referenced by obj. + struct tm date; ///< Release date. + const char *changelog; ///< Pointer to the changelog string, referenced by obj. + const char *download_url; ///< Pointer to the download URL string, referenced by obj. +} UtilsGitHubReleaseJsonData; + /// Resource initialization. /// Called at program startup. bool utilsInitializeResources(const int program_argc, const char **program_argv); @@ -140,6 +151,25 @@ void utilsCreateDirectoryTree(const char *path, bool create_last_element); /// Furthermore, if the full length for the generated path is >= FS_MAX_PATH, NULL will be returned. char *utilsGeneratePath(const char *prefix, const char *filename, const char *extension); +/// Parses the provided GitHub release JSON data buffer. +/// The data from the output buffer must be freed using utilsFreeGitHubReleaseJsonData(). +bool utilsParseGitHubReleaseJsonData(const char *json_buf, size_t json_buf_size, UtilsGitHubReleaseJsonData *out); + +/// Frees previously allocated data from a UtilsGitHubReleaseJsonData element. +NX_INLINE void utilsFreeGitHubReleaseJsonData(UtilsGitHubReleaseJsonData *data) +{ + if (!data) return; + if (data->obj) json_object_put(data->obj); + memset(data, 0, sizeof(UtilsGitHubReleaseJsonData)); +} + +/// Returns the current application updated state. +bool utilsGetApplicationUpdatedState(void); + +/// Sets the application updated state to true, which makes utilsCloseResources() replace the application NRO. +/// Use carefully. +void utilsSetApplicationUpdatedState(void); + /// Simple wrapper to sleep the current thread for a specific number of full seconds. NX_INLINE void utilsSleep(u64 seconds) { diff --git a/include/defines.h b/include/defines.h index 68e61f8..505a421 100644 --- a/include/defines.h +++ b/include/defines.h @@ -72,6 +72,8 @@ #define UTF8_BOM "\xEF\xBB\xBF" #define CRLF "\r\n" +#define DEVOPTAB_SDMC_DEVICE "sdmc:" + #define HBMENU_BASE_PATH "/switch/" #define APP_BASE_PATH HBMENU_BASE_PATH APP_TITLE "/" @@ -83,13 +85,14 @@ #define NCA_PATH APP_BASE_PATH "NCA/" #define NCA_FS_PATH APP_BASE_PATH "NCA FS/" -#define CONFIG_PATH "sdmc:" APP_BASE_PATH "config.json" +#define CONFIG_PATH DEVOPTAB_SDMC_DEVICE APP_BASE_PATH "config.json" #define DEFAULT_CONFIG_PATH "romfs:/default_config.json" #define NRO_NAME APP_TITLE ".nro" -#define NRO_PATH APP_BASE_PATH NRO_NAME +#define NRO_PATH DEVOPTAB_SDMC_DEVICE APP_BASE_PATH NRO_NAME +#define NRO_TMP_PATH NRO_PATH ".tmp" -#define KEYS_FILE_PATH "sdmc:" HBMENU_BASE_PATH "prod.keys" /* Location used by Lockpick_RCM. */ +#define KEYS_FILE_PATH DEVOPTAB_SDMC_DEVICE HBMENU_BASE_PATH "prod.keys" /* Location used by Lockpick_RCM. */ #define LOG_FILE_NAME APP_TITLE ".log" #define LOG_BUF_SIZE 0x400000 /* 4 MiB. */ @@ -103,9 +106,15 @@ #define HTTP_CONNECT_TIMEOUT 10L /* 10 seconds. */ #define HTTP_BUFFER_SIZE 131072L /* 128 KiB. */ -#define GITHUB_REPOSITORY_URL "https://github.com/DarkMatterCore/nxdumptool" +#define GITHUB_URL "https://github.com" +#define GITHUB_API_URL "https://api.github.com" +#define GITHUB_REPOSITORY APP_AUTHOR "/" APP_TITLE + +#define GITHUB_REPOSITORY_URL GITHUB_URL "/" GITHUB_REPOSITORY #define GITHUB_NEW_ISSUE_URL GITHUB_REPOSITORY_URL "/issues/new/choose" +#define GITHUB_API_RELEASE_URL GITHUB_API_URL "/repos/" GITHUB_REPOSITORY "/releases/latest" + #define NSWDB_XML_URL "http://nswdb.com/xml.php" #define NSWDB_XML_PATH APP_BASE_PATH "NSWreleases.xml" diff --git a/include/download_task.hpp b/include/download_task.hpp index 6a1ba1f..a8f173d 100644 --- a/include/download_task.hpp +++ b/include/download_task.hpp @@ -162,7 +162,7 @@ namespace nxdt::tasks /* Fill struct. */ progress.size = static_cast(dltotal); progress.current = static_cast(dlnow); - progress.percentage = static_cast((progress.current * 100) / progress.size); + progress.percentage = (progress.size ? static_cast((progress.current * 100) / progress.size) : 0); /* Push progress onto the class. */ task->publishProgress(progress); @@ -243,7 +243,7 @@ namespace nxdt::tasks /* Calculate remaining data size and ETA if we know the download size. */ double remaining = static_cast(progress.size - progress.current); double eta = (remaining / speed); - new_progress.eta = fmt::format("{:02}H{:02}M{:02}S", std::fmod(eta, 86400.0) / 3600.0, std::fmod(eta, 3600.0) / 60.0, std::fmod(eta, 60.0)); + new_progress.eta = fmt::format("{:02.0F}H{:02.0F}M{:02.0F}S", std::fmod(eta, 86400.0) / 3600.0, std::fmod(eta, 3600.0) / 60.0, std::fmod(eta, 60.0)); } else { /* No download size means no ETA calculation, sadly. */ new_progress.eta = ""; diff --git a/include/options_tab.hpp b/include/options_tab.hpp index c407773..0ed0bec 100644 --- a/include/options_tab.hpp +++ b/include/options_tab.hpp @@ -30,14 +30,13 @@ namespace nxdt::views { - /* Used as the content view for OptionsTabUpdateFileDialog. */ - class OptionsTabUpdateFileDialogContent: public brls::View + /* Used in OptionsTabUpdateFileDialog and OptionsTabUpdateApplicationFrame to display the update progress. */ + class OptionsTabUpdateProgress: public brls::View { private: brls::ProgressDisplay *progress_display = nullptr; - brls::Label *size_label = nullptr, *speed_eta_label = nullptr; + brls::Label *size_lbl = nullptr, *speed_eta_lbl = nullptr; - std::string GetFormattedSizeString(size_t size); std::string GetFormattedSizeString(double size); protected: @@ -45,8 +44,8 @@ namespace nxdt::views void layout(NVGcontext* vg, brls::Style* style, brls::FontStash* stash) override; public: - OptionsTabUpdateFileDialogContent(void); - ~OptionsTabUpdateFileDialogContent(void); + OptionsTabUpdateProgress(void); + ~OptionsTabUpdateProgress(void); void SetProgress(const nxdt::tasks::DownloadTaskProgress& progress); @@ -63,8 +62,35 @@ namespace nxdt::views public: OptionsTabUpdateFileDialog(std::string path, std::string url, bool force_https, std::string success_str); + }; + + /* Update application frame. */ + class OptionsTabUpdateApplicationFrame: public brls::StagedAppletFrame + { + private: + nxdt::tasks::DownloadDataTask json_task; + char *json_buf = NULL; + size_t json_buf_size = 0; + UtilsGitHubReleaseJsonData json_data = {0}; + brls::Label *wait_lbl = nullptr; /// First stage. + brls::List *changelog_list = nullptr; /// Second stage. + OptionsTabUpdateProgress *update_progress = nullptr; /// Third stage. + + nxdt::tasks::DownloadFileTask nro_task; + + brls::GenericEvent::Subscription focus_event_sub; + + void DisplayChangelog(void); + void DisplayUpdateProgress(void); + + protected: + void layout(NVGcontext* vg, brls::Style* style, brls::FontStash* stash) override; bool onCancel(void) override; + + public: + OptionsTabUpdateApplicationFrame(void); + ~OptionsTabUpdateApplicationFrame(void); }; class OptionsTab: public brls::List diff --git a/include/root_view.hpp b/include/root_view.hpp index 49857ca..8238833 100644 --- a/include/root_view.hpp +++ b/include/root_view.hpp @@ -56,6 +56,8 @@ namespace nxdt::views public: RootView(void); ~RootView(void); + + static std::string GetFormattedDateString(const struct tm& timeinfo); }; } diff --git a/include/tasks.hpp b/include/tasks.hpp index 4cff63b..38b5e06 100644 --- a/include/tasks.hpp +++ b/include/tasks.hpp @@ -37,7 +37,7 @@ namespace nxdt::tasks { /* Used to hold status info data. */ typedef struct { - struct tm *timeinfo; + struct tm timeinfo; u32 charge_percentage; PsmChargerType charger_type; NifmInternetConnectionType connection_type; diff --git a/libs/borealis b/libs/borealis index cf58d01..fb8891e 160000 --- a/libs/borealis +++ b/libs/borealis @@ -1 +1 @@ -Subproject commit cf58d0115c358faa39c71867c6e49f0aa5756e59 +Subproject commit fb8891e4d75474d1a1ecbde1c930b0a308b1c3e0 diff --git a/libs/libusbhsfs b/libs/libusbhsfs index 782aa51..85a802a 160000 --- a/libs/libusbhsfs +++ b/libs/libusbhsfs @@ -1 +1 @@ -Subproject commit 782aa51e0aa149427664cc3a9c2e520937576fcc +Subproject commit 85a802a38a0abc655284b9d77a0e452f0ee9763b diff --git a/romfs/i18n/en-US/generic.json b/romfs/i18n/en-US/generic.json index df0d422..965b8e5 100644 --- a/romfs/i18n/en-US/generic.json +++ b/romfs/i18n/en-US/generic.json @@ -1,5 +1,13 @@ { + "__comment__": "Comments about how specific fields work use keys that follow the '__{field}_comment__' format. These don't have to be replicated in your translation files.", + "unknown": "Unknown", - "applet_mode_warning": "\uE8B2 Warning: the application is running under Applet Mode! \uE8B2\nThis mode severely limits the amount of usable RAM. If you consistently reproduce any crashes, please consider running the application via title override (hold R while launching a game)." + "applet_mode_warning": "\uE8B2 Warning: the application is running under Applet Mode! \uE8B2\nThis mode severely limits the amount of usable RAM. If you consistently reproduce any crashes, please consider running the application via title override (hold R while launching a game).", + + "time_format": "12", + "__time_format_comment__": "Use 12 for a 12-hour clock, or 24 for a 24-hour clock", + + "date": "{1:02d}/{2:02d}/{0} {3:02d}:{4:02d}:{5:02d} {6}", + "__date_comment__": "{0} = Year, {1} = Month, {2} = Day, {3} = Hour, {4} = Minute, {5} = Second, {6} = AM/PM (if time_format is set to 12)" } diff --git a/romfs/i18n/en-US/options_tab.json b/romfs/i18n/en-US/options_tab.json index 0d3d347..8482fb6 100644 --- a/romfs/i18n/en-US/options_tab.json +++ b/romfs/i18n/en-US/options_tab.json @@ -22,7 +22,13 @@ "update_app": { "label": "Update application", - "description": "Checks if an update is available in nxdumptool's GitHub repository. Requires Internet connectivity." + "description": "Checks if an update is available in nxdumptool's GitHub repository. Requires Internet connectivity.", + "frame": { + "please_wait": "Please wait…", + "release_details": "Commit hash: {0:.7}\nRelease date: {1} UTC+0", + "changelog_header": "Changelog", + "update_action": "Update" + } }, "update_dialog": { @@ -32,9 +38,11 @@ "notifications": { "no_internet_connection": "Internet connection unavailable. Unable to update.", - "update_failed": "Update failed! For more information, please check the logfile.", + "update_failed": "Update failed! Check the logfile for more info.", "nswdb_xml_updated": "NSWDB XML successfully updated!", "is_nso": "The application is running as an NSO. Unable to update.", - "already_updated": "The application has already been updated. Please reload." + "already_updated": "The application has already been updated. Please reload.", + "github_json_failed": "Failed to download or parse GitHub release JSON!", + "app_updated": "Application successfully updated! Please reload for the changes to take effect." } } diff --git a/romfs/i18n/en-US/root_view.json b/romfs/i18n/en-US/root_view.json index 8960912..ed32133 100644 --- a/romfs/i18n/en-US/root_view.json +++ b/romfs/i18n/en-US/root_view.json @@ -1,14 +1,6 @@ { - "__comment__": "Comments about how specific fields work use keys that follow the '__{field}_comment__' format. These don't have to be replicated in your translation files.", - "applet_mode": "\uE8B2 Applet Mode \uE8B2", - "time_format": "12", - "__time_format_comment__": "Use 12 for a 12-hour clock, or 24 for a 24-hour clock", - - "date": "{1:02d}/{2:02d}/{0} {3:02d}:{4:02d}:{5:02d} {6}", - "__date_comment__": "{0} = Year, {1} = Month, {2} = Day, {3} = Hour, {4} = Minute, {5} = Second, {6} = AM/PM (if time_format is set to 12)", - "not_connected": "Not connected", "tabs": { diff --git a/source/core/cert.c b/source/core/cert.c index 0da9041..d066468 100644 --- a/source/core/cert.c +++ b/source/core/cert.c @@ -322,7 +322,7 @@ static bool _certRetrieveCertificateChainBySignatureIssuer(CertificateChain *dst snprintf(issuer_copy, sizeof(issuer_copy), "%s", issuer + 5); pch = strtok_r(issuer_copy, "-", &state); - while(pch != NULL) + while(pch) { if (!_certRetrieveCertificateByName(&(dst->certs[i]), pch)) { @@ -352,7 +352,7 @@ static u32 certGetCertificateCountInSignatureIssuer(const char *issuer) snprintf(issuer_copy, sizeof(issuer_copy), "%s", issuer + 5); pch = strtok_r(issuer_copy, "-", &state); - while(pch != NULL) + while(pch) { count++; pch = strtok_r(NULL, "-", &state); diff --git a/source/core/config.c b/source/core/config.c index 9f630ca..12e9efd 100644 --- a/source/core/config.c +++ b/source/core/config.c @@ -23,43 +23,35 @@ #include "config.h" #include "title.h" -#define JSON_VALIDATE_FIELD(type, name, ...) \ +#define CONFIG_VALIDATE_FIELD(type, name, ...) \ if (!strcmp(key, #name)) { \ - if (name##_found || !configValidateJson##type(val, ##__VA_ARGS__)) goto end; \ + if (name##_found || !jsonValidate##type(val, ##__VA_ARGS__)) goto end; \ name##_found = true; \ continue; \ } -#define JSON_VALIDATE_OBJECT(type, name) \ +#define CONFIG_VALIDATE_OBJECT(type, name) \ if (!strcmp(key, #name)) { \ if (name##_found || !configValidateJson##type##Object(val)) goto end; \ name##_found = true; \ continue; \ } -#define JSON_GETTER(functype, vartype, jsontype, ...) \ +#define CONFIG_GETTER(functype, vartype, ...) \ vartype configGet##functype(const char *path) { \ vartype ret = (vartype)0; \ SCOPED_LOCK(&g_configMutex) { \ if (!g_configInterfaceInit) break; \ - struct json_object *obj = configGetJsonObjectByPath(g_configJson, path); \ - if (!obj || !configValidateJson##functype(obj, ##__VA_ARGS__)) break; \ - ret = (vartype)json_object_get_##jsontype(obj); \ + ret = jsonGet##functype(g_configJson, path); \ } \ return ret; \ } -#define JSON_SETTER(functype, vartype, jsontype, ...) \ +#define CONFIG_SETTER(functype, vartype, ...) \ void configSet##functype(const char *path, vartype value) { \ SCOPED_LOCK(&g_configMutex) { \ if (!g_configInterfaceInit) break; \ - struct json_object *obj = configGetJsonObjectByPath(g_configJson, path); \ - if (!obj || !configValidateJson##functype(obj, ##__VA_ARGS__)) break; \ - if (json_object_set_##jsontype(obj, value)) { \ - configWriteConfigJson(); \ - } else { \ - LOG_MSG("Failed to update \"%s\"!", path); \ - } \ + if (jsonSet##functype(g_configJson, path, value)) configWriteConfigJson(); \ } \ } @@ -76,20 +68,12 @@ static bool configParseConfigJson(void); static void configWriteConfigJson(void); static void configFreeConfigJson(void); -static struct json_object *configGetJsonObjectByPath(const struct json_object *obj, const char *path); - static bool configValidateJsonRootObject(const struct json_object *obj); static bool configValidateJsonGameCardObject(const struct json_object *obj); static bool configValidateJsonNspObject(const struct json_object *obj); static bool configValidateJsonTicketObject(const struct json_object *obj); static bool configValidateJsonNcaFsObject(const struct json_object *obj); -NX_INLINE bool configValidateJsonBoolean(const struct json_object *obj); -NX_INLINE bool configValidateJsonInteger(const struct json_object *obj, int lower_boundary, int upper_boundary); -NX_INLINE bool configValidateJsonObject(const struct json_object *obj); - -static void configLogJsonError(void); - bool configInitialize(void) { bool ret = false; @@ -126,11 +110,11 @@ void configExit(void) } } -JSON_GETTER(Boolean, bool, boolean); -JSON_SETTER(Boolean, bool, boolean); +CONFIG_GETTER(Boolean, bool); +CONFIG_SETTER(Boolean, bool); -JSON_GETTER(Integer, int, int, INT32_MIN, INT32_MAX); -JSON_SETTER(Integer, int, int, INT32_MIN, INT32_MAX); +CONFIG_GETTER(Integer, int); +CONFIG_SETTER(Integer, int); static bool configParseConfigJson(void) { @@ -140,7 +124,7 @@ static bool configParseConfigJson(void) g_configJson = json_object_from_file(CONFIG_PATH); if (!g_configJson) { - configLogJsonError(); + jsonLogLastError(); goto end; } @@ -163,7 +147,7 @@ end: configWriteConfigJson(); ret = true; } else { - configLogJsonError(); + jsonLogLastError(); } } @@ -173,7 +157,7 @@ end: static void configWriteConfigJson(void) { if (!g_configJson) return; - if (json_object_to_file_ext(CONFIG_PATH, g_configJson, JSON_C_TO_STRING_SPACED | JSON_C_TO_STRING_PRETTY) != 0) configLogJsonError(); + if (json_object_to_file_ext(CONFIG_PATH, g_configJson, JSON_C_TO_STRING_SPACED | JSON_C_TO_STRING_PRETTY) != 0) jsonLogLastError(); } static void configFreeConfigJson(void) @@ -183,70 +167,22 @@ static void configFreeConfigJson(void) g_configJson = NULL; } -static struct json_object *configGetJsonObjectByPath(const struct json_object *obj, const char *path) -{ - const struct json_object *parent_obj = obj; - struct json_object *child_obj = NULL; - char *path_dup = NULL, *pch = NULL, *state = NULL; - - if (!configValidateJsonObject(obj) || !path || !*path) - { - LOG_MSG("Invalid parameters!"); - return NULL; - } - - /* Duplicate path to avoid problems with strtok_r(). */ - if (!(path_dup = strdup(path))) - { - LOG_MSG("Unable to duplicate input path! (\"%s\").", path); - return NULL; - } - - pch = strtok_r(path_dup, "/", &state); - if (!pch) - { - LOG_MSG("Failed to tokenize input path! (\"%s\").", path); - goto end; - } - - while(pch) - { - if (!json_object_object_get_ex(parent_obj, pch, &child_obj)) - { - LOG_MSG("Failed to retrieve JSON object by key for \"%s\"! (\"%s\").", pch, path); - break; - } - - pch = strtok_r(NULL, "/", &state); - if (pch) - { - parent_obj = child_obj; - child_obj = NULL; - } - } - -end: - if (path_dup) free(path_dup); - - return child_obj; -} - static bool configValidateJsonRootObject(const struct json_object *obj) { bool ret = false, overclock_found = false, naming_convention_found = false, dump_destination_found = false, gamecard_found = false; bool nsp_found = false, ticket_found = false, nca_fs_found = false; - if (!configValidateJsonObject(obj)) goto end; + if (!jsonValidateObject(obj)) goto end; json_object_object_foreach(obj, key, val) { - JSON_VALIDATE_FIELD(Boolean, overclock); - JSON_VALIDATE_FIELD(Integer, naming_convention, TitleNamingConvention_Full, TitleNamingConvention_Count - 1); - JSON_VALIDATE_FIELD(Integer, dump_destination, ConfigDumpDestination_SdCard, ConfigDumpDestination_Count - 1); - JSON_VALIDATE_OBJECT(GameCard, gamecard); - JSON_VALIDATE_OBJECT(Nsp, nsp); - JSON_VALIDATE_OBJECT(Ticket, ticket); - JSON_VALIDATE_OBJECT(NcaFs, nca_fs); + CONFIG_VALIDATE_FIELD(Boolean, overclock); + CONFIG_VALIDATE_FIELD(Integer, naming_convention, TitleNamingConvention_Full, TitleNamingConvention_Count - 1); + CONFIG_VALIDATE_FIELD(Integer, dump_destination, ConfigDumpDestination_SdCard, ConfigDumpDestination_Count - 1); + CONFIG_VALIDATE_OBJECT(GameCard, gamecard); + CONFIG_VALIDATE_OBJECT(Nsp, nsp); + CONFIG_VALIDATE_OBJECT(Ticket, ticket); + CONFIG_VALIDATE_OBJECT(NcaFs, nca_fs); goto end; } @@ -260,15 +196,15 @@ static bool configValidateJsonGameCardObject(const struct json_object *obj) { bool ret = false, append_key_area_found = false, keep_certificate_found = false, trim_dump_found = false, calculate_checksum_found = false, checksum_lookup_method_found = false; - if (!configValidateJsonObject(obj)) goto end; + if (!jsonValidateObject(obj)) goto end; json_object_object_foreach(obj, key, val) { - JSON_VALIDATE_FIELD(Boolean, append_key_area); - JSON_VALIDATE_FIELD(Boolean, keep_certificate); - JSON_VALIDATE_FIELD(Boolean, trim_dump); - JSON_VALIDATE_FIELD(Boolean, calculate_checksum); - JSON_VALIDATE_FIELD(Integer, checksum_lookup_method, ConfigChecksumLookupMethod_None, ConfigChecksumLookupMethod_Count - 1); + CONFIG_VALIDATE_FIELD(Boolean, append_key_area); + CONFIG_VALIDATE_FIELD(Boolean, keep_certificate); + CONFIG_VALIDATE_FIELD(Boolean, trim_dump); + CONFIG_VALIDATE_FIELD(Boolean, calculate_checksum); + CONFIG_VALIDATE_FIELD(Integer, checksum_lookup_method, ConfigChecksumLookupMethod_None, ConfigChecksumLookupMethod_Count - 1); goto end; } @@ -283,20 +219,20 @@ static bool configValidateJsonNspObject(const struct json_object *obj) bool ret = false, set_download_distribution_found = false, remove_console_data_found = false, remove_titlekey_crypto_found = false, replace_acid_key_sig_found = false; bool disable_linked_account_requirement_found = false, enable_screenshots_found = false, enable_video_capture_found = false, disable_hdcp_found = false, append_authoringtool_data_found = false, lookup_checksum_found = false; - if (!configValidateJsonObject(obj)) goto end; + if (!jsonValidateObject(obj)) goto end; json_object_object_foreach(obj, key, val) { - JSON_VALIDATE_FIELD(Boolean, set_download_distribution); - JSON_VALIDATE_FIELD(Boolean, remove_console_data); - JSON_VALIDATE_FIELD(Boolean, remove_titlekey_crypto); - JSON_VALIDATE_FIELD(Boolean, replace_acid_key_sig); - JSON_VALIDATE_FIELD(Boolean, disable_linked_account_requirement); - JSON_VALIDATE_FIELD(Boolean, enable_screenshots); - JSON_VALIDATE_FIELD(Boolean, enable_video_capture); - JSON_VALIDATE_FIELD(Boolean, disable_hdcp); - JSON_VALIDATE_FIELD(Boolean, lookup_checksum); - JSON_VALIDATE_FIELD(Boolean, append_authoringtool_data); + CONFIG_VALIDATE_FIELD(Boolean, set_download_distribution); + CONFIG_VALIDATE_FIELD(Boolean, remove_console_data); + CONFIG_VALIDATE_FIELD(Boolean, remove_titlekey_crypto); + CONFIG_VALIDATE_FIELD(Boolean, replace_acid_key_sig); + CONFIG_VALIDATE_FIELD(Boolean, disable_linked_account_requirement); + CONFIG_VALIDATE_FIELD(Boolean, enable_screenshots); + CONFIG_VALIDATE_FIELD(Boolean, enable_video_capture); + CONFIG_VALIDATE_FIELD(Boolean, disable_hdcp); + CONFIG_VALIDATE_FIELD(Boolean, lookup_checksum); + CONFIG_VALIDATE_FIELD(Boolean, append_authoringtool_data); goto end; } @@ -311,11 +247,11 @@ static bool configValidateJsonTicketObject(const struct json_object *obj) { bool ret = false, remove_console_data_found = false; - if (!configValidateJsonObject(obj)) goto end; + if (!jsonValidateObject(obj)) goto end; json_object_object_foreach(obj, key, val) { - JSON_VALIDATE_FIELD(Boolean, remove_console_data); + CONFIG_VALIDATE_FIELD(Boolean, remove_console_data); goto end; } @@ -329,11 +265,11 @@ static bool configValidateJsonNcaFsObject(const struct json_object *obj) { bool ret = false, use_layeredfs_dir_found = false; - if (!configValidateJsonObject(obj)) goto end; + if (!jsonValidateObject(obj)) goto end; json_object_object_foreach(obj, key, val) { - JSON_VALIDATE_FIELD(Boolean, use_layeredfs_dir); + CONFIG_VALIDATE_FIELD(Boolean, use_layeredfs_dir); goto end; } @@ -342,34 +278,3 @@ static bool configValidateJsonNcaFsObject(const struct json_object *obj) end: return ret; } - -NX_INLINE bool configValidateJsonBoolean(const struct json_object *obj) -{ - return (obj != NULL && json_object_is_type(obj, json_type_boolean)); -} - -NX_INLINE bool configValidateJsonInteger(const struct json_object *obj, int lower_boundary, int upper_boundary) -{ - if (!obj || !json_object_is_type(obj, json_type_int) || lower_boundary > upper_boundary) return false; - int val = json_object_get_int(obj); - return (val >= lower_boundary && val <= upper_boundary); -} - -NX_INLINE bool configValidateJsonObject(const struct json_object *obj) -{ - return (obj != NULL && json_object_is_type(obj, json_type_object) && json_object_object_length(obj) > 0); -} - -static void configLogJsonError(void) -{ - size_t str_len = 0; - char *str = (char*)json_util_get_last_err(); /* Drop the const. */ - if (!str || !(str_len = strlen(str))) return; - - /* Remove line breaks. */ - if (str[str_len - 1] == '\n') str[--str_len] = '\0'; - if (str[str_len - 1] == '\r') str[--str_len] = '\0'; - - /* Log error message. */ - LOG_MSG("%s", str); -} diff --git a/source/core/nxdt_json.c b/source/core/nxdt_json.c new file mode 100644 index 0000000..9bf8417 --- /dev/null +++ b/source/core/nxdt_json.c @@ -0,0 +1,220 @@ +/* + * nxdt_json.c + * + * Copyright (c) 2020-2021, DarkMatterCore . + * + * 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 . + */ + +#include "nxdt_utils.h" +#include "nxdt_json.h" + +#define JSON_GETTER(functype, vartype, jsontype, ...) \ +vartype jsonGet##functype(const struct json_object *obj, const char *path) { \ + vartype ret = (vartype)0; \ + struct json_object *child = jsonGetObjectByPath(obj, path, NULL); \ + if (child && jsonValidate##functype(child, ##__VA_ARGS__)) ret = (vartype)json_object_get_##jsontype(child); \ + return ret; \ +} + +#define JSON_SETTER(functype, vartype, jsontype, ...) \ +bool jsonSet##functype(const struct json_object *obj, const char *path, vartype value) { \ + bool ret = false; \ + struct json_object *child = jsonGetObjectByPath(obj, path, NULL); \ + if (child && jsonValidate##functype(child, ##__VA_ARGS__)) { \ + ret = (json_object_set_##jsontype(child, value) == 1); \ + if (!ret) LOG_MSG("Failed to update \"%s\"!", path); \ + } \ + return ret; \ +} + +struct json_object *jsonParseFromString(const char *str, size_t size) +{ + if (!str || !*str) + { + LOG_MSG("Invalid parameters!"); + return false; + } + + /* Calculate string size if it wasn't provided. */ + if (!size) size = (strlen(str) + 1); + + struct json_tokener *tok = NULL; + struct json_object *obj = NULL; + enum json_tokener_error jerr = json_tokener_success; + + /* Allocate tokener. */ + tok = json_tokener_new(); + if (!tok) + { + LOG_MSG("json_tokener_new failed!"); + goto end; + } + + /* Parse JSON buffer. */ + obj = json_tokener_parse_ex(tok, str, (int)size); + if ((jerr = json_tokener_get_error(tok)) != json_tokener_success) + { + LOG_MSG("json_tokener_parse_ex failed! Reason: \"%s\".", json_tokener_error_desc(jerr)); + + if (obj) + { + json_object_put(obj); + obj = NULL; + } + } + +end: + if (tok) json_tokener_free(tok); + + return obj; +} + +struct json_object *jsonGetObjectByPath(const struct json_object *obj, const char *path, char **out_last_element) +{ + const struct json_object *parent_obj = obj; + struct json_object *child_obj = NULL; + char *path_dup = NULL, *pch = NULL, *state = NULL, *prev_pch = NULL; + + if (!jsonValidateObject(obj) || !path || !*path) + { + LOG_MSG("Invalid parameters!"); + return NULL; + } + + /* Duplicate path to avoid problems with strtok_r(). */ + if (!(path_dup = strdup(path))) + { + LOG_MSG("Unable to duplicate input path! (\"%s\").", path); + return NULL; + } + + /* Tokenize input path. */ + pch = strtok_r(path_dup, "/", &state); + if (!pch) + { + LOG_MSG("Failed to tokenize input path! (\"%s\").", path); + goto end; + } + + while(pch) + { + prev_pch = pch; + + pch = strtok_r(NULL, "/", &state); + if (pch || !out_last_element) + { + /* Retrieve JSON object using the current path element. */ + if (!json_object_object_get_ex(parent_obj, prev_pch, &child_obj)) + { + LOG_MSG("Failed to retrieve JSON object by key for \"%s\"! (\"%s\").", prev_pch, path); + break; + } + + /* Update parent and child pointers if we can still proceed. */ + if (pch) + { + parent_obj = child_obj; + child_obj = NULL; + } + } else { + /* No additional path elements can be found + the user wants the string for the last path element. */ + /* Let's start by setting the last parent object as the return value. */ + child_obj = (struct json_object*)parent_obj; /* Drop the const. */ + + /* Duplicate last path element string. */ + *out_last_element = strdup(prev_pch); + if (!*out_last_element) + { + LOG_MSG("Failed to duplicate last path element \"%s\"! (\"%s\").", prev_pch, path); + child_obj = NULL; + } + } + } + +end: + if (path_dup) free(path_dup); + + return child_obj; +} + +void jsonLogLastError(void) +{ + size_t str_len = 0; + char *str = (char*)json_util_get_last_err(); /* Drop the const. */ + if (!str || !(str_len = strlen(str))) return; + + /* Remove line breaks. */ + if (str[str_len - 1] == '\n') str[--str_len] = '\0'; + if (str[str_len - 1] == '\r') str[--str_len] = '\0'; + + /* Log error message. */ + LOG_MSG("%s", str); +} + +JSON_GETTER(Boolean, bool, boolean); +JSON_SETTER(Boolean, bool, boolean); + +JSON_GETTER(Integer, int, int, INT32_MIN, INT32_MAX); +JSON_SETTER(Integer, int, int, INT32_MIN, INT32_MAX); + +JSON_GETTER(String, const char*, string); +JSON_SETTER(String, const char*, string); + +/* Special handling for JSON arrays. */ + +struct json_object *jsonGetArray(const struct json_object *obj, const char *path) +{ + struct json_object *ret = NULL, *child = jsonGetObjectByPath(obj, path, NULL); + if (child && jsonValidateArray(child)) ret = child; + return ret; +} + +bool jsonSetArray(const struct json_object *obj, const char *path, struct json_object *value) +{ + if (!obj || !path || !*path || !jsonValidateArray(value)) + { + LOG_MSG("Invalid parameters!"); + return false; + } + + bool ret = false; + struct json_object *parent_obj = NULL; + char *key = NULL; + + /* Get parent JSON object. */ + parent_obj = jsonGetObjectByPath(obj, path, &key); + if (!parent_obj) + { + LOG_MSG("Failed to retrieve parent JSON object! (\"%s\").", path); + return false; + } + + /* Set new JSON array. */ + if (json_object_object_add(parent_obj, key, value) != 0) + { + LOG_MSG("json_object_object_add failed! (\"%s\").", path); + goto end; + } + + /* Update return value. */ + ret = true; + +end: + if (key) free(key); + + return ret; +} diff --git a/source/core/nxdt_log.c b/source/core/nxdt_log.c index e7f837f..4964dee 100644 --- a/source/core/nxdt_log.c +++ b/source/core/nxdt_log.c @@ -70,21 +70,23 @@ __attribute__((format(printf, 4, 5))) void logWriteFormattedStringToBuffer(char char *dst_ptr = *dst, *tmp_str = NULL; size_t dst_cur_size = *dst_size, dst_str_len = (dst_ptr ? strlen(dst_ptr) : 0); + struct tm ts = {0}; + struct timespec now = {0}; + if (dst_str_len >= dst_cur_size) return; va_start(args, fmt); /* Get current time with nanosecond precision. */ - struct timespec now = {0}; clock_gettime(CLOCK_REALTIME, &now); /* Get local time. */ - struct tm *ts = localtime(&(now.tv_sec)); - ts->tm_year += 1900; - ts->tm_mon++; + localtime_r(&(now.tv_sec), &ts); + ts.tm_year += 1900; + ts.tm_mon++; /* Get formatted string length. */ - str1_len = snprintf(NULL, 0, g_logStrFormat, ts->tm_year, ts->tm_mon, ts->tm_mday, ts->tm_hour, ts->tm_min, ts->tm_sec, now.tv_nsec, func_name); + str1_len = snprintf(NULL, 0, g_logStrFormat, ts.tm_year, ts.tm_mon, ts.tm_mday, ts.tm_hour, ts.tm_min, ts.tm_sec, now.tv_nsec, func_name); if (str1_len <= 0) goto end; str2_len = vsnprintf(NULL, 0, fmt, args); @@ -113,7 +115,7 @@ __attribute__((format(printf, 4, 5))) void logWriteFormattedStringToBuffer(char } /* Generate formatted string. */ - sprintf(dst_ptr + dst_str_len, g_logStrFormat, ts->tm_year, ts->tm_mon, ts->tm_mday, ts->tm_hour, ts->tm_min, ts->tm_sec, now.tv_nsec, func_name); + sprintf(dst_ptr + dst_str_len, g_logStrFormat, ts.tm_year, ts.tm_mon, ts.tm_mday, ts.tm_hour, ts.tm_min, ts.tm_sec, now.tv_nsec, func_name); vsprintf(dst_ptr + dst_str_len + (size_t)str1_len, fmt, args); strcat(dst_ptr, CRLF); @@ -272,17 +274,19 @@ static void _logWriteFormattedStringToLogFile(bool save, const char *func_name, char *tmp_str = NULL; size_t tmp_len = 0; - /* Get current time with nanosecond precision. */ + struct tm ts = {0}; struct timespec now = {0}; + + /* Get current time with nanosecond precision. */ clock_gettime(CLOCK_REALTIME, &now); /* Get local time. */ - struct tm *ts = localtime(&(now.tv_sec)); - ts->tm_year += 1900; - ts->tm_mon++; + localtime_r(&(now.tv_sec), &ts); + ts.tm_year += 1900; + ts.tm_mon++; /* Get formatted string length. */ - str1_len = snprintf(NULL, 0, g_logStrFormat, ts->tm_year, ts->tm_mon, ts->tm_mday, ts->tm_hour, ts->tm_min, ts->tm_sec, now.tv_nsec, func_name); + str1_len = snprintf(NULL, 0, g_logStrFormat, ts.tm_year, ts.tm_mon, ts.tm_mday, ts.tm_hour, ts.tm_min, ts.tm_sec, now.tv_nsec, func_name); if (str1_len <= 0) return; str2_len = vsnprintf(NULL, 0, fmt, args); @@ -314,7 +318,7 @@ static void _logWriteFormattedStringToLogFile(bool save, const char *func_name, } /* Nice and easy string formatting using the log buffer. */ - sprintf(g_logBuffer + g_logBufferLength, g_logStrFormat, ts->tm_year, ts->tm_mon, ts->tm_mday, ts->tm_hour, ts->tm_min, ts->tm_sec, now.tv_nsec, func_name); + sprintf(g_logBuffer + g_logBufferLength, g_logStrFormat, ts.tm_year, ts.tm_mon, ts.tm_mday, ts.tm_hour, ts.tm_min, ts.tm_sec, now.tv_nsec, func_name); vsprintf(g_logBuffer + g_logBufferLength + (size_t)str1_len, fmt, args); strcat(g_logBuffer, CRLF); g_logBufferLength += log_str_len; @@ -328,7 +332,7 @@ static void _logWriteFormattedStringToLogFile(bool save, const char *func_name, if (!tmp_str) return; /* Generate formatted string. */ - sprintf(tmp_str, g_logStrFormat, ts->tm_year, ts->tm_mon, ts->tm_mday, ts->tm_hour, ts->tm_min, ts->tm_sec, now.tv_nsec, func_name); + sprintf(tmp_str, g_logStrFormat, ts.tm_year, ts.tm_mon, ts.tm_mday, ts.tm_hour, ts.tm_min, ts.tm_sec, now.tv_nsec, func_name); vsprintf(tmp_str + (size_t)str1_len, fmt, args); strcat(tmp_str, CRLF); diff --git a/source/core/nxdt_utils.c b/source/core/nxdt_utils.c index 11ffcb7..f55dbb8 100644 --- a/source/core/nxdt_utils.c +++ b/source/core/nxdt_utils.c @@ -79,6 +79,8 @@ static const char *g_outputDirs[] = { static const size_t g_outputDirsCount = MAX_ELEMENTS(g_outputDirs); +static bool g_appUpdated = false; + /* Function prototypes. */ static void _utilsGetLaunchPath(int program_argc, const char **program_argv); @@ -114,7 +116,7 @@ bool utilsInitializeResources(const int program_argc, const char **program_argv) _utilsGetLaunchPath(program_argc, program_argv); /* Retrieve pointer to the SD card FsFileSystem element. */ - if (!(g_sdCardFileSystem = fsdevGetDeviceFileSystem("sdmc:"))) + if (!(g_sdCardFileSystem = fsdevGetDeviceFileSystem(DEVOPTAB_SDMC_DEVICE))) { LOG_MSG("Failed to retrieve FsFileSystem object for the SD card!"); break; @@ -123,7 +125,6 @@ bool utilsInitializeResources(const int program_argc, const char **program_argv) /* Create logfile. */ logWriteStringToLogFile("________________________________________________________________\r\n"); LOG_MSG(APP_TITLE " v%u.%u.%u starting (" GIT_REV "). Built on " __DATE__ " - " __TIME__ ".", VERSION_MAJOR, VERSION_MINOR, VERSION_MICRO); - if (g_appLaunchPath) LOG_MSG("Launch path: \"%s\".", g_appLaunchPath); /* Log Horizon OS version. */ u32 hos_version = hosversionGet(); @@ -148,6 +149,22 @@ bool utilsInitializeResources(const int program_argc, const char **program_argv) /* Create output directories (SD card only). */ utilsCreateOutputDirectories(NULL); + if (g_appLaunchPath) + { + LOG_MSG("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) + { + remove(NRO_PATH); + rename(g_appLaunchPath, NRO_PATH); + + LOG_MSG("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; @@ -280,6 +297,14 @@ void utilsCloseResources(void) /* 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); + }*/ + /* Close logfile. */ logCloseLogFile(); @@ -622,7 +647,7 @@ void utilsCreateOutputDirectories(const char *device) for(size_t i = 0; i < g_outputDirsCount; i++) { - sprintf(path, "%s%s", (device ? device : "sdmc:"), g_outputDirs[i]); + sprintf(path, "%s%s", (device ? device : DEVOPTAB_SDMC_DEVICE), g_outputDirs[i]); mkdir(path, 0744); } } @@ -803,13 +828,103 @@ end: return path; } +bool utilsParseGitHubReleaseJsonData(const char *json_buf, size_t json_buf_size, UtilsGitHubReleaseJsonData *out) +{ + if (!json_buf || !json_buf_size || !out) + { + LOG_MSG("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("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("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("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("Failed to retrieve required elements from the provided JSON!"); + goto end; + } + + /* Update return value. */ + ret = true; + +end: + if (!ret) utilsFreeGitHubReleaseJsonData(out); + + return ret; +} + +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; +} + static void _utilsGetLaunchPath(int program_argc, const char **program_argv) { if (program_argc <= 0 || !program_argv) return; for(int i = 0; i < program_argc; i++) { - if (program_argv[i] && !strncmp(program_argv[i], "sdmc:/", 6)) + if (program_argv[i] && !strncmp(program_argv[i], DEVOPTAB_SDMC_DEVICE "/", 6)) { g_appLaunchPath = program_argv[i]; break; diff --git a/source/core/save.c b/source/core/save.c index 43e86c2..ae06170 100644 --- a/source/core/save.c +++ b/source/core/save.c @@ -1749,7 +1749,7 @@ save_ctx_t *save_open_savefile(const char *path, u32 action) /* Code to dump the requested file in its entirety. Useful to retrieve protected system savefiles without exiting HOS. */ /*char sd_path[FS_MAX_PATH] = {0}; - sprintf(sd_path, "sdmc:/%s", strrchr(path, '/') + 1); + sprintf(sd_path, DEVOPTAB_SDMC_DEVICE "/%s", strrchr(path, '/') + 1); UINT blksize = 0x100000; u8 *buf = malloc(blksize); diff --git a/source/options_tab.cpp b/source/options_tab.cpp index a805288..a2d19a1 100644 --- a/source/options_tab.cpp +++ b/source/options_tab.cpp @@ -21,75 +21,80 @@ #include #include +#include +#include #include +#include -using namespace brls::i18n::literals; /* For _i18n. */ +namespace i18n = brls::i18n; /* For getStr(). */ +using namespace i18n::literals; /* For _i18n. */ namespace nxdt::views { - OptionsTabUpdateFileDialogContent::OptionsTabUpdateFileDialogContent(void) + OptionsTabUpdateProgress::OptionsTabUpdateProgress(void) { this->progress_display = new brls::ProgressDisplay(); this->progress_display->setParent(this); - this->size_label = new brls::Label(brls::LabelStyle::MEDIUM, "", false); - this->size_label->setVerticalAlign(NVG_ALIGN_BOTTOM); - this->size_label->setParent(this); + this->size_lbl = new brls::Label(brls::LabelStyle::MEDIUM, "", false); + this->size_lbl->setVerticalAlign(NVG_ALIGN_BOTTOM); + this->size_lbl->setParent(this); - this->speed_eta_label = new brls::Label(brls::LabelStyle::MEDIUM, "", false); - this->speed_eta_label->setVerticalAlign(NVG_ALIGN_TOP); - this->speed_eta_label->setParent(this); + this->speed_eta_lbl = new brls::Label(brls::LabelStyle::MEDIUM, "", false); + this->speed_eta_lbl->setVerticalAlign(NVG_ALIGN_TOP); + this->speed_eta_lbl->setParent(this); } - OptionsTabUpdateFileDialogContent::~OptionsTabUpdateFileDialogContent(void) + OptionsTabUpdateProgress::~OptionsTabUpdateProgress(void) { delete this->progress_display; - delete this->size_label; - delete this->speed_eta_label; + delete this->size_lbl; + delete this->speed_eta_lbl; } - void OptionsTabUpdateFileDialogContent::SetProgress(const nxdt::tasks::DownloadTaskProgress& progress) + void OptionsTabUpdateProgress::SetProgress(const nxdt::tasks::DownloadTaskProgress& progress) { /* Update progress percentage. */ - this->progress_display->setProgress(progress.size ? progress.percentage : 0, 100); + this->progress_display->setProgress(progress.percentage, 100); /* Update size string. */ - this->size_label->setText(fmt::format("{} / {}", this->GetFormattedSizeString(progress.current), progress.size ? this->GetFormattedSizeString(progress.size) : "?")); + this->size_lbl->setText(fmt::format("{} / {}", this->GetFormattedSizeString(static_cast(progress.current)), \ + progress.size ? this->GetFormattedSizeString(static_cast(progress.size)) : "?")); /* Update speed / ETA string. */ - if (progress.eta != "") + if (progress.eta.length()) { - this->speed_eta_label->setText(fmt::format("{}/s - ETA: {}", this->GetFormattedSizeString(progress.speed), progress.eta)); + this->speed_eta_lbl->setText(fmt::format("{}/s - ETA: {}", this->GetFormattedSizeString(progress.speed), progress.eta)); } else { - this->speed_eta_label->setText(fmt::format("{}/s", this->GetFormattedSizeString(progress.speed))); + this->speed_eta_lbl->setText(fmt::format("{}/s", this->GetFormattedSizeString(progress.speed))); } this->invalidate(); } - void OptionsTabUpdateFileDialogContent::willAppear(bool resetState) + void OptionsTabUpdateProgress::willAppear(bool resetState) { this->progress_display->willAppear(resetState); } - void OptionsTabUpdateFileDialogContent::willDisappear(bool resetState) + void OptionsTabUpdateProgress::willDisappear(bool resetState) { this->progress_display->willDisappear(resetState); } - void OptionsTabUpdateFileDialogContent::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, brls::Style* style, brls::FrameContext* ctx) + void OptionsTabUpdateProgress::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, brls::Style* style, brls::FrameContext* ctx) { /* Progress display. */ this->progress_display->frame(ctx); /* Size label. */ - this->size_label->frame(ctx); + this->size_lbl->frame(ctx); /* Speed / ETA label. */ - this->speed_eta_label->frame(ctx); + this->speed_eta_lbl->frame(ctx); } - void OptionsTabUpdateFileDialogContent::layout(NVGcontext* vg, brls::Style* style, brls::FontStash* stash) + void OptionsTabUpdateProgress::layout(NVGcontext* vg, brls::Style* style, brls::FontStash* stash) { unsigned elem_width = roundf(static_cast(this->width) * 0.90f); @@ -103,34 +108,27 @@ namespace nxdt::views this->progress_display->invalidate(true); /* Size label. */ - this->size_label->setWidth(elem_width); - this->size_label->invalidate(true); + this->size_lbl->setWidth(elem_width); + this->size_lbl->invalidate(true); - this->size_label->setBoundaries( - this->x + (this->width - this->size_label->getWidth()) / 2, + this->size_lbl->setBoundaries( + this->x + (this->width - this->size_lbl->getWidth()) / 2, this->progress_display->getY() - this->progress_display->getHeight() / 8, - this->size_label->getWidth(), - this->size_label->getHeight()); + this->size_lbl->getWidth(), + this->size_lbl->getHeight()); /* Speed / ETA label. */ - this->speed_eta_label->setWidth(elem_width); - this->speed_eta_label->invalidate(true); + this->speed_eta_lbl->setWidth(elem_width); + this->speed_eta_lbl->invalidate(true); - this->speed_eta_label->setBoundaries( - this->x + (this->width - this->speed_eta_label->getWidth()) / 2, + this->speed_eta_lbl->setBoundaries( + this->x + (this->width - this->speed_eta_lbl->getWidth()) / 2, this->progress_display->getY() + this->progress_display->getHeight() + this->progress_display->getHeight() / 8, - this->speed_eta_label->getWidth(), - this->speed_eta_label->getHeight()); + this->speed_eta_lbl->getWidth(), + this->speed_eta_lbl->getHeight()); } - std::string OptionsTabUpdateFileDialogContent::GetFormattedSizeString(size_t size) - { - char strbuf[0x40] = {0}; - utilsGenerateFormattedSizeString(static_cast(size), strbuf, sizeof(strbuf)); - return std::string(strbuf); - } - - std::string OptionsTabUpdateFileDialogContent::GetFormattedSizeString(double size) + std::string OptionsTabUpdateProgress::GetFormattedSizeString(double size) { char strbuf[0x40] = {0}; utilsGenerateFormattedSizeString(size, strbuf, sizeof(strbuf)); @@ -140,27 +138,31 @@ namespace nxdt::views OptionsTabUpdateFileDialog::OptionsTabUpdateFileDialog(std::string path, std::string url, bool force_https, std::string success_str) : brls::Dialog(), success_str(success_str) { /* Set content view. */ - OptionsTabUpdateFileDialogContent *content = new OptionsTabUpdateFileDialogContent(); - this->setContentView(content); + OptionsTabUpdateProgress *update_progress = new OptionsTabUpdateProgress(); + this->setContentView(update_progress); /* Add cancel button. */ this->addButton("options_tab/update_dialog/cancel"_i18n, [this](brls::View* view) { - this->onCancel(); + /* Cancel download task. */ + this->download_task.cancel(); + + /* Close dialog. */ + this->close(); }); /* Disable cancelling with B button. */ this->setCancelable(false); /* Subscribe to the download task. */ - this->download_task.RegisterListener([this, content](const nxdt::tasks::DownloadTaskProgress& progress) { + this->download_task.RegisterListener([this, update_progress](const nxdt::tasks::DownloadTaskProgress& progress) { /* Update progress. */ - content->SetProgress(progress); + update_progress->SetProgress(progress); /* Check if the download task has finished. */ if (this->download_task.isFinished()) { /* Stop spinner. */ - content->willDisappear(); + update_progress->willDisappear(); /* Update button label. */ this->setButtonText(0, "options_tab/update_dialog/close"_i18n); @@ -174,17 +176,205 @@ namespace nxdt::views this->download_task.execute(path, url, force_https); } - bool OptionsTabUpdateFileDialog::onCancel(void) + OptionsTabUpdateApplicationFrame::OptionsTabUpdateApplicationFrame(void) : brls::StagedAppletFrame(false) { - /* Cancel download task. */ - this->download_task.cancel(); + /* Set UI properties. */ + this->setTitle("options_tab/update_app/label"_i18n); + this->setIcon(BOREALIS_ASSET("icon/" APP_TITLE ".jpg")); + this->setFooterText("v" APP_VERSION " (" GIT_REV ")"); - /* Close dialog. */ - this->close(); + /* Add first stage. */ + this->wait_lbl = new brls::Label(brls::LabelStyle::DIALOG, "options_tab/update_app/frame/please_wait"_i18n, false); + this->wait_lbl->setHorizontalAlign(NVG_ALIGN_CENTER); + this->addStage(this->wait_lbl); + + /* Add second stage. */ + this->changelog_list = new brls::List(); + this->changelog_list->setSpacing(this->changelog_list->getSpacing() / 2); + this->changelog_list->setMarginBottom(20); + this->addStage(this->changelog_list); + + /* Add third stage. */ + this->update_progress = new OptionsTabUpdateProgress(); + this->addStage(this->update_progress); + + /* Register cancel action. */ + this->registerAction("brls/hints/back"_i18n, brls::Key::B, [this](void){ + return this->onCancel(); + }); + + /* Subscribe to the global focus change event so we can rebuild hints as soon as this frame is pushed to the view stack. */ + this->focus_event_sub = brls::Application::getGlobalFocusChangeEvent()->subscribe([this](brls::View* view) { + this->rebuildHints(); + }); + + /* Subscribe to the JSON task. */ + this->json_task.RegisterListener([this](const nxdt::tasks::DownloadTaskProgress& progress) { + /* Return immediately if the JSON task hasn't finished. */ + if (!this->json_task.isFinished()) return; + + /* Retrieve task result. */ + nxdt::tasks::DownloadDataResult json_task_result = this->json_task.get(); + this->json_buf = json_task_result.first; + this->json_buf_size = json_task_result.second; + + /* Parse downloaded JSON object. */ + if (utilsParseGitHubReleaseJsonData(this->json_buf, this->json_buf_size, &(this->json_data))) + { + /* Display changelog. */ + this->DisplayChangelog(); + } else { + /* Log downloaded data if we failed to parse it. */ + LOG_DATA(this->json_buf, this->json_buf_size, "Failed to parse GitHub release JSON. Downloaded data:"); + + /* Display notification. */ + brls::Application::notify("options_tab/notifications/github_json_failed"_i18n); + + /* Pop view */ + this->onCancel(); + } + }); + + /* Start JSON task. */ + this->json_task.execute(GITHUB_API_RELEASE_URL, true); + } + + OptionsTabUpdateApplicationFrame::~OptionsTabUpdateApplicationFrame(void) + { + /* Free parsed JSON data. */ + utilsFreeGitHubReleaseJsonData(&(this->json_data)); + + /* Free JSON buffer. */ + if (this->json_buf) free(this->json_buf); + + /* Unsubscribe focus event listener. */ + brls::Application::getGlobalFocusChangeEvent()->unsubscribe(this->focus_event_sub); + } + + void OptionsTabUpdateApplicationFrame::layout(NVGcontext* vg, brls::Style* style, brls::FontStash* stash) + { + brls::StagedAppletFrame::layout(vg, style, stash); + + if (this->getCurrentStage() == 0) + { + /* Center wait label. */ + this->wait_lbl->setBoundaries(this->x + (this->width / 2), this->y, this->width, this->height); + this->wait_lbl->invalidate(); + } + } + + bool OptionsTabUpdateApplicationFrame::onCancel(void) + { + /* Cancel NRO task. */ + this->nro_task.cancel(); + + /* Cancel JSON task. */ + this->json_task.cancel(); + + /* Pop view. */ + brls::Application::popView(); return true; } + void OptionsTabUpdateApplicationFrame::DisplayChangelog(void) + { + std::string item; + std::stringstream ss(std::string(this->json_data.changelog)); + + /* Display version string at the top. */ + FocusableLabel *version_lbl = new FocusableLabel(false, brls::LabelStyle::CRASH, std::string(this->json_data.version), true); + version_lbl->setHorizontalAlign(NVG_ALIGN_CENTER); + this->changelog_list->addView(version_lbl); + + /* Display release date and commit hash. */ + brls::Label *release_details_lbl = new brls::Label(brls::LabelStyle::DESCRIPTION, i18n::getStr("options_tab/update_app/frame/release_details"_i18n, \ + this->json_data.commit_hash, RootView::GetFormattedDateString(this->json_data.date)), true); + release_details_lbl->setHorizontalAlign(NVG_ALIGN_CENTER); + this->changelog_list->addView(release_details_lbl); + + /* Add changelog header. */ + this->changelog_list->addView(new brls::Header("options_tab/update_app/frame/changelog_header"_i18n)); + + /* Split changelog string and fill list. */ + while(std::getline(ss, item)) + { + /* Don't proceed if this is an empty line. */ + /* Make sure to remove any possible carriage returns. */ + size_t item_len = item.length(); + if (!item_len) continue; + + if (item.back() == '\r') + { + if (item_len > 1) + { + item.pop_back(); + } else { + continue; + } + } + + /* Add line to the changelog view. */ + this->changelog_list->addView(new FocusableLabel(false, brls::LabelStyle::SMALL, item, true)); + } + + /* Register update action. */ + this->registerAction("options_tab/update_app/frame/update_action"_i18n, brls::Key::PLUS, [this](void){ + /* Display update progress. */ + this->DisplayUpdateProgress(); + + return true; + }); + + /* Rebuild action hints. */ + this->rebuildHints(); + + /* Go to the next stage. */ + this->nextStage(); + } + + void OptionsTabUpdateApplicationFrame::DisplayUpdateProgress(void) + { + /* Remove update action. */ + this->registerAction("options_tab/update_app/frame/update_action"_i18n, brls::Key::PLUS, [](void){ + return true; + }, true); + + /* Register cancel action once more, using a different label. */ + this->registerAction("options_tab/update_dialog/cancel"_i18n, brls::Key::B, [this](void){ + return this->onCancel(); + }); + + /* Rebuild action hints. */ + this->rebuildHints(); + + /* Subscribe to the NRO task. */ + this->nro_task.RegisterListener([this](const nxdt::tasks::DownloadTaskProgress& progress) { + /* Update progress. */ + this->update_progress->SetProgress(progress); + + /* Check if the download task has finished. */ + if (this->nro_task.isFinished()) + { + /* Get NRO task result and immediately set application updated state if the task succeeded. */ + bool ret = this->nro_task.get(); + if (ret) utilsSetApplicationUpdatedState(); + + /* Display notification. */ + brls::Application::notify(ret ? "options_tab/notifications/app_updated"_i18n : "options_tab/notifications/update_failed"_i18n); + + /* Pop view */ + this->onCancel(); + } + }); + + /* Start NRO task. */ + this->nro_task.execute(NRO_TMP_PATH, std::string(this->json_data.download_url), true); + + /* Go to the next stage. */ + this->nextStage(); + } + OptionsTab::OptionsTab(nxdt::tasks::StatusInfoTask *status_info_task) : brls::List(), status_info_task(status_info_task) { /* Set custom spacing. */ @@ -271,17 +461,15 @@ namespace nxdt::views this->DisplayNotification("options_tab/notifications/no_internet_connection"_i18n); return; } else - if (false) /// TODO: add a proper check here + if (utilsGetApplicationUpdatedState()) { /* Display a notification if the application has already been updated. */ this->DisplayNotification("options_tab/notifications/already_updated"_i18n); return; } - /*brls::StagedAppletFrame *staged_frame = new brls::StagedAppletFrame(); - staged_frame->setTitle("options_tab/update_app/label"_i18n); - - brls::Application::pushView(staged_frame);*/ + /* Display update frame. */ + brls::Application::pushView(new OptionsTabUpdateApplicationFrame(), brls::ViewAnimation::FADE, false); }); this->addView(update_app); diff --git a/source/root_view.cpp b/source/root_view.cpp index 8881539..205ac29 100644 --- a/source/root_view.cpp +++ b/source/root_view.cpp @@ -119,9 +119,6 @@ namespace nxdt::views /* Subscribe to status info event. */ this->status_info_task_sub = this->status_info_task->RegisterListener([this](const nxdt::tasks::StatusInfoData *status_info_data) { - bool is_am = true; - struct tm *timeinfo = status_info_data->timeinfo; - u32 charge_percentage = status_info_data->charge_percentage; PsmChargerType charger_type = status_info_data->charger_type; @@ -129,25 +126,7 @@ namespace nxdt::views char *ip_addr = status_info_data->ip_addr; /* Update time label. */ - timeinfo->tm_mon++; - timeinfo->tm_year += 1900; - - if ("root_view/time_format"_i18n.compare("12") == 0) - { - /* Adjust time for 12-hour clock. */ - if (timeinfo->tm_hour > 12) - { - timeinfo->tm_hour -= 12; - is_am = false; - } else - if (!timeinfo->tm_hour) - { - timeinfo->tm_hour = 12; - } - } - - this->time_lbl->setText(i18n::getStr("root_view/date"_i18n, timeinfo->tm_year, timeinfo->tm_mon, timeinfo->tm_mday, timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec, \ - is_am ? "AM" : "PM")); + this->time_lbl->setText(this->GetFormattedDateString(status_info_data->timeinfo)); /* Update battery labels. */ this->battery_icon->setText(charger_type != PsmChargerType_Unconnected ? "\uE1A3" : (charge_percentage <= 15 ? "\uE19C" : "\uE1A4")); @@ -192,6 +171,32 @@ namespace nxdt::views delete this->usb_host_speed_lbl; } + std::string RootView::GetFormattedDateString(const struct tm& timeinfo) + { + bool is_am = true; + struct tm ts = timeinfo; + + /* Update time label. */ + ts.tm_mon++; + ts.tm_year += 1900; + + if ("generic/time_format"_i18n.compare("12") == 0) + { + /* Adjust time for 12-hour clock. */ + if (ts.tm_hour > 12) + { + ts.tm_hour -= 12; + is_am = false; + } else + if (!ts.tm_hour) + { + ts.tm_hour = 12; + } + } + + return i18n::getStr("generic/date"_i18n, ts.tm_year, ts.tm_mon, ts.tm_mday, ts.tm_hour, ts.tm_min, ts.tm_sec, is_am ? "AM" : "PM"); + } + void RootView::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, brls::Style* style, brls::FrameContext* ctx) { brls::AppletFrame::draw(vg, x, y, width, height, style, ctx); diff --git a/source/tasks.cpp b/source/tasks.cpp index 69a9977..49d3330 100644 --- a/source/tasks.cpp +++ b/source/tasks.cpp @@ -55,7 +55,7 @@ namespace nxdt::tasks /* Get current time. */ time_t unix_time = time(NULL); - status_info_data->timeinfo = localtime(&unix_time); + localtime_r(&unix_time, &(status_info_data->timeinfo)); /* Get battery stats. */ psmGetBatteryChargePercentage(&(status_info_data->charge_percentage)); diff --git a/todo.txt b/todo.txt index 2d3f41d..b75fb00 100644 --- a/todo.txt +++ b/todo.txt @@ -23,10 +23,10 @@ todo: usb: improve abi (make it rest-like?) usb: improve cancel mechanism - others: move curl (de)initialization to http.c - others: use hardcoded directories, move data to hardcoded directory if the launch path isn't the right one + others: fix shrinking bar + others: add version / commit hash check before updating app + others: check todo with grep others: dump verification via nswdb / no-intro - others: update application feature others: fatfs browser for emmc partitions reminder: