<repo_name>Epicpkmn11/MultiUpdater<file_sep>/source/config.c
#include <jansson.h>
#include "config.h"

#define ENTRIES_STRING "entries"

#define NAME_STRING "name"
#define URL_STRING "url"
#define PATH_STRING "path"
#define IN_ARCHIVE_STRING "inarchive"
#define IN_RELEASE_STRING "inrelease"

void parse_entries(json_t * entries_elem, config_t * todo_config)
{
	if (json_is_array(entries_elem)) {
		
		size_t entries_number = json_array_size(entries_elem);

		const char *key;
		json_t *value;
		todo_config->entries_number = entries_number;
		
		for (u16 i = 0; i < entries_number; i++) {
			if (json_is_object(json_array_get(entries_elem, i)))
			{
				json_object_foreach(json_array_get(entries_elem, i), key, value) {
					if(json_is_string(value)) {
						if(!strcmp(key, NAME_STRING))
						{
							todo_config->entries[i].name = strdup(json_string_value(value));
						}
						else if(!strcmp(key, URL_STRING))
						{
							todo_config->entries[i].url = strdup(json_string_value(value));
						}
						else if(!strcmp(key, PATH_STRING))
						{
							todo_config->entries[i].path = strdup(json_string_value(value));
						}
						else if(!strcmp(key, IN_ARCHIVE_STRING))
						{
							todo_config->entries[i].in_archive = strdup(json_string_value(value));
						}
						else if(!strcmp(key, IN_RELEASE_STRING))
						{
							todo_config->entries[i].in_release = strdup(json_string_value(value));
						}
					}
				}
			}
			else
			{
				todo_config->errorState = ERROR_JSON;
			}
		}
	}
	else
	{
		todo_config->errorState = ERROR_JSON;
	}
}

json_t * load_json(const char * text) {
	json_t *root;
	json_error_t error;
	
	root = json_loads(text, 0, &error);
	
	if (root) {
		return root;
	} else {
		return (json_t *)0;
	}
}

void get_config(const char * filepath, config_t * parsed_config)
{
	
	parsed_config->entries_number = 1;
	parsed_config->errorState = 0;
	
	FILE * fptr = fopen(filepath, "r");
	if (fptr == NULL) {
		parsed_config->errorState = ERROR_FILE;
		return;
	}
	
	fseek(fptr, 0, SEEK_END);
	u32 size = ftell(fptr);
	char * config_contents = malloc(size + 1);
	rewind(fptr);
	fread(config_contents, size, 1, fptr);
	fclose(fptr);
	
	json_t *root = load_json(config_contents);
	if (root == 0) {
		parsed_config->errorState = ERROR_JSON;
	}
	
	const char *key;
	json_t *value;
	
	if (json_is_object(root)) {
		json_object_foreach(root, key, value) {
			if (!strcmp(key, ENTRIES_STRING))
			{
				parse_entries(value, parsed_config);
			}
		}
	}
	else
	{
		parsed_config->errorState = ERROR_JSON;
	}
	
	free(config_contents);
	json_decref(root);
}

void clean_config(config_t * parsed_config)
{
	for (int i = 0; i < parsed_config->entries_number; i++) {
		free((char*)(parsed_config->entries[i].name));
		free((char*)(parsed_config->entries[i].url));
		free((char*)(parsed_config->entries[i].path));
		free((char*)(parsed_config->entries[i].in_archive));
		free((char*)(parsed_config->entries[i].in_release));
	}
}
<file_sep>/source/params.h
#pragma once

#include "basic.h"
#include "config.h"

bool handleParams(entry_t * received_entry);
<file_sep>/source/download.h
#pragma once

#include "basic.h"

#define DL_ERROR_STATUSCODE -1
#define DL_ERROR_WRITEFILE -2
#define DL_ERROR_ALLOC -3
#define DL_ERROR_CONFIG -4
#define DL_ERROR_GIT -5

Result setupContext(httpcContext * context, const char * url, u32 * size, bool gitapi);
Result downloadToFile(const char * url, const char * filepath, bool gitapi);
Result downloadFromRelease(const char * url, const char * element, const char * filepath);
<file_sep>/source/download.c
#include "download.h"
#include "gitapi.h"
#include "file.h"

#include "certs/cybertrust.h"
#include "certs/digicert.h"

