1
0
Fork 0
mirror of https://github.com/DarkMatterCore/nxdumptool.git synced 2024-12-22 16:42:11 +00:00

utils: implement utilsIsApplicationUpdatable().

Also removed legacy code that has already been reimplemented.
This commit is contained in:
Pablo Curiel 2021-08-07 05:44:36 -04:00
parent 28cd0ce10f
commit ba0c5d9e35
7 changed files with 97 additions and 571 deletions

View file

@ -151,10 +151,20 @@ 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);
/// Returns the current application updated state.
bool utilsGetApplicationUpdatedState(void);
/// Sets the application updated state to true, which makes utilsCloseResources() replace the application NRO.
void utilsSetApplicationUpdatedState(void);
/// 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);
/// Parses the provided version string and compares it to the application version. Returns true if the application can be updated.
/// If both versions are equal, the provided commit hash is compared to our commit hash - if they're different, true will be returned.
bool utilsIsApplicationUpdatable(const char *version, const char *commit_hash);
/// Frees previously allocated data from a UtilsGitHubReleaseJsonData element.
NX_INLINE void utilsFreeGitHubReleaseJsonData(UtilsGitHubReleaseJsonData *data)
{
@ -163,13 +173,6 @@ NX_INLINE void utilsFreeGitHubReleaseJsonData(UtilsGitHubReleaseJsonData *data)
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)
{

View file

@ -1840,119 +1840,6 @@ void gameCardDumpNSWDBCheck(u32 crc)
if (!found) uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "No match found in NSWDB.COM XML database! This could either be a bad dump or an undumped gamecard.");
}
static Result networkInit()
{
if (initNet) return 0;
Result result = socketInitializeDefault();
if (R_SUCCEEDED(result))
{
curl_global_init(CURL_GLOBAL_ALL);
initNet = true;
}
return result;
}
static void networkExit()
{
if (!initNet) return;
curl_global_cleanup();
socketExit();
initNet = false;
}
static size_t writeCurlFile(char *buffer, size_t size, size_t number_of_items, void *input_stream)
{
size_t total_size = (size * number_of_items);
if (fwrite(buffer, 1, total_size, input_stream) != total_size) return 0;
return total_size;
}
static size_t writeCurlBuffer(char *buffer, size_t size, size_t number_of_items, void *input_stream)
{
(void) input_stream;
const size_t bsz = (size * number_of_items);
if (result_sz == 0 || !result_buf)
{
result_sz = 0x1000;
result_buf = malloc(result_sz);
if (!result_buf) return 0;
}
bool need_realloc = false;
while((result_written + bsz) > result_sz)
{
result_sz <<= 1;
need_realloc = true;
}
if (need_realloc)
{
char *new_buf = realloc(result_buf, result_sz);
if (!new_buf) return 0;
result_buf = new_buf;
}
memcpy(result_buf + result_written, buffer, bsz);
result_written += bsz;
return bsz;
}
static bool performCurlRequest(CURL *curl, const char *url, FILE *filePtr, bool forceHttps, bool verbose)
{
if (!curl || !url || !strlen(url))
{
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to perform CURL request!", __func__);
return false;
}
curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 102400L);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_USERAGENT, HTTP_USER_AGENT);
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L);
curl_easy_setopt(curl, CURLOPT_NOBODY, 0L);
curl_easy_setopt(curl, CURLOPT_HEADER, 0L);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 50L);
if (forceHttps) curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS);
if (filePtr)
{
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCurlFile);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, filePtr);
} else {
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCurlBuffer);
}
CURLcode res;
long http_code = 0;
double size = 0.0;
bool success = false;
res = curl_easy_perform(curl);
result_sz = result_written = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
curl_easy_getinfo(curl, CURLINFO_SIZE_DOWNLOAD, &size);
if (res == CURLE_OK && http_code >= 200 && http_code <= 299 && size > 0)
{
if (verbose) uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Successfully downloaded %.0lf bytes!", size);
success = true;
} else {
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: CURL request failed for \"%s\" endpoint!\nHTTP status code: %ld", __func__, url, http_code);
}
return success;
}
void noIntroDumpCheck(bool isDigital, u32 crc)
{
Result result;
@ -2004,436 +1891,3 @@ out:
if (R_SUCCEEDED(result)) networkExit();
}
void updateNSWDBXml()
{
Result result;
CURL *curl = NULL;
bool success = false;
FILE *nswdbXml = NULL;
result = networkInit();
if (R_FAILED(result))
{
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to initialize socket! (%08X)", __func__, result);
goto out;
}
char xmlPath[256] = {'\0'};
snprintf(xmlPath, MAX_CHARACTERS(xmlPath), "%s.tmp", NSWDB_XML_PATH);
curl = curl_easy_init();
if (!curl)
{
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to initialize CURL context!", __func__);
goto out;
}
nswdbXml = fopen(xmlPath, "wb");
if (!nswdbXml)
{
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open \"%s\" in write mode!", __func__, NSWDB_XML_URL);
goto out;
}
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Downloading XML database from \"%s\", please wait...", NSWDB_XML_URL);
breaks++;
appletModeOperationWarning();
uiRefreshDisplay();
breaks++;
changeHomeButtonBlockStatus(true);
success = performCurlRequest(curl, NSWDB_XML_URL, nswdbXml, false, true);
changeHomeButtonBlockStatus(false);
out:
if (nswdbXml) fclose(nswdbXml);
if (success)
{
remove(NSWDB_XML_PATH);
rename(xmlPath, NSWDB_XML_PATH);
} else {
remove(xmlPath);
}
if (curl) curl_easy_cleanup(curl);
if (R_SUCCEEDED(result)) networkExit();
breaks += 2;
}
static int versionNumCmp(char *ver1, char *ver2)
{
int i, curPart, res;
char *token = NULL;
// Define a struct for comparison purposes
typedef struct {
int major;
int minor;
int build;
} version_t;
version_t versionNum1, versionNum2;
memset(&versionNum1, 0, sizeof(version_t));
memset(&versionNum2, 0, sizeof(version_t));
// Create copies of the version strings to avoid modifications by strtok()
char ver1tok[64] = {'\0'};
snprintf(ver1tok, 63, ver1);
char ver2tok[64] = {'\0'};
snprintf(ver2tok, 63, ver2);
// Parse version string 1
i = 0;
token = strtok(ver1tok, ".");
while(token != NULL && i < 3)
{
curPart = atoi(token);
switch(i)
{
case 0:
versionNum1.major = curPart;
break;
case 1:
versionNum1.minor = curPart;
break;
case 2:
versionNum1.build = curPart;
break;
default:
break;
}
token = strtok(NULL, ".");
i++;
}
// Parse version string 2
i = 0;
token = strtok(ver2tok, ".");
while(token != NULL && i < 3)
{
curPart = atoi(token);
switch(i)
{
case 0:
versionNum2.major = curPart;
break;
case 1:
versionNum2.minor = curPart;
break;
case 2:
versionNum2.build = curPart;
break;
default:
break;
}
token = strtok(NULL, ".");
i++;
}
// Compare version_t structs
if (versionNum1.major == versionNum2.major)
{
if (versionNum1.minor == versionNum2.minor)
{
if (versionNum1.build == versionNum2.build)
{
res = 0;
} else
if (versionNum1.build < versionNum2.build)
{
res = -1;
} else {
res = 1;
}
} else
if (versionNum1.minor < versionNum2.minor)
{
res = -1;
} else {
res = 1;
}
} else
if (versionNum1.major < versionNum2.major)
{
res = -1;
} else {
res = 1;
}
return res;
}
static struct json_object *retrieveJsonObjMemberByNameAndType(struct json_object *jobj, char *memberName, json_type memberType)
{
if (!jobj || !memberName || !strlen(memberName))
{
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to retrieve member by name and type from JSON object!", __func__);
return NULL;
}
struct json_object *memberObj = NULL;
json_type memberObjType;
if (!json_object_object_get_ex(jobj, memberName, &memberObj))
{
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to retrieve member \"%s\" from JSON object!", __func__, memberName);
return NULL;
}
memberObjType = json_object_get_type(memberObj);
if (memberObjType != memberType)
{
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid type for member \"%s\" in JSON object! (got \"%s\", expected \"%s\")", __func__, memberName, json_type_to_name(memberObjType), json_type_to_name(memberType));
return NULL;
}
return memberObj;
}
static const char *retrieveJsonObjStrMemberContentsByName(struct json_object *jobj, char *memberName)
{
if (!jobj || !memberName || !strlen(memberName))
{
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to retrieve string member contents by name from JSON object!", __func__);
return NULL;
}
struct json_object *memberObj = retrieveJsonObjMemberByNameAndType(jobj, memberName, json_type_string);
if (!memberObj) return NULL;
const char *memberObjStr = json_object_get_string(memberObj);
if (!memberObjStr || !strlen(memberObjStr))
{
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: string member \"%s\" from JSON object is empty!", __func__, memberName);
return NULL;
}
return memberObjStr;
}
static struct json_object *retrieveJsonObjArrayMemberByName(struct json_object *jobj, char *memberName, size_t *outputArrayLength)
{
if (!jobj || !memberName || !strlen(memberName) || !outputArrayLength)
{
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to retrieve array member by name from JSON object!", __func__);
return NULL;
}
struct json_object *memberObj = retrieveJsonObjMemberByNameAndType(jobj, memberName, json_type_array);
if (memberObj) *outputArrayLength = json_object_array_length(memberObj);
return memberObj;
}
static struct json_object *retrieveJsonObjArrayElementByIndex(struct json_object *jobj, size_t idx)
{
if (!jobj || json_object_get_type(jobj) != json_type_array)
{
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: invalid parameters to retrieve element by index from JSON array object!", __func__);
return NULL;
}
struct json_object *memberObjArrayElement = json_object_array_get_idx(jobj, idx);
if (!memberObjArrayElement) uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to retrieve element at index %lu from JSON array object!", __func__, idx);
return memberObjArrayElement;
}
bool updateApplication()
{
if (envIsNso())
{
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to update application. Not running as a NRO.", __func__);
breaks += 2;
return false;
}
Result result;
CURL *curl = NULL;
FILE *nxDumpToolNro = NULL;
char releaseTag[32] = {'\0'};
bool success = false;
size_t i, assetsCnt = 0;
struct json_object *jobj = NULL, *assets = NULL;
const char *releaseNameObjStr = NULL, *dlUrlObjStr = NULL;
char nroPath[NAME_BUF_LEN] = {'\0'};
snprintf(nroPath, MAX_CHARACTERS(nroPath), "%s.tmp", (appLaunchPath ? appLaunchPath : NRO_PATH));
result = networkInit();
if (R_FAILED(result))
{
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to initialize socket! (%08X)", __func__, result);
goto out;
}
curl = curl_easy_init();
if (!curl)
{
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to initialize CURL context!", __func__);
goto out;
}
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Requesting latest release information from \"%s\"...", GITHUB_API_URL);
breaks++;
uiRefreshDisplay();
if (!performCurlRequest(curl, GITHUB_API_URL, NULL, true, false)) goto out;
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Parsing response JSON data...");
breaks++;
uiRefreshDisplay();
jobj = json_tokener_parse(result_buf);
if (!jobj)
{
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to parse JSON response!", __func__);
goto out;
}
releaseNameObjStr = retrieveJsonObjStrMemberContentsByName(jobj, GITHUB_API_JSON_RELEASE_NAME);
if (!releaseNameObjStr) goto out;
snprintf(releaseTag, MAX_CHARACTERS(releaseTag), releaseNameObjStr);
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Latest release: %s.", releaseTag);
breaks++;
uiRefreshDisplay();
// Remove the first character from the release name if it's v/V/r/R
if (releaseTag[0] == 'v' || releaseTag[0] == 'V' || releaseTag[0] == 'r' || releaseTag[0] == 'R')
{
u32 releaseTagLen = strlen(releaseTag);
memmove(releaseTag, releaseTag + 1, releaseTagLen - 1);
releaseTag[releaseTagLen - 1] = '\0';
}
// Compare versions
if (versionNumCmp(releaseTag, APP_VERSION) <= 0)
{
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "You already have the latest version!");
breaks += 2;
// Ask the user if they want to perform a forced update
int cur_breaks = breaks;
if (yesNoPrompt("Do you want to perform a forced update?"))
{
// Remove the prompt from the screen
breaks = cur_breaks;
uiFill(0, STRING_Y_POS(breaks), FB_WIDTH, FB_HEIGHT - STRING_Y_POS(breaks), BG_COLOR_RGB);
uiRefreshDisplay();
} else {
breaks -= 2;
goto out;
}
}
assets = retrieveJsonObjArrayMemberByName(jobj, GITHUB_API_JSON_ASSETS, &assetsCnt);
if (!assets) goto out;
// Cycle through the assets to find the right download URL
for(i = 0; i < assetsCnt; i++)
{
struct json_object *assetElement = retrieveJsonObjArrayElementByIndex(assets, i);
if (!assetElement) break;
const char *assetName = retrieveJsonObjStrMemberContentsByName(assetElement, GITHUB_API_JSON_ASSETS_NAME);
if (!assetName) break;
if (!strncmp(assetName, NRO_NAME, strlen(assetName)))
{
// Found it
dlUrlObjStr = retrieveJsonObjStrMemberContentsByName(assetElement, GITHUB_API_JSON_ASSETS_DL_URL);
break;
}
}
if (!dlUrlObjStr)
{
breaks++;
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: unable to locate NRO download URL!", __func__);
goto out;
}
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Download URL: \"%s\".", dlUrlObjStr);
breaks++;
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_RGB, "Please wait...");
breaks++;
appletModeOperationWarning();
uiRefreshDisplay();
breaks++;
changeHomeButtonBlockStatus(true);
nxDumpToolNro = fopen(nroPath, "wb");
if (!nxDumpToolNro)
{
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_ERROR_RGB, "%s: failed to open \"%s\" in write mode!", __func__, nroPath);
goto out;
}
curl_easy_reset(curl);
success = performCurlRequest(curl, dlUrlObjStr, nxDumpToolNro, true, true);
if (!success) goto out;
breaks++;
uiDrawString(STRING_X_POS, STRING_Y_POS(breaks), FONT_COLOR_SUCCESS_RGB, "Please restart the application to reflect the changes.");
out:
if (nxDumpToolNro) fclose(nxDumpToolNro);
if (strlen(nroPath))
{
if (success)
{
snprintf(strbuf, MAX_CHARACTERS(strbuf), nroPath);
nroPath[strlen(nroPath) - 4] = '\0';
remove(nroPath);
rename(strbuf, nroPath);
} else {
remove(nroPath);
}
}
if (jobj) json_object_put(jobj);
if (result_buf)
{
free(result_buf);
result_buf = NULL;
}
if (curl) curl_easy_cleanup(curl);
if (R_SUCCEEDED(result)) networkExit();
breaks += 2;
changeHomeButtonBlockStatus(false);
return success;
}

View file

@ -290,8 +290,4 @@ void gameCardDumpNSWDBCheck(u32 crc);
void noIntroDumpCheck(bool isDigital, u32 crc);
void updateNSWDBXml();
bool updateApplication();
#endif

View file

@ -43,6 +43,7 @@
"is_nso": "The application is running as an NSO. Unable to update.",
"already_updated": "The application has already been updated. Please reload.",
"github_json_failed": "Failed to download or parse GitHub release JSON!",
"up_to_date": "The application is up to date!",
"app_updated": "Application successfully updated! Please reload for the changes to take effect."
}
}

View file

@ -35,6 +35,14 @@
/* Reference: https://docs.microsoft.com/en-us/windows/win32/fileio/filesystem-functionality-comparison#limits. */
#define NT_MAX_FILENAME_LENGTH 255
/* Type definitions. */
typedef struct {
u32 major;
u32 minor;
u32 micro;
} UtilsApplicationVersion;
/* Global variables. */
static bool g_resourcesInit = false;
@ -828,6 +836,18 @@ end:
return path;
}
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)
@ -906,18 +926,48 @@ end:
return ret;
}
bool utilsGetApplicationUpdatedState(void)
bool utilsIsApplicationUpdatable(const char *version, const char *commit_hash)
{
if (!version || !*version || *version != 'v' || !commit_hash || !*commit_hash)
{
LOG_MSG("Invalid parameters!");
return false;
}
bool ret = false;
SCOPED_LOCK(&g_resourcesMutex) ret = g_appUpdated;
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;
}
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;