Result setupContext(httpcContext * context, const char * url, u32 * size, bool gitapi)
{
	Result ret = 0;
	u32 statuscode = 0;
	
	ret = httpcOpenContext(context, HTTPC_METHOD_GET, url, 1);
	if (ret != 0) {
		printf_log("Error in:\nhttpcOpenContext\n");
		httpcCloseContext(context);
		return ret;
	}
	
	ret = httpcAddRequestHeaderField(context, "User-Agent", "MultiUpdater");
	if (ret != 0) {
		printf_log("Error in:\nhttpcAddRequestHeaderField\n");
		httpcCloseContext(context);
		return ret;
	}
	
	if (gitapi) {
		ret = httpcAddTrustedRootCA(context, cybertrust_cer, cybertrust_cer_len);
		if (ret != 0) {
			printf_log("Error in:\nhttpcAddRequestHeaderField\n");
			httpcCloseContext(context);
			return ret;
		}
		
		ret = httpcAddTrustedRootCA(context, digicert_cer, digicert_cer_len);
		if (ret != 0) {
			printf_log("Error in:\nhttpcAddRequestHeaderField\n");
			httpcCloseContext(context);
			return ret;
		}
	}
	else {
		ret = httpcSetSSLOpt(context, SSLCOPT_DisableVerify);
		if (ret != 0) {
			printf_log("Error in:\nhttpcSetSSLOpt\n");
			httpcCloseContext(context);
			return ret;
		}
	}
	
	ret = httpcAddRequestHeaderField(context, "Connection", "Keep-Alive");
	if (ret != 0) {
		printf_log("Error in:\nhttpcAddRequestHeaderField\n");
		httpcCloseContext(context);
		return ret;
	}
	
	ret = httpcBeginRequest(context);
	if (ret != 0) {
		printf_log("Error in:\nhttpcBeginRequest\n");
		httpcCloseContext(context);
		return ret;
	}
	
	ret = httpcGetResponseStatusCode(context, &statuscode);
	if (ret != 0) {
		printf_log("Error in:\nhttpcGetResponseStatusCode\n");
		httpcCloseContext(context);
		return ret;
	}
	
	if ((statuscode >= 301 && statuscode <= 303) || (statuscode >= 307 && statuscode <= 308)) {
		char * newurl = malloc(0x1000); // One 4K page for new URL
		if (newurl == NULL) {
			httpcCloseContext(context);
			return DL_ERROR_ALLOC;
		}
		
		ret = httpcGetResponseHeader(context, "Location", newurl, 0x1000);
		if (ret != 0) {
			printf_log("Error in:\nhttpcGetResponseHeader\n");
			httpcCloseContext(context);
			free(newurl);
			return ret;
		}
		
		httpcCloseContext(context); // Close this context before we try the next
		
		if (newurl[0] == '/') { //if the url starts with a slash, it's local
			int slashpos = 0;
			char * domainname = strdup(url);
			if (strncmp("http", domainname, 4) == 0) slashpos = 8; //if the url in the entry starts with http:// or https:// we need to skip that
			slashpos = strchr(domainname+slashpos, '/')-domainname;
			domainname[slashpos] = '\0'; // replace the slash with a nullbyte to cut the url
			char * copyurl = strdup(newurl);
			sprintf(newurl, "%s%s", domainname, copyurl);
			free(copyurl);
			free(domainname);
		}
		
		if (gitapi) printf_log("Redirecting...\n");
		else printf_log("Redirecting to url:\n%s\n", newurl);
		
		ret = setupContext(context, newurl, size, gitapi);
		free(newurl);
		return ret;
	}
	
	if (statuscode != 200) {
		printf_log("Error: HTTP status code is not 200 OK.\nStatus code: %lu\n", statuscode);
		httpcCloseContext(context);
		return DL_ERROR_STATUSCODE;
	}
	
	ret = httpcGetDownloadSizeState(context, NULL, size);
	if (ret != 0) {
		printf_log("Error in:\nhttpcGetDownloadSizeState\n");
		httpcCloseContext(context);
		return ret;
	}
	
	return 0;
}

Result downloadToFile(const char * url, const char * filepath, bool gitapi)
{
	if (url == NULL) {
		printf_log("Download cannot start, the URL in config.json is blank.\n");
		return DL_ERROR_CONFIG;
	}
	
	if (filepath == NULL) {
		printf_log("Download cannot start, file path in config.json is blank.\n");
		return DL_ERROR_CONFIG;
	}
	
	printf_log("Downloading file from:\n%s\nto:\n%s\n", url, filepath);
	
	httpcContext context;
	Result ret = 0;
	u32 contentsize = 0, readsize = 0;
	
	ret = setupContext(&context, url, &contentsize, gitapi);
	if (ret != 0) return ret;
	
	printf_log("Downloading %lu bytes...\n", contentsize);
	
	Handle filehandle;
	u64 offset = 0;
	u32 bytesWritten = 0;
	
	ret = openFile(filepath, &filehandle, true);
	if (ret != 0) {
		printf_log("Error: couldn't open file to write.\n");
		httpcCloseContext(&context);
		return DL_ERROR_WRITEFILE;
	}
	
	u8 * buf = malloc(0x1000);
	if (buf == NULL) {
		httpcCloseContext(&context);
		return DL_ERROR_ALLOC;
	}
	
	u64 startTime = osGetTime();
	do {
		ret = httpcDownloadData(&context, buf, 0x1000, &readsize);
		writeFile(filehandle, &bytesWritten, offset, buf, readsize);
		offset += readsize;
	} while (ret == (Result)HTTPC_RESULTCODE_DOWNLOADPENDING);
	printf_log("Download took %llu milliseconds.\n", osGetTime()-startTime);
	
	free(buf);
	closeFile(filehandle);
	httpcCloseContext(&context);
	
	if (ret != 0) {
		printf_log("Error in:\nhttpcDownloadData\n");
		return ret;
	}
	
	return 0;
}

Result downloadFromRelease(const char * url, const char * element, const char * filepath)
{
	if (url == NULL) {
		printf_log("Download cannot start, the URL in config.json is blank.\n");
		return DL_ERROR_CONFIG;
	}
	
	if (element == NULL) {
		printf_log("Download cannot start, inrelease in config.json is blank.\n");
		return DL_ERROR_CONFIG;
	}
	
	if (filepath == NULL) {
		printf_log("Download cannot start, file path in config.json is blank.\n");
		return DL_ERROR_CONFIG;
	}
	
	char * copyurl = strdup(url);
	char * repo_name = copyurl;
	char * repo_owner = copyurl;
	
	unsigned int startpos = 0;
	if (strncmp("http", copyurl, 4) == 0) startpos = 8; //if the url in the entry starts with http:// or https:// we need to skip that
	for (unsigned int i = startpos; copyurl[i]; i++) {
		if (copyurl[i] == '/') {
			copyurl[i] = '\0';
			if (repo_owner == copyurl) repo_owner += i+1;
			else if (repo_name == copyurl) repo_name += i+1;
			else break;
		}
	}
	
	char * apiurl = NULL;
	asprintf(&apiurl, "https://api.github.com/repos/%s/%s/releases/latest", repo_owner, repo_name);
	printf_log("Downloading latest release from repo:\n%s\nby:\n%s\n", repo_name, repo_owner);
	printf_log("Crafted api url:\n%s\n", apiurl);
	free(copyurl);
	
	httpcContext context;
	Result ret = 0;
	u32 contentsize = 0, readsize = 0;
	
	ret = setupContext(&context, apiurl, &contentsize, true);
	free(apiurl);
	if (ret != 0) return ret;
	
	char * buf = malloc(contentsize+1);
	if (buf == NULL) {
		httpcCloseContext(&context);
		return DL_ERROR_ALLOC;
	}
	buf[contentsize] = 0; //nullbyte to end it as a proper C style string
	
	do {
		ret = httpcDownloadData(&context, (u8 *)buf, contentsize, &readsize);
	} while (ret == (Result)HTTPC_RESULTCODE_DOWNLOADPENDING);
	
	httpcCloseContext(&context);
	if (ret != 0) {
		printf_log("Error in:\nhttpcDownloadData\n");
		free(buf);
		return ret;
	}
	
	char * asseturl = NULL;
	printf_log("\x1b[40;34mSearching for asset %s in api response...\x1b[0m\n", element);
	getAssetUrl((const char *)buf, element, &asseturl);
	free(buf);
	
	if (asseturl == NULL)
		ret = DL_ERROR_GIT;
	else {
		ret = downloadToFile(asseturl, filepath, true);
		free(asseturl);
	}
	
	return ret;
}
<file_sep>/source/gitapi.c
#include "gitapi.h"
#include "stringutils.h"

char * findTagValue(const char * apiresponse, const char * tagname)
{
	char * endstring = "\"";
	char *tagstart, *tagend, *retstr = NULL;
	
	if ((tagstart = strstr(apiresponse, tagname)) != NULL) {
		if ((tagend = strstr(tagstart+strlen(tagname), endstring)) != NULL) {
			tagstart += strlen(tagname);
			int len = tagend-tagstart;
			char * tempstr = calloc(len+1, sizeof(char));
			strncpy(tempstr, tagstart, len);
			retstr = strdup(tempstr);
			free(tempstr);
		}
	}
	
	return retstr;
}

void getAssetUrl(const char * apiresponse, const char * element, char ** asseturl)
{
	char * assets_tagname = "\"assets\":";
	char * name_tagname = "\"name\":\"";
	char * url_tagname = "\"browser_download_url\":\"";
	int offset = strstr(apiresponse, assets_tagname)-apiresponse;
	
	char * foundpos = NULL;
	while ((foundpos = strstr(apiresponse+offset, name_tagname)) != NULL) {
		offset = (int)(foundpos+strlen(name_tagname)-apiresponse);
		char * name = findTagValue(foundpos, name_tagname);
		if (!matchPattern(element, name)) {
			printf_log("Found asset with name matching %s\n", element);
			printf_log("Finding asset url...\n");
			*asseturl = findTagValue(foundpos, url_tagname);
			free(name);
			return;
		}
		free(name);
	}
	
	printf_log("No asset with name matching %s found.\n", element);
}<file_sep>/selfupdater/selfupdater.h
/*
Those 2 files (selfupdater.c and selfupdater.h) are in the public domain.
You can use them in whatever your project is, without caring about the license
*/

#pragma once

void update(const char * name, const char * url, const char * path, const char * in_archive, const char * in_release);
<file_sep>/source/main.c
#include "download.h"
#include "draw.h"
#include "file.h"
#include "cia.h"
#include "stringutils.h"
#include "params.h"

u8 update(entry_t entry)
{
	Result ret = 0;
	printf_log("\x1b[40;34mBeginning update...\x1b[0m\n");
	
	char dl_path[256] = {0};
	
	//if the file to download isnt an archive, direcly download where wanted
	if (entry.in_archive == NULL)
		strcpy(dl_path, entry.path);
	//otherwise, download to an archive in the working dir, then extract where wanted
	else {
		sprintf(dl_path, "%s%s.archive", WORKING_DIR, entry.name);
		cleanPath(dl_path);
	}
	
	//if the entry doesnt want anything from a release, expect it to be a normal file
	if (entry.in_release == NULL)
		ret = downloadToFile(entry.url, dl_path, false);
	else
		ret = downloadFromRelease(entry.url, entry.in_release, dl_path);
	
	if (ret != 0) {
		printf_log("\x1b[40;31mDownload failed!");
		goto failure;
	}
	else printf_log("\x1b[40;32mDownload successful!");
	
	if (entry.in_archive != NULL) {
		printf_log("\n\x1b[40;34mExtracting file from the archive...\x1b[0m\n");
		ret = extractFileFromArchive(dl_path, entry.in_archive, entry.path);
		if (ret != 0) {
			printf_log("\x1b[40;31mExtraction failed!");
			goto failure;
		}
		else printf_log("\x1b[40;32mExtraction successful!");
	}
	
	//if the extracted/downloaded file ends with ".cia", try to install it
	if (strncmp(entry.path+strlen(entry.path)-4, ".cia", 4) == 0) {
		printf_log("\n\x1b[40;34mInstalling CIA...\x1b[0m\n");
		ret = installCia(entry.path);
		if (ret != 0) {
			printf_log("\x1b[40;31mInstall failed!");
			goto failure;
		}
		else
			printf_log("\x1b[40;32mInstall successful!");
	}
	
	printf_log("\n\x1b[40;32mUpdate complete!");
	return UPDATE_DONE;
	
	failure:
	printf_log("\nError: 0x%08x", (unsigned int)ret);
	return UPDATE_ERROR;
}