View file

@ -213,6 +213,9 @@ namespace nxdt::views
/* Return immediately if the JSON task hasn't finished. */
if (!this->json_task.isFinished()) return;
bool pop_view = false;
std::string notification = "";
/* Retrieve task result. */
nxdt::tasks::DownloadDataResult json_task_result = this->json_task.get();
this->json_buf = json_task_result.first;
@ -221,16 +224,36 @@ namespace nxdt::views
/* Parse downloaded JSON object. */
if (utilsParseGitHubReleaseJsonData(this->json_buf, this->json_buf_size, &(this->json_data)))
{
/* Display changelog. */
this->DisplayChangelog();
/* Check if the application can be updated. */
if (utilsIsApplicationUpdatable(this->json_data.version, this->json_data.commit_hash))
{
/* Display changelog. */
this->DisplayChangelog();
} else {
/* Update flag. */
pop_view = true;
/* Set notification string. */
notification = "options_tab/notifications/up_to_date"_i18n;
}
} else {
/* Log downloaded data if we failed to parse it. */
/* Log downloaded data. */
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);
/* Update flag. */
pop_view = true;
/* Pop view */
/* Set notification string. */
notification = "options_tab/notifications/github_json_failed"_i18n;
}
/* Pop view (if needed). */
if (pop_view)
{
/* Display notification. */
brls::Application::notify(notification);
/* Pop view. */
this->onCancel();
}
});

View file

@ -24,7 +24,6 @@ todo:
usb: improve cancel mechanism
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: fatfs browser for emmc partitions