int main()
{
	gfxInitDefault();
	httpcInit(0);
	amInit();
	fsInit();
	AM_InitializeExternalTitleDatabase(false);
	
	PrintConsole topScreen, bottomScreen;
	consoleInit(GFX_TOP, &topScreen);
	consoleInit(GFX_BOTTOM, &bottomScreen);
	
	asprintf(&log_file_path, "%s%s", WORKING_DIR, LOGFILE_NAME);
	Handle fileHandle;
	openFile(log_file_path, &fileHandle, true);
	closeFile(fileHandle);
	
	consoleSelect(&topScreen);
	printf_log("\x1b[2;2HMultiUpdater %s by LiquidFenrir", VERSION_STRING);
	printf("\x1b[27;2HPress SELECT to show instructions.");
	printf("\x1b[28;2HPress START to exit.\x1b[0;0H");
	
	config_t parsed_config;
	if (handleParams(&parsed_config.entries[0])) {
		consoleSelect(&bottomScreen);
		printf_log("\x1b[40;33mUpdating requested application:\n%s\n\x1b[0m", parsed_config.entries[0].name);
		update(parsed_config.entries[0]);
		printf_log("\n\x1b[40;33mClosing in 10 seconds.\nTake note of error codes if needed...\n\x1b[0m");
		
		gfxFlushBuffers();
		gfxSwapBuffers();
		gspWaitForVBlank();
		
		svcSleepThread(10e9);
		parsed_config.entries_number = 1;
		goto exit;
	}

	char * filepath = NULL;
	asprintf(&filepath, "%sconfig.json", WORKING_DIR);

	get_config(filepath, &parsed_config);

	if (parsed_config.errorState == 0) {
		
		int selected_entry = 0;
		u8 state[256] = {0};
		
		while (aptMainLoop()) {

			consoleSelect(&topScreen);
			drawMenu(&parsed_config, state, selected_entry);
			consoleSelect(&bottomScreen);
			
			hidScanInput();
			
			if (hidKeysDown() & KEY_START) {
				exit:
				//strings copied with strdup need to be freed
				clean_config(&parsed_config);
				break;
			}
			else if (hidKeysDown() & KEY_SELECT) {
				drawInstructions();
			}
			else if (hidKeysDown() & KEY_DOWN) {
				selected_entry++;
				if (selected_entry >= parsed_config.entries_number)
					selected_entry = parsed_config.entries_number-1;
			}
			else if (hidKeysDown() & KEY_UP) {
				selected_entry--;
				if (selected_entry < 0)
					selected_entry = 0;
			}
			else if (hidKeysDown() & KEY_RIGHT) { //go to bottom of the menu
				selected_entry = parsed_config.entries_number-1;
			}
			else if (hidKeysDown() & KEY_LEFT) { //go to top of the menu
				selected_entry = 0;
			}
			else if (hidKeysDown() & KEY_L) { //mark all entries
				for (u8 i = 0; i < parsed_config.entries_number; i++) {
					state[i] |= STATE_MARKED;
				}
			}
			else if (hidKeysDown() & KEY_R) { //unmark all entries
				for (u8 i = 0; i < parsed_config.entries_number; i++) {
					state[i] &= ~STATE_MARKED;
				}
			}
			else if (hidKeysDown() & KEY_Y) { //mark/unmark selected entry
				state[selected_entry] ^= STATE_MARKED;
			}
			else if (hidKeysDown() & KEY_A) { //update all marked entries and currently selected entry (even if it's not marked)
				int selected_entry_bak = selected_entry;
				for (int i = 0; i < parsed_config.entries_number; i++) {
					if (i == selected_entry || state[i] & STATE_MARKED) {
						consoleSelect(&topScreen);
						drawMenu(&parsed_config, state, i);
						consoleSelect(&bottomScreen);
						
						u8 ret = update(parsed_config.entries[i]);
						printf_log("\x1b[0m\n");
						state[i] |= ret;
						state[i] &= ~STATE_MARKED;
						
						gfxFlushBuffers();
						gfxSwapBuffers();
						gspWaitForVBlank();
					}
				}
				selected_entry = selected_entry_bak;
			}
			else if (hidKeysDown() & KEY_B) { //backup all marked entries and currently selected entry (even if it's not marked)
				int selected_entry_bak = selected_entry;
				for (int i = 0; i < parsed_config.entries_number; i++) {
					if (i == selected_entry || state[i] & STATE_MARKED) {
						consoleSelect(&topScreen);
						drawMenu(&parsed_config, state, i);
						consoleSelect(&bottomScreen);
						
						char * backuppath = NULL;
						asprintf(&backuppath, "%s%s.bak", WORKING_DIR, parsed_config.entries[i].name);
						cleanPath(backuppath);
						printf_log("\x1b[40;32mBacking up %s...\x1b[0m\n", parsed_config.entries[i].name);
						
						Result ret = copyFile(parsed_config.entries[i].path, backuppath);
						if (ret != 0) {
							printf_log("\x1b[40;31mBackup failed...");
						}
						else {
							printf_log("\x1b[40;32mBackup complete!");
						}
						printf_log("\x1b[0m\n");
						
						free(backuppath);
						
						gfxFlushBuffers();
						gfxSwapBuffers();
						gspWaitForVBlank();
					}
				}
				selected_entry = selected_entry_bak;
			}
			else if (hidKeysDown() & KEY_X) { //restore the backups of all marked entries and currently selected entry (even if it's not marked)
				int selected_entry_bak = selected_entry;
				for (int i = 0; i < parsed_config.entries_number; i++) {
					if (i == selected_entry || state[i] & STATE_MARKED) {
						consoleSelect(&topScreen);
						drawMenu(&parsed_config, state, i);
						consoleSelect(&bottomScreen);
						
						char * backuppath = NULL;
						asprintf(&backuppath, "%s%s.bak", WORKING_DIR, parsed_config.entries[i].name);
						cleanPath(backuppath);
						printf_log("\x1b[40;Restoring %s...\x1b[0m\n", parsed_config.entries[i].name);
						
						Result ret = copyFile(backuppath, parsed_config.entries[i].path);
						if (ret != 0) {
							printf_log("\x1b[40;31mRestore failed...");
						}
						else {
							printf_log("\x1b[40;32mRestore complete!");
							remove(backuppath);
						}
						printf_log("\x1b[0m\n");
						
						free(backuppath);
						
						gfxFlushBuffers();
						gfxSwapBuffers();
						gspWaitForVBlank();
					}
				}
				selected_entry = selected_entry_bak;
			}
			
			gfxFlushBuffers();
			gfxSwapBuffers();
			gspWaitForVBlank();
		}
	}
	else if (parsed_config.errorState == ERROR_FILE) {
		
		printf_log("\x1b[40;31m\x1b[13;2HError: config file not found.\x1b[0m");
		printf("\x1b[14;2HPress A to download the example config.json");
		consoleSelect(&bottomScreen);
		
		while (aptMainLoop()) {
			
			hidScanInput();
			if (hidKeysDown() & KEY_START) break;
			else if (hidKeysDown() & KEY_A) {
				printf_log("\x1b[40;34mDownloading example config.json...\x1b[0m\n");
				Result ret = downloadToFile("https://raw.githubusercontent.com/LiquidFenrir/MultiUpdater/master/config.json" , filepath, true);
				if (ret != 0) printf_log("\x1b[40;31mDownload failed!\nError: 0x%08x\x1b[0m\n", (unsigned int)ret);
				else printf_log("\x1b[40;32mDownload successful!\x1b[0m\nYou can now restart the application and enjoy the multiple functions.\n");
			}
			
			gfxFlushBuffers();
			gfxSwapBuffers();
			gspWaitForVBlank();
		}
	}
	else {
		printf_log("\x1b[40;31m\x1b[13;2HError: invalid config.json.\x1b[0m");
		while (aptMainLoop()) {
			
			hidScanInput();
			if (hidKeysDown() & KEY_START) break;
			
			gfxFlushBuffers();
			gfxSwapBuffers();
			
			gspWaitForVBlank();
		}
	}
	
	free(log_file_path);
	
	fsExit();
	amExit();
	httpcExit();
	gfxExit();
	return 0;
}
<file_sep>/source/file.c
#include "file.h"
#include "stringutils.h"

#include "minizip/unzip.h"

#include "7z/7z.h"
#include "7z/7zAlloc.h"
#include "7z/7zCrc.h"
#include "7z/7zMemInStream.h"

void makeDirs(FS_Archive archive, FS_Path filePath)
{
	Result ret = 0;
	
	char * path = NULL;
	asprintf(&path, "%s", (char*)filePath.data);
	
	ret = FSUSER_OpenArchive(&archive, archive, fsMakePath(PATH_EMPTY, ""));
	if (ret != 0) printf_log("Error in:\nFSUSER_OpenArchive\nError: 0x%08x\n", (unsigned int)ret);
	
	for (char * slashpos = strchr(path+1, '/'); slashpos != NULL; slashpos = strchr(slashpos+1, '/')) {
		char bak = *(slashpos);
		*(slashpos) = '\0';
		FS_Path dirpath = fsMakePath(PATH_ASCII, path);
		Handle dirHandle;
		ret = FSUSER_OpenDirectory(&dirHandle, archive, dirpath);
		if(ret == 0) FSDIR_Close(dirHandle);
		else {
			printf_log("Creating dir: %s\n", path);
			ret = FSUSER_CreateDirectory(archive, dirpath, FS_ATTRIBUTE_DIRECTORY);
			if (ret != 0) printf_log("Error in:\nFSUSER_CreateDirectory\nError: 0x%08x\n", (unsigned int)ret);
		}
		*(slashpos) = bak;
	}
	FSUSER_CloseArchive(archive);
	free(path);
	
}

Result openFile(const char * path, Handle * filehandle, bool write)
{
	FS_Archive archive = (FS_Archive){ARCHIVE_SDMC};
	FS_Path emptyPath = fsMakePath(PATH_EMPTY, "");
	u32 flags = (write ? (FS_OPEN_CREATE | FS_OPEN_WRITE) : FS_OPEN_READ);
	FS_Path filePath = {0};
	int prefixlen = 0;
	Result ret = 0;
	
	if (!strncmp(path, "ctrnand:/", 9)) {
		archive = (FS_Archive){ARCHIVE_NAND_CTR_FS};
		prefixlen = 8;
	}
	else if (!strncmp(path, "twlp:/", 6)) {
		archive = (FS_Archive){ARCHIVE_TWL_PHOTO};
		prefixlen = 5;
	}
	else if (!strncmp(path, "twln:/", 6)) {
		archive = (FS_Archive){ARCHIVE_NAND_TWL_FS};
		prefixlen = 5;
	}
	else if (!strncmp(path, "sdmc:/", 6)) {
		prefixlen = 5;
	}
	else if (*path != '/') {
		//if the path is local (doesnt start with a slash), it needs to be appended to the working dir to be valid
		char * actualPath = NULL;
		asprintf(&actualPath, "%s%s", WORKING_DIR, path);
		filePath = fsMakePath(PATH_ASCII, actualPath);
		free(actualPath);
	}
	
	//if the filePath wasnt set above, set it
	if (filePath.size == 0) filePath = fsMakePath(PATH_ASCII, path+prefixlen);
	
	makeDirs(archive, filePath);
	ret = FSUSER_OpenFileDirectly(filehandle, archive, emptyPath, filePath, flags, 0);
	if (ret != 0) printf_log("Error in:\nFSUSER_OpenFileDirectly\nError: 0x%08x\n", (unsigned int)ret);
	if (write) ret = FSFILE_SetSize(*filehandle, 0); //truncate the file to remove previous contents before writing
	if (ret != 0) printf_log("Error in:\nFSFILE_SetSize\nError: 0x%08x\n", (unsigned int)ret);
	
	return ret;
}

Result copyFile(const char * srcpath, const char * destpath)
{
	if (srcpath == NULL || destpath == NULL) {
		printf_log("Can't copy, path is empty.\n");
		return -1;
	}
	
	Handle filehandle;
	Result ret = openFile(srcpath, &filehandle, false);
	if (ret != 0) {
		printf_log("Can't copy, couldn't open source file.\n");
		return -2;
	}
	
	FILE *destptr = fopen(destpath, "wb");
	if (destptr == NULL) {
		printf_log("Couldnt open destination file to write.\n");
		return -3;
	}
	
	printf_log("Copying:\n%s\nto:\n%s\n", srcpath, destpath);
	
	u8 * buf = malloc(0x1000);
	u32 bytesRead = 0;
	u64 offset = 0;
	
	do {
		readFile(filehandle, &bytesRead, offset, buf, 0x1000);
		fwrite(buf, 1, bytesRead, destptr);
		offset += bytesRead;
	} while(bytesRead);
	
	printf_log("Copied %llu bytes.\n", offset);
	
	closeFile(filehandle);
	fclose(destptr);
	free(buf);
	
	return 0;
}

Result extractFileFrom7z(const char * archive_file, const char * filename, const char * filepath)
{	
	if (filename == NULL) {
		printf_log("Cannot start, the inarchive in config.json is blank.\n");
		return EXTRACTION_ERROR_CONFIG;
	}
	
	if (filepath == NULL) {
		printf_log("Cannot start, the path in config.json is blank.\n");
		return EXTRACTION_ERROR_CONFIG;
	}
	
	Result ret = 0;
	
	FILE * archive = fopen(archive_file, "rb");
	if (archive == NULL) {
		printf_log("Error: couldn't open archive to read.\n");
		return EXTRACTION_ERROR_ARCHIVE_OPEN;
	}
	
	fseek(archive, 0, SEEK_END);
	u32 archiveSize = ftell(archive);
	fseek(archive, 0, SEEK_SET);
	
	u8 * archiveData = malloc(archiveSize);
	fread(archiveData, archiveSize, 1, archive);
	
	CMemInStream memStream;
	CSzArEx db;
	ISzAlloc allocImp;
	ISzAlloc allocTempImp;
	
	MemInStream_Init(&memStream, archiveData, archiveSize);
	
	allocImp.Alloc = SzAlloc;
	allocImp.Free = SzFree;
	allocTempImp.Alloc = SzAllocTemp;
	allocTempImp.Free = SzFreeTemp;
	
	CrcGenerateTable();
	SzArEx_Init(&db);
	
	SRes res = SzArEx_Open(&db, &memStream.s, &allocImp, &allocTempImp);
	if (res != SZ_OK) {
		ret = EXTRACTION_ERROR_ARCHIVE_OPEN;
		goto finish;
	}
	
	for (u32 i = 0; i < db.NumFiles; ++i) {
		// Skip directories
		unsigned isDir = SzArEx_IsDir(&db, i);
		if (isDir) continue;
		
		// Get name
		size_t len;
		len = SzArEx_GetFileNameUtf16(&db, i, NULL);
		// Super long filename? Just skip it..
		if (len >= 256) continue;
		
		u16 name[256] = {0};
		SzArEx_GetFileNameUtf16(&db, i, name);
		
		// Convert name to ASCII (just cut the other bytes)
		char name8[256] = {0};
		for (size_t j = 0; j < len; ++j) {
			name8[j] = name[j] % 0xff;
		}
		
		u8 * buf = NULL;
		UInt32 blockIndex = UINT32_MAX;
		size_t fileBufSize = 0;
		size_t offset = 0;
		size_t fileSize = 0;

		if (!matchPattern(filename, name8)) {
			res = SzArEx_Extract(
						&db,
						&memStream.s,
						i,
						&blockIndex,
						&buf,
						&fileBufSize,
						&offset,
						&fileSize,
						&allocImp,
						&allocTempImp
			);
			if (res != SZ_OK) {
				printf_log("Error: Couldn't extract file data from archive.\n");
				ret = EXTRACTION_ERROR_READ_IN_ARCHIVE;
				goto finish;
			}
			
			Handle filehandle;
			u32 bytesWritten = 0;
			
			ret = openFile(filepath, &filehandle, true);
			if (ret != 0) {
				printf_log("Error: couldn't open file to write.\n");
				return EXTRACTION_ERROR_WRITEFILE;
			}
			
			ret = writeFile(filehandle, &bytesWritten, 0, buf+offset, (u32)fileSize);
			closeFile(filehandle);
			
			free(buf);
			
			goto finish;
		}
	}
	
	printf_log("Couldn't find a file with a name matching %s in the archive.\n", filename);
	ret = EXTRACTION_ERROR_FIND;
	
	finish:
	free(archiveData);
	fclose(archive);
	
	return ret;
}

static int unzipMatchPattern(__attribute__ ((unused)) unzFile file, const char *filename1, const char *filename2)
{
	return matchPattern(filename2, filename1);
}

Result extractFileFromZip(const char * archive_file, const char * filename, const char * filepath)
{	
	if (filename == NULL) {
		printf_log("Cannot start, the inarchive in config.json is blank.\n");
		return EXTRACTION_ERROR_CONFIG;
	}
	
	if (filepath == NULL) {
		printf_log("Cannot start, the path in config.json is blank.\n");
		return EXTRACTION_ERROR_CONFIG;
	}
	
	Result ret = 0;
	u8 * buf = NULL;
	Handle filehandle;
	u32 bytesWritten = 0;
	u64 offset = 0;
	
	ret = openFile(filepath, &filehandle, true);
	if (ret != 0) {
		printf_log("Error: couldn't open file to write.\n");
		return EXTRACTION_ERROR_WRITEFILE;
	}
	
	unzFile uf = unzOpen64(archive_file);
	if (uf == NULL) {
		printf_log("Couldn't open zip file.\n");
		closeFile(filehandle);
		return EXTRACTION_ERROR_ARCHIVE_OPEN;
	}
	
	unz_file_info payloadInfo = {};
	
	ret = unzLocateFile(uf, filename, &unzipMatchPattern);

	if (ret == UNZ_END_OF_LIST_OF_FILE) {
		printf_log("Couldn't find the wanted file in the zip.\n");
		ret = EXTRACTION_ERROR_FIND;
	}
	else if (ret == UNZ_OK) {
		ret = unzGetCurrentFileInfo(uf, &payloadInfo, NULL, 0, NULL, 0, NULL, 0);
		if (ret != UNZ_OK) {
			printf_log("Couldn't get the file information.\n");
			ret = EXTRACTION_ERROR_INFO;
			goto finish;
		}
		
		u32 toRead = 0x1000;
		buf = malloc(toRead);
		if (buf == NULL) {
			ret = EXTRACTION_ERROR_ALLOC;
			goto finish;
		}
		
		ret = unzOpenCurrentFile(uf);
		if (ret != UNZ_OK) {
			printf_log("Couldn't open file in the archive to read\n");
			ret = EXTRACTION_ERROR_OPEN_IN_ARCHIVE;
			goto finish;
		}
		
		u32 size = payloadInfo.uncompressed_size;
		
		do {
			if (size < toRead) toRead = size;
			ret = unzReadCurrentFile(uf, buf, toRead);
			if (ret < 0) {
				printf_log("Couldn't read data from the file in the archive\n");
				ret = EXTRACTION_ERROR_READ_IN_ARCHIVE;
				goto finish;
			}
			else {
				ret = writeFile(filehandle, &bytesWritten, offset, buf, toRead);
				offset += toRead;
			}
			size -= toRead;
		} while(size);
	}
	
	finish:
	free(buf);
	unzClose(uf);
	
	closeFile(filehandle);
	
	return ret;
}

Result extractFileFromArchive(const char * archive_path, const char * filename, const char * filepath)
{
	Result ret = EXTRACTION_ERROR_ARCHIVE_OPEN;
	
	unzFile uf = unzOpen64(archive_path);
	if (uf == NULL) {
		printf_log("Opening as a 7z file...\n");
		//failure to open as a zip -> must be 7z
		ret = extractFileFrom7z(archive_path, filename, filepath);
	}
	else {
		printf_log("Opening as a zip file...\n");
		unzClose(uf);
		ret = extractFileFromZip(archive_path, filename, filepath);
	}
	
	remove(archive_path);
	return ret;
}
<file_sep>/source/draw.c
#include "draw.h"

#define MENU_WIDTH 40
#define MAX_ENTRIES_PER_SCREEN 18
int scroll = 0;

void drawInstructions()
{
	printf("\x1b[40;33m" //color the text to make it more noticeable
	       "Press (A) to update the highlighted\n and marked entries.\n" 
	       "Press (B) to backup the highlighted\n and marked entries.\n" 
	       "Press (X) to restore the backups of the\n highlighted and marked entries\n"
	       "\n"
	       "Press (Y) to mark or unmark the\n highlighted entry.\n" 
	       "Press [L] to mark all entries.\n" 
	       "Press [R] to unmark all entries.\x1b[0m\n");
}

void drawMenu(config_t * parsed_config, u8 * state, int selected_entry)
{
	int i;
	
	for (i = 0; i < parsed_config->entries_number; i++) {
		if (parsed_config->entries_number <= MAX_ENTRIES_PER_SCREEN) break;
		
		if (scroll > selected_entry) {
			scroll--;
		}
		
		if (i < selected_entry && selected_entry-scroll >= MAX_ENTRIES_PER_SCREEN && scroll != parsed_config->entries_number-MAX_ENTRIES_PER_SCREEN) {
			scroll++;
		}
	}
	
	for (i = 0; i <= 40; i++) {
		printf("\x1b[0;40;37m\x1b[%u;%uH=", 5, (4+i));
		printf("\x1b[0;40;37m\x1b[%u;%uH=", 5+MAX_ENTRIES_PER_SCREEN+1, (4+i));
	}
	
	for(i = scroll; i < (MAX_ENTRIES_PER_SCREEN + scroll); i++) {
		
		char * current_name = (char *)parsed_config->entries[i].name;
		
		if (current_name != NULL) {
			char format[64];
			sprintf(format, "\x1b[%u;4H", (i+6-scroll));
			
			strcat(format, (i == selected_entry) ? "\x1b[47;" : "\x1b[40;"); //selected entry has gray background, otherwise, black background
			if (state[i] & STATE_MARKED) {
				strcat(format, "33"); //marked entries have yellow text
			}
			else if (state[i] & UPDATE_DONE) {
				strcat(format, "32"); //already completed entries have green/lime text
			}
			else if (state[i] & UPDATE_ERROR) {
				strcat(format, "31"); //entries where errors have happened have red text
			}
			else {
				strcat(format, (i == selected_entry) ? "30" : "37"); //all the others have white text, except the selected which has black
			}
			
			//always print * characters, with padding if needed, and truncate if over
			strcat(format, "m%*.*s");
			printf(format, -(MENU_WIDTH+1), MENU_WIDTH+1, current_name); //the - tells it to pad to the right
		}
	}
}
<file_sep>/source/log.c
#include "log.h"
#include <stdarg.h>

char * log_file_path = NULL;
FILE * log_file = NULL;

void open_log_file(void)
{
	log_file = fopen(log_file_path, "a");
}

void close_log_file(void)
{
	fclose(log_file);
}

void printf_log(const char *format, ...)
{
	open_log_file();
	va_list args;
	va_start(args, format);
	vfprintf(log_file, format, args);
	vprintf(format, args);
	va_end(args);
	close_log_file();
}
<file_sep>/source/config.h
#pragma once

#include "basic.h"

typedef struct {
	const char * name;
	const char * url;
	const char * path;
	const char * in_archive;
	const char * in_release;
} entry_t;

typedef struct {
	u8 errorState;
	int entries_number;
	entry_t entries[256];
} config_t;

void get_config(const char * filepath, config_t * parsed_config);
void clean_config(config_t * parsed_config);
<file_sep>/source/log.h
#pragma once

#include "basic.h"

#define LOGFILE_NAME "output_log.txt"

extern char * log_file_path;
void printf_log(const char *format, ...);
<file_sep>/source/cia.h
#pragma once

#include "basic.h"

Result installCia(const char * ciaPath);
<file_sep>/source/stringutils.h
#pragma once

#include "basic.h"

int matchPattern(const char * pattern, const char * str);
void cleanPath(char * path);
<file_sep>/source/file.h
#pragma once

#include "basic.h"

#define EXTRACTION_ERROR_CONFIG -1
#define EXTRACTION_ERROR_WRITEFILE -2
#define EXTRACTION_ERROR_ARCHIVE_OPEN -3
#define EXTRACTION_ERROR_FIND -4
#define EXTRACTION_ERROR_INFO -5
#define EXTRACTION_ERROR_ALLOC -6
#define EXTRACTION_ERROR_OPEN_IN_ARCHIVE -7
#define EXTRACTION_ERROR_READ_IN_ARCHIVE -8

Result openFile(const char * path, Handle * filehandle, bool write);

inline Result readFile(Handle filehandle, u32 * bytesRead, u64 offset, void * buffer, u32 size)
{
	return FSFILE_Read(filehandle, bytesRead, offset, buffer, size);
}
inline Result writeFile(Handle filehandle, u32 * bytesWritten, u64 offset, void * buffer, u32 size)
{
	return FSFILE_Write(filehandle, bytesWritten, offset, buffer, size, 0);
}
inline Result closeFile(Handle filehandle)
{
	return FSFILE_Close(filehandle);
}

Result copyFile(const char * srcpath, const char * destpath);
Result extractFileFromArchive(const char * archive_path, const char * filename, const char * filepath);
<file_sep>/source/stringutils.c
#include "stringutils.h"

int matchPattern(const char * pattern, const char * str)
{
	int p_offset = 0, s_offset = 0;
	char current_p_char = '\0', current_s_char = '\0', next_p_char = '\0';

	while (str[s_offset] != '\0') {
		current_p_char = pattern[p_offset];
		current_s_char = str[s_offset];
		if (current_p_char == '*' && next_p_char == '\0') {
			next_p_char = pattern[p_offset+1];
		}
		if (next_p_char != '\0') {
			if (current_s_char != next_p_char) {
				s_offset++;
				continue;
			}
			else {
				current_p_char = next_p_char;
				p_offset++;
				next_p_char = '\0';
			}
		}
		if (current_p_char != current_s_char) return 1;
		
		s_offset++;
		p_offset++;
	}
	return (next_p_char != '\0');
}

void cleanPath(char * path)
{
	for (int i = 0; path[i]; i++) { //replace all spaces and fat32 reserved characters in the path with underscores 
		switch (path[i]) {
			case ' ':
			case '"':
			case '*':
			case ':':
			case '<':
			case '>':
			case '?':
			case '\\':
			case '|':
				path[i] = '_';
			default:
				break;
		}
	}
}
<file_sep>/selfupdater/selfupdater.c
/*
Those 2 files (selfupdater.c and selfupdater.h) are in the public domain.
You can use them in whatever your project is, without caring about the license
*/

#include <3ds.h>
#include <string.h>

#define MULTIUPDATER_TITLEID 0x000400000D5C4900

typedef struct {
	char name[256];
	char url[256];
	char path[256];
	char in_archive[256];
	char in_release[256];
	char magic[5];
} temp_entry_t;

void update(const char * name, const char * url, const char * path, const char * in_archive, const char * in_release)
{
	temp_entry_t sent_entry;
	
	strncpy(sent_entry.name, name, 256);
	strncpy(sent_entry.url, url, 256);
	strncpy(sent_entry.path, path, 256);
	strncpy(sent_entry.in_archive, in_archive, 256);
	strncpy(sent_entry.in_release, in_release, 256);
	strncpy(sent_entry.magic, "MULTI", 5);
	
	APT_PrepareToDoApplicationJump(0, MULTIUPDATER_TITLEID, MEDIATYPE_SD);
	
	u8 hmac[0x20] = {0};
	APT_DoApplicationJump(&sent_entry, sizeof(sent_entry), hmac);
}<file_sep>/source/gitapi.h
#pragma once

#include "basic.h"

void getAssetUrl(const char * apiresponse, const char * element, char ** asseturl);
<file_sep>/source/basic.h
#pragma once

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>
#include <3ds.h>

#define WORKING_DIR "/3ds/MultiUpdater/"

#include "log.h"

#define ERROR_JSON 1
#define ERROR_FILE 2

#define UPDATE_ERROR 1
#define UPDATE_DONE 2
#define STATE_MARKED 4
<file_sep>/source/draw.h
#pragma once

#include "basic.h"
#include "config.h"

void drawInstructions(void);
void drawMenu(config_t * config, u8 * state, int selected_entry);
<file_sep>/source/params.c
#include "params.h"

typedef struct {
	char name[256];
	char url[256];
	char path[256];
	char in_archive[256];
	char in_release[256];
	char magic[5];
} temp_entry_t;

bool handleParams(entry_t * received_entry)
{
	u8 hmac[0x20];
	u64 sender = 0;
	bool received = false;
	temp_entry_t temp_entry;
	
	APT_ReceiveDeliverArg(&temp_entry, sizeof(temp_entry), hmac, &sender, &received);
	//received is useless, it will get set to 1 (true) everytime
	if (strncmp(temp_entry.magic, "MULTI", 5))
		return false;
	
	if (strlen(temp_entry.name) != 0)
		received_entry->name = strdup(temp_entry.name);
	if (strlen(temp_entry.url) != 0)
		received_entry->url = strdup(temp_entry.url);
	if (strlen(temp_entry.path) != 0)
		received_entry->path = strdup(temp_entry.path);
	if (strlen(temp_entry.in_archive) != 0)
		received_entry->in_archive = strdup(temp_entry.in_archive);
	if (strlen(temp_entry.in_release) != 0)
		received_entry->in_release = strdup(temp_entry.in_release);
	
	return true;
}
