diff --git a/include/remote.h b/include/remote.h index acea822..db2cf84 100644 --- a/include/remote.h +++ b/include/remote.h @@ -52,6 +52,6 @@ #define CACHE_PATH_FORMAT "/3ds/" APP_TITLE "/cache/%" JSON_INTEGER_FORMAT bool themeplaza_browser(EntryMode mode); -u32 http_get(const char *url, char ** filename, char ** buf, InstallType install_type); +u32 http_get(const char *url, char ** filename, char ** buf, InstallType install_type, const char *mime_type); #endif diff --git a/source/camera.c b/source/camera.c index c84aeac..f050afa 100644 --- a/source/camera.c +++ b/source/camera.c @@ -229,7 +229,7 @@ void update_qr(qr_data *data) draw_install(INSTALL_DOWNLOAD); char * zip_buf = NULL; char * filename = NULL; - u32 zip_size = http_get((char*)scan_data.payload, &filename, &zip_buf, INSTALL_DOWNLOAD); + u32 zip_size = http_get((char*)scan_data.payload, &filename, &zip_buf, INSTALL_DOWNLOAD, "application/zip"); if(zip_size != 0) { @@ -288,19 +288,17 @@ void update_qr(qr_data *data) } else { + // TODO: handle more elegantly throw_error("Zip downloaded is neither\na splash nor a theme.", ERROR_LEVEL_WARNING); } } else { + // TODO: marked for deletion throw_error("File downloaded isn't a zip.", ERROR_LEVEL_WARNING); } free(zip_buf); } - else - { - throw_error("Download failed.", ERROR_LEVEL_WARNING); - } free(filename); } diff --git a/source/remote.c b/source/remote.c index f43b09d..74fd46f 100644 --- a/source/remote.c +++ b/source/remote.c @@ -99,11 +99,11 @@ static Instructions_s extra_instructions = { static void free_icons(Entry_List_s * list) { - if(list != NULL) + if (list != NULL) { - if(list->icons != NULL) + if (list->icons != NULL) { - for(int i = 0; i < list->entries_count; i++) + for (int i = 0; i < list->entries_count; i++) { C3D_TexDelete(list->icons[i]->tex); free(list->icons[i]->tex); @@ -122,17 +122,17 @@ static C2D_Image * load_remote_smdh(Entry_s * entry, bool ignore_cache) not_cached = !smdh_size || ignore_cache; // if the size is 0, the file wasn't there - if(not_cached) + if (not_cached) { free(smdh_buf); smdh_buf = NULL; char * api_url = NULL; asprintf(&api_url, THEMEPLAZA_SMDH_FORMAT, entry->tp_download_id); - smdh_size = http_get(api_url, NULL, &smdh_buf, INSTALL_NONE); + smdh_size = http_get(api_url, NULL, &smdh_buf, INSTALL_NONE, "application/octet-stream"); free(api_url); } - if(!smdh_size) + if (!smdh_size) { free(smdh_buf); smdh_buf = NULL; @@ -140,16 +140,16 @@ static C2D_Image * load_remote_smdh(Entry_s * entry, bool ignore_cache) Icon_s * smdh = (Icon_s *)smdh_buf; - u16 fallback_name[0x81] = {0}; - utf8_to_utf16(fallback_name, (u8*)"No name", 0x80); + u16 fallback_name[0x81] = { 0 }; + utf8_to_utf16(fallback_name, (u8 *)"No name", 0x80); parse_smdh(smdh, entry, fallback_name); C2D_Image * image = loadTextureIcon(smdh); - if(not_cached) + if (not_cached) { FSUSER_CreateDirectory(ArchiveSD, fsMakePath(PATH_UTF16, entry->path), FS_ATTRIBUTE_DIRECTORY); - u16 path[0x107] = {0}; + u16 path[0x107] = { 0 }; strucat(path, entry->path); struacat(path, "/info.smdh"); remake_file(fsMakePath(PATH_UTF16, path), ArchiveSD, smdh_size); @@ -160,13 +160,13 @@ static C2D_Image * load_remote_smdh(Entry_s * entry, bool ignore_cache) return image; } -static void load_remote_entries(Entry_List_s * list, json_t *ids_array, bool ignore_cache, InstallType type) +static void load_remote_entries(Entry_List_s * list, json_t * ids_array, bool ignore_cache, InstallType type) { free_icons(list); list->entries_count = json_array_size(ids_array); free(list->entries); list->entries = calloc(list->entries_count, sizeof(Entry_s)); - list->icons = calloc(list->entries_count, sizeof(C2D_Image*)); + list->icons = calloc(list->entries_count, sizeof(C2D_Image * )); list->entries_loaded = list->entries_count; size_t i = 0; @@ -179,7 +179,7 @@ static void load_remote_entries(Entry_List_s * list, json_t *ids_array, bool ign char * entry_path = NULL; asprintf(&entry_path, CACHE_PATH_FORMAT, current_entry->tp_download_id); - utf8_to_utf16(current_entry->path, (u8*)entry_path, 0x106); + utf8_to_utf16(current_entry->path, (u8 *)entry_path, 0x106); free(entry_path); list->icons[i] = load_remote_smdh(current_entry, ignore_cache); @@ -188,27 +188,27 @@ static void load_remote_entries(Entry_List_s * list, json_t *ids_array, bool ign static void load_remote_list(Entry_List_s * list, json_int_t page, EntryMode mode, bool ignore_cache) { - if(page > list->tp_page_count) + if (page > list->tp_page_count) page = 1; - if(page <= 0) + if (page <= 0) page = list->tp_page_count; list->selected_entry = 0; InstallType loading_screen = INSTALL_NONE; - if(mode == MODE_THEMES) + if (mode == MODE_THEMES) loading_screen = INSTALL_LOADING_REMOTE_THEMES; - else if(mode == MODE_SPLASHES) + else if (mode == MODE_SPLASHES) loading_screen = INSTALL_LOADING_REMOTE_SPLASHES; draw_install(loading_screen); char * page_json = NULL; char * api_url = NULL; - asprintf(&api_url, THEMEPLAZA_PAGE_FORMAT, page, mode+1, list->tp_search); - u32 json_len = http_get(api_url, NULL, &page_json, INSTALL_NONE); + asprintf(&api_url, THEMEPLAZA_PAGE_FORMAT, page, mode + 1, list->tp_search); + u32 json_len = http_get(api_url, NULL, &page_json, INSTALL_NONE, "application/json"); free(api_url); - if(json_len) + if (json_len) { list->tp_current_page = page; list->mode = mode; @@ -217,18 +217,19 @@ static void load_remote_list(Entry_List_s * list, json_int_t page, EntryMode mod list->entries_per_screen_h = entries_per_screen_h[mode]; json_error_t error; - json_t *root = json_loadb(page_json, json_len, 0, &error); - if(root) + json_t * root = json_loadb(page_json, json_len, 0, &error); + if (root) { - const char *key; - json_t *value; + const char * key; + json_t * value; json_object_foreach(root, key, value) { - if(json_is_integer(value) && !strcmp(key, THEMEPLAZA_JSON_PAGE_COUNT)) + if (json_is_integer(value) && !strcmp(key, THEMEPLAZA_JSON_PAGE_COUNT)) list->tp_page_count = json_integer_value(value); - else if(json_is_array(value) && !strcmp(key, THEMEPLAZA_JSON_PAGE_IDS)) + else if (json_is_array(value) && !strcmp(key, THEMEPLAZA_JSON_PAGE_IDS)) load_remote_entries(list, value, ignore_cache, loading_screen); - else if(json_is_string(value) && !strcmp(key, THEMEPLAZA_JSON_ERROR_MESSAGE) && !strcmp(json_string_value(value), THEMEPLAZA_JSON_ERROR_MESSAGE_NOT_FOUND)) + else if (json_is_string(value) && !strcmp(key, THEMEPLAZA_JSON_ERROR_MESSAGE) + && !strcmp(json_string_value(value), THEMEPLAZA_JSON_ERROR_MESSAGE_NOT_FOUND)) throw_error("No results for this search.", ERROR_LEVEL_WARNING); } } @@ -243,19 +244,20 @@ static void load_remote_list(Entry_List_s * list, json_int_t page, EntryMode mod free(page_json); } -static u16 previous_path_preview[0x106] = {0}; -static bool load_remote_preview(Entry_s * entry, C2D_Image* preview_image, int * preview_offset) +static u16 previous_path_preview[0x106] = { 0 }; + +static bool load_remote_preview(Entry_s * entry, C2D_Image * preview_image, int * preview_offset) { bool not_cached = true; - if(!memcmp(&previous_path_preview, entry->path, 0x106*sizeof(u16))) return true; + if (!memcmp(&previous_path_preview, entry->path, 0x106 * sizeof(u16))) return true; char * preview_png = NULL; u32 preview_size = load_data("/preview.png", *entry, &preview_png); not_cached = !preview_size; - if(not_cached) + if (not_cached) { free(preview_png); preview_png = NULL; @@ -265,11 +267,11 @@ static bool load_remote_preview(Entry_s * entry, C2D_Image* preview_image, int * draw_install(INSTALL_LOADING_REMOTE_PREVIEW); - preview_size = http_get(preview_url, NULL, &preview_png, INSTALL_LOADING_REMOTE_PREVIEW); + preview_size = http_get(preview_url, NULL, &preview_png, INSTALL_LOADING_REMOTE_PREVIEW, "image/png"); free(preview_url); } - if(!preview_size) + if (!preview_size) { free(preview_png); return false; @@ -277,9 +279,9 @@ static bool load_remote_preview(Entry_s * entry, C2D_Image* preview_image, int * bool ret = load_preview_from_buffer(preview_png, preview_size, preview_image, preview_offset); - if(ret && not_cached) // only save the preview if it loaded correctly - isn't corrupted + if (ret && not_cached) // only save the preview if it loaded correctly - isn't corrupted { - u16 path[0x107] = {0}; + u16 path[0x107] = { 0 }; strucat(path, entry->path); struacat(path, "/preview.png"); remake_file(fsMakePath(PATH_UTF16, path), ArchiveSD, preview_size); @@ -291,15 +293,16 @@ static bool load_remote_preview(Entry_s * entry, C2D_Image* preview_image, int * return ret; } -static u16 previous_path_bgm[0x106] = {0}; +static u16 previous_path_bgm[0x106] = { 0 }; + static void load_remote_bgm(Entry_s * entry) { - if(!memcmp(&previous_path_bgm, entry->path, 0x106*sizeof(u16))) return; + if (!memcmp(&previous_path_bgm, entry->path, 0x106 * sizeof(u16))) return; char * bgm_ogg = NULL; u32 bgm_size = load_data("/bgm.ogg", *entry, &bgm_ogg); - if(!bgm_size) + if (!bgm_size) { free(bgm_ogg); bgm_ogg = NULL; @@ -309,16 +312,16 @@ static void load_remote_bgm(Entry_s * entry) draw_install(INSTALL_LOADING_REMOTE_BGM); - bgm_size = http_get(bgm_url, NULL, &bgm_ogg, INSTALL_LOADING_REMOTE_BGM); + bgm_size = http_get(bgm_url, NULL, &bgm_ogg, INSTALL_LOADING_REMOTE_BGM, "application/ogg, audio/ogg"); free(bgm_url); - u16 path[0x107] = {0}; + u16 path[0x107] = { 0 }; strucat(path, entry->path); struacat(path, "/bgm.ogg"); remake_file(fsMakePath(PATH_UTF16, path), ArchiveSD, bgm_size); buf_to_file(bgm_size, fsMakePath(PATH_UTF16, path), ArchiveSD, bgm_ogg); - memcpy(&previous_path_bgm, entry->path, 0x106*sizeof(u16)); + memcpy(&previous_path_bgm, entry->path, 0x106 * sizeof(u16)); } free(bgm_ogg); @@ -332,10 +335,10 @@ static void download_remote_entry(Entry_s * entry, EntryMode mode) char * zip_buf = NULL; char * filename = NULL; draw_install(INSTALL_DOWNLOAD); - u32 zip_size = http_get(download_url, &filename, &zip_buf, INSTALL_DOWNLOAD); + u32 zip_size = http_get(download_url, &filename, &zip_buf, INSTALL_DOWNLOAD, "application/zip"); free(download_url); - char path_to_file[0x107] = {0}; + char path_to_file[0x107] = { 0 }; sprintf(path_to_file, "%s%s", main_paths[mode], filename); free(filename); @@ -349,16 +352,17 @@ static void download_remote_entry(Entry_s * entry, EntryMode mode) free(zip_buf); } -static SwkbdCallbackResult jump_menu_callback(void* page_number, const char** ppMessage, const char* text, size_t textlen) +static SwkbdCallbackResult +jump_menu_callback(void * page_number, const char ** ppMessage, const char * text, size_t textlen) { (void)textlen; int typed_value = atoi(text); - if(typed_value > *(json_int_t*)page_number) + if (typed_value > *(json_int_t *)page_number) { *ppMessage = "The new page has to be\nsmaller or equal to the\nnumber of pages!"; return SWKBD_CALLBACK_CONTINUE; } - else if(typed_value == 0) + else if (typed_value == 0) { *ppMessage = "The new position has to\nbe positive!"; return SWKBD_CALLBACK_CONTINUE; @@ -368,17 +372,19 @@ static SwkbdCallbackResult jump_menu_callback(void* page_number, const char** pp static void jump_menu(Entry_List_s * list) { - if(list == NULL) return; + if (list == NULL) return; - char numbuf[64] = {0}; + char numbuf[64] = { 0 }; SwkbdState swkbd; - sprintf(numbuf, "%" JSON_INTEGER_FORMAT, list->tp_page_count); + sprintf(numbuf, "%" + JSON_INTEGER_FORMAT, list->tp_page_count); int max_chars = strlen(numbuf); swkbdInit(&swkbd, SWKBD_TYPE_NUMPAD, 2, max_chars); - sprintf(numbuf, "%" JSON_INTEGER_FORMAT, list->tp_current_page); + sprintf(numbuf, "%" + JSON_INTEGER_FORMAT, list->tp_current_page); swkbdSetInitialText(&swkbd, numbuf); sprintf(numbuf, "Which page do you want to jump to?"); @@ -391,10 +397,10 @@ static void jump_menu(Entry_List_s * list) memset(numbuf, 0, sizeof(numbuf)); SwkbdButton button = swkbdInputText(&swkbd, numbuf, sizeof(numbuf)); - if(button == SWKBD_BUTTON_CONFIRM) + if (button == SWKBD_BUTTON_CONFIRM) { json_int_t newpage = (json_int_t)atoi(numbuf); - if(newpage != list->tp_current_page) + if (newpage != list->tp_current_page) load_remote_list(list, newpage, list->mode, false); } } @@ -402,7 +408,7 @@ static void jump_menu(Entry_List_s * list) static void search_menu(Entry_List_s * list) { const int max_chars = 256; - char * search = calloc(max_chars+1, sizeof(char)); + char * search = calloc(max_chars + 1, sizeof(char)); SwkbdState swkbd; @@ -414,12 +420,12 @@ static void search_menu(Entry_List_s * list) swkbdSetValidation(&swkbd, SWKBD_NOTBLANK, 0, max_chars); SwkbdButton button = swkbdInputText(&swkbd, search, max_chars); - if(button == SWKBD_BUTTON_CONFIRM) + if (button == SWKBD_BUTTON_CONFIRM) { free(list->tp_search); - for(unsigned int i = 0; i < strlen(search); i++) + for (unsigned int i = 0; i < strlen(search); i++) { - if(search[i] == ' ') + if (search[i] == ' ') search[i] = '+'; } list->tp_search = search; @@ -433,21 +439,21 @@ static void search_menu(Entry_List_s * list) static void change_selected(Entry_List_s * list, int change_value) { - if(abs(change_value) >= list->entries_count) return; + if (abs(change_value) >= list->entries_count) return; int newval = list->selected_entry + change_value; - if(abs(change_value) == 1) + if (abs(change_value) == 1) { - if(newval < 0) + if (newval < 0) newval += list->entries_per_screen_h; - if(newval/list->entries_per_screen_h != list->selected_entry/list->entries_per_screen_h) - newval += list->entries_per_screen_h*(-change_value); + if (newval / list->entries_per_screen_h != list->selected_entry / list->entries_per_screen_h) + newval += list->entries_per_screen_h * (-change_value); } else { - if(newval < 0) - newval += list->entries_per_screen_h*list->entries_per_screen_v; + if (newval < 0) + newval += list->entries_per_screen_h * list->entries_per_screen_v; newval %= list->entries_count; } list->selected_entry = newval; @@ -461,27 +467,27 @@ bool themeplaza_browser(EntryMode mode) int preview_offset = 0; audio_s * audio = NULL; - Entry_List_s list = {0}; + Entry_List_s list = { 0 }; Entry_List_s * current_list = &list; current_list->tp_search = strdup(""); load_remote_list(current_list, 1, mode, false); - C2D_Image preview = {0}; + C2D_Image preview = { 0 }; bool extra_mode = false; - while(aptMainLoop()) + while (aptMainLoop()) { - if(current_list->entries == NULL) + if (current_list->entries == NULL) break; - if(preview_mode) + if (preview_mode) { draw_preview(preview, preview_offset); } else { Instructions_s instructions = browser_instructions[mode]; - if(extra_mode) + if (extra_mode) instructions = extra_instructions; draw_grid_interface(current_list, instructions); } @@ -492,12 +498,12 @@ bool themeplaza_browser(EntryMode mode) u32 kHeld = hidKeysHeld(); u32 kUp = hidKeysUp(); - if(kDown & KEY_START) + if (kDown & KEY_START) { - exit: + exit: quit = true; downloaded = false; - if(audio) + if (audio) { audio->stop = true; svcWaitSynchronization(audio->finished, U64_MAX); @@ -506,15 +512,15 @@ bool themeplaza_browser(EntryMode mode) break; } - if(extra_mode) + if (extra_mode) { - if(kUp & KEY_X) + if (kUp & KEY_X) extra_mode = false; - if(!extra_mode) + if (!extra_mode) { - if((kDown | kHeld) & KEY_DLEFT) + if ((kDown | kHeld) & KEY_DLEFT) { - change_mode: + change_mode: mode++; mode %= MODE_AMOUNT; @@ -523,15 +529,15 @@ bool themeplaza_browser(EntryMode mode) load_remote_list(current_list, 1, mode, false); } - else if((kDown | kHeld) & KEY_DUP) + else if ((kDown | kHeld) & KEY_DUP) { jump_menu(current_list); } - else if((kDown | kHeld) & KEY_DRIGHT) + else if ((kDown | kHeld) & KEY_DRIGHT) { load_remote_list(current_list, current_list->tp_current_page, mode, true); } - else if((kDown | kHeld) & KEY_DDOWN) + else if ((kDown | kHeld) & KEY_DDOWN) { search_menu(current_list); } @@ -542,24 +548,24 @@ bool themeplaza_browser(EntryMode mode) int selected_entry = current_list->selected_entry; Entry_s * current_entry = ¤t_list->entries[selected_entry]; - if(kDown & KEY_Y) + if (kDown & KEY_Y) { - toggle_preview: - if(!preview_mode) + toggle_preview: + if (!preview_mode) { preview_mode = load_remote_preview(current_entry, &preview, &preview_offset); - if(mode == MODE_THEMES && dspfirm) + if (mode == MODE_THEMES && dspfirm) { load_remote_bgm(current_entry); audio = calloc(1, sizeof(audio_s)); - if(R_FAILED(load_audio(*current_entry, audio))) audio = NULL; - if(audio != NULL) play_audio(audio); + if (R_FAILED(load_audio(*current_entry, audio))) audio = NULL; + if (audio != NULL) play_audio(audio); } } else { preview_mode = false; - if(mode == MODE_THEMES && audio != NULL) + if (mode == MODE_THEMES && audio != NULL) { audio->stop = true; svcWaitSynchronization(audio->finished, U64_MAX); @@ -567,12 +573,12 @@ bool themeplaza_browser(EntryMode mode) } } } - else if(kDown & KEY_B) + else if (kDown & KEY_B) { - if(preview_mode) + if (preview_mode) { preview_mode = false; - if(mode == MODE_THEMES && audio != NULL) + if (mode == MODE_THEMES && audio != NULL) { audio->stop = true; svcWaitSynchronization(audio->finished, U64_MAX); @@ -583,64 +589,64 @@ bool themeplaza_browser(EntryMode mode) break; } - if(preview_mode) + if (preview_mode) goto touch; - if(kDown & KEY_A) + if (kDown & KEY_A) { download_remote_entry(current_entry, mode); downloaded = true; } - else if(kDown & KEY_X) + else if (kDown & KEY_X) { extra_mode = true; } - else if(kDown & KEY_L) + else if (kDown & KEY_L) { - load_remote_list(current_list, current_list->tp_current_page-1, mode, false); + load_remote_list(current_list, current_list->tp_current_page - 1, mode, false); } - else if(kDown & KEY_R) + else if (kDown & KEY_R) { - load_remote_list(current_list, current_list->tp_current_page+1, mode, false); + load_remote_list(current_list, current_list->tp_current_page + 1, mode, false); } - // Movement in the UI - else if(kDown & KEY_UP) + // Movement in the UI + else if (kDown & KEY_UP) { change_selected(current_list, -current_list->entries_per_screen_h); } - else if(kDown & KEY_DOWN) + else if (kDown & KEY_DOWN) { change_selected(current_list, current_list->entries_per_screen_h); } - // Quick moving - else if(kDown & KEY_LEFT) + // Quick moving + else if (kDown & KEY_LEFT) { change_selected(current_list, -1); } - else if(kDown & KEY_RIGHT) + else if (kDown & KEY_RIGHT) { change_selected(current_list, 1); } - touch: - if((kDown | kHeld) & KEY_TOUCH) + touch: + if ((kDown | kHeld) & KEY_TOUCH) { - touchPosition touch = {0}; + touchPosition touch = { 0 }; hidTouchRead(&touch); u16 x = touch.px; u16 y = touch.py; - #define BETWEEN(min, x, max) (min < x && x < max) +#define BETWEEN(min, x, max) (min < x && x < max) int border = 16; - if(kDown & KEY_TOUCH) + if (kDown & KEY_TOUCH) { - if(preview_mode) + if (preview_mode) { preview_mode = false; - if(mode == MODE_THEMES && audio) + if (mode == MODE_THEMES && audio) { audio->stop = true; svcWaitSynchronization(audio->finished, U64_MAX); @@ -648,62 +654,62 @@ bool themeplaza_browser(EntryMode mode) } continue; } - else if(y < 24) + else if (y < 24) { - if(BETWEEN(0, x, 80)) + if (BETWEEN(0, x, 80)) { search_menu(current_list); } - else if(BETWEEN(320-96, x, 320-72)) + else if (BETWEEN(320 - 96, x, 320 - 72)) { break; } - else if(BETWEEN(320-72, x, 320-48)) + else if (BETWEEN(320 - 72, x, 320 - 48)) { goto exit; } - else if(BETWEEN(320-48, x, 320-24)) + else if (BETWEEN(320 - 48, x, 320 - 24)) { goto toggle_preview; } - else if(BETWEEN(320-24, x, 320)) + else if (BETWEEN(320 - 24, x, 320)) { goto change_mode; } } - else if(BETWEEN(240-24, y, 240) && BETWEEN(176, x, 320)) + else if (BETWEEN(240 - 24, y, 240) && BETWEEN(176, x, 320)) { jump_menu(current_list); } else { - if(BETWEEN(0, x, border)) + if (BETWEEN(0, x, border)) { - load_remote_list(current_list, current_list->tp_current_page-1, mode, false); + load_remote_list(current_list, current_list->tp_current_page - 1, mode, false); } - else if(BETWEEN(320-border, x, 320)) + else if (BETWEEN(320 - border, x, 320)) { - load_remote_list(current_list, current_list->tp_current_page+1, mode, false); + load_remote_list(current_list, current_list->tp_current_page + 1, mode, false); } } } else { - if(preview_mode) + if (preview_mode) { preview_mode = false; continue; } - else if(BETWEEN(24, y, 240-24)) + else if (BETWEEN(24, y, 240 - 24)) { - if(BETWEEN(border, x, 320-border)) + if (BETWEEN(border, x, 320 - border)) { x -= border; x /= current_list->entry_size; y -= 24; y /= current_list->entry_size; - int new_selected = y*current_list->entries_per_screen_h + x; - if(new_selected < current_list->entries_count) + int new_selected = y * current_list->entries_per_screen_h + x; + if (new_selected < current_list->entries_count) current_list->selected_entry = new_selected; } } @@ -720,171 +726,327 @@ bool themeplaza_browser(EntryMode mode) return downloaded; } -u32 http_get(const char *url, char ** filename, char ** buf, InstallType install_type) +typedef struct header +{ + char * filename; // allocated in parse_header; if NULL, this is user-provided + u32 file_size; // if == 0, fall back to chunked read +} header; + +typedef enum ParseResult +{ + SUCCESS, // 200/203 (203 indicates a successful request with a transformation applied by a proxy) + REDIRECT, // 301/302/303/307/308 + HTTPC_ERROR, + SERVER_IS_MISBEHAVING, + NO_FILENAME, // provisional + HTTP_UNAUTHORIZED = 401, + HTTP_FORBIDDEN = 403, + HTTP_NOT_FOUND = 404, + HTTP_UNACCEPTABLE = 406, // like 204, usually doesn't happen + HTTP_PROXY_UNAUTHORIZED = 407, + HTTP_GONE = 410, + HTTP_URI_TOO_LONG = 414, + HTTP_IM_A_TEAPOT = 418, // Note that a combined coffee/tea pot that is temporarily out of coffee should instead return 503. + HTTP_UPGRADE_REQUIRED = 426, // the 3DS doesn't support HTTP/2, so we can't upgrade - inform and return + HTTP_LEGAL_REASONS = 451, + HTTP_INTERNAL_SERVER_ERROR = 500, + HTTP_BAD_GATEWAY = 502, + HTTP_SERVICE_UNAVAILABLE = 503, + HTTP_GATEWAY_TIMEOUT = 504, +} ParseResult; + +// the good paths for this function return SUCCESS or REDIRECT; +// all other paths are failures +static ParseResult parse_header(struct header * out, httpcContext * context, bool get_filename, const char * mime) +{ + // status code + u32 status_code; + + if (httpcGetResponseStatusCode(context, &status_code)) + { + DEBUG("httpcGetResponseStatusCode\n"); + return HTTPC_ERROR; + } + + DEBUG("HTTP %lu\n", status_code); + switch (status_code) + { + case 301: + case 302: + case 307: + case 308: + return REDIRECT; + case 200: + case 203: + break; + default: + return (ParseResult)status_code; + } + + char * content_buf = calloc(1024, sizeof(char)); + + // Content-Type + + if (mime) + { + httpcGetResponseHeader(context, "Content-Type", content_buf, 1024); + if (!strstr(mime, content_buf)) + { + free(content_buf); + return SERVER_IS_MISBEHAVING; + } + } + + // Content-Length + + if (httpcGetDownloadSizeState(context, NULL, &out->file_size)) + { + DEBUG("httpcGetDownloadSizeState\n"); + return HTTPC_ERROR; // no need to free, program dies anyway + } + + // Content-Disposition + + if (get_filename) + { + if (httpcGetResponseHeader(context, "Content-Disposition", content_buf, 1024)) + { + free(content_buf); + DEBUG("httpcGetResponseHeader\n"); + return HTTPC_ERROR; + } + + // content_buf: Content-Disposition: attachment; ... filename=;? ... + + char * filename = strstr(content_buf, "filename="); // filename=;? ... + if (!filename) + { + free(content_buf); + return NO_FILENAME; + } + + filename = strpbrk(filename, "=") + 1; // ;? + char * end = strpbrk(filename, ";"); + if (end) + *end = '\0'; // + + if (filename[0] == '"') + // safe to assume the filename is quoted + { + filename[strlen(filename) - 1] = '\0'; + filename++; + } + + char * illegal_char; + // filter out characters illegal in FAT32 filenames + while ((illegal_char = strpbrk(filename, "><\"?;:/\\+,.|[=]"))) + *illegal_char = '-'; + + out->filename = malloc(strlen(filename) + 1); + strcpy(out->filename, filename); + DEBUG("%s\n", out->filename); + } + return SUCCESS; +} + +#define ZIP_NOT_AVAILABLE "ZIP not found at this URL\nIf you believe this is an error, please\ncontact the site administrator" + +/* + * call example: written = http_get("url", &filename, &buffer_to_download_to, INSTALL_DOWNLOAD, "application/json"); + */ +u32 http_get(const char * url, char ** filename, char ** buf, InstallType install_type, const char * acceptable_mime_types) { Result ret; httpcContext context; - char *new_url = NULL; - u32 status_code; - u32 content_size = 0; - u32 read_size = 0; - u32 size = 0; - char *last_buf; + char redirect_url[0x824] = {0}; + char new_url[0x824] = {0}; - do { - ret = httpcOpenContext(&context, HTTPC_METHOD_GET, url, 1); - if (ret != 0) - { - httpcCloseContext(&context); - if (new_url != NULL) free(new_url); - DEBUG("httpcOpenContext %.8lx\n", ret); - return 0; - } - ret = httpcSetSSLOpt(&context, SSLCOPT_DisableVerify); // should let us do https - ret = httpcSetKeepAlive(&context, HTTPC_KEEPALIVE_ENABLED); - ret = httpcAddRequestHeaderField(&context, "User-Agent", USER_AGENT); - ret = httpcAddRequestHeaderField(&context, "Connection", "Keep-Alive"); + struct header _header = {}; - ret = httpcBeginRequest(&context); - if (ret != 0) - { - httpcCloseContext(&context); - if (new_url != NULL) free(new_url); - DEBUG("httpcBeginRequest %.8lx\n", ret); - return 0; - } + DEBUG("Original URL: %s\n", url); - ret = httpcGetResponseStatusCode(&context, &status_code); - if(ret!=0){ - httpcCloseContext(&context); - if(new_url!=NULL) free(new_url); - DEBUG("httpcGetResponseStatusCode\n"); - return 0; - } - - if ((status_code >= 301 && status_code <= 303) || (status_code >= 307 && status_code <= 308)) - { - if (new_url == NULL) new_url = malloc(0x1000); - ret = httpcGetResponseHeader(&context, "Location", new_url, 0x1000); - url = new_url; - httpcCloseContext(&context); - } - } while ((status_code >= 301 && status_code <= 303) || (status_code >= 307 && status_code <= 308)); - - if (status_code != 200) - { - httpcCloseContext(&context); - if (new_url != NULL) free(new_url); - DEBUG("status_code, %lu\n", status_code); - return 0; - } - - ret = httpcGetDownloadSizeState(&context, NULL, &content_size); +redirect: // goto here if we need to redirect + ret = httpcOpenContext(&context, HTTPC_METHOD_GET, url, 1); if (ret != 0) { httpcCloseContext(&context); - if (new_url != NULL) free(new_url); - DEBUG("httpcGetDownloadSizeState\n"); + DEBUG("httpcOpenContext %.8lx\n", ret); return 0; } - *buf = malloc(0x1000); - if (*buf == NULL) + httpcSetSSLOpt(&context, SSLCOPT_DisableVerify); // should let us do https + httpcSetKeepAlive(&context, HTTPC_KEEPALIVE_ENABLED); + httpcAddRequestHeaderField(&context, "User-Agent", USER_AGENT); + httpcAddRequestHeaderField(&context, "Connection", "Keep-Alive"); + if (acceptable_mime_types) + httpcAddRequestHeaderField(&context, "Accept", acceptable_mime_types); + + ret = httpcBeginRequest(&context); + if (ret != 0) { httpcCloseContext(&context); - free(new_url); - DEBUG("malloc\n"); + DEBUG("httpcBeginRequest %.8lx\n", ret); return 0; } - if(filename) + ParseResult parse = parse_header(&_header, &context, (bool)filename, acceptable_mime_types); + switch (parse) { - char *content_disposition = calloc(1024, sizeof(char)); - ret = httpcGetResponseHeader(&context, "Content-Disposition", content_disposition, 1024); - if (ret != 0) + case NO_FILENAME: + case SUCCESS: + break; + case REDIRECT: + httpcGetResponseHeader(&context, "Location", redirect_url, 0x824); + httpcCloseContext(&context); + if (*redirect_url == '/') // if relative URL { - free(content_disposition); - free(new_url); - free(*buf); - DEBUG("httpcGetResponseHeader\n"); - return 0; + strcpy(new_url, url); + // this is good code, i promise + *(strchr(strchr(strchr(new_url, '/') + 1, '/') + 1, '/')) = '\0'; + strncat(new_url, redirect_url, 0x824 - strlen(new_url)); + url = new_url; } - - char * tok = strstr(content_disposition, "filename="); - - if(!(tok)) + else { - free(content_disposition); - free(new_url); - free(*buf); - throw_error("Target is not valid!", ERROR_LEVEL_WARNING); - DEBUG("filename\n"); - return 0; + url = redirect_url; } - - tok += sizeof("filename=") - 1; - if(ispunct((int)*tok)) - { - tok++; - } - char* last_char = tok + strlen(tok) - 1; - if(ispunct((int)*last_char)) - { - *last_char = '\0'; - } - - char *illegal_characters = "\"?;:/\\+"; - for (size_t i = 0; i < strlen(tok); i++) - { - for (size_t n = 0; n < strlen(illegal_characters); n++) - { - if ((tok)[i] == illegal_characters[n]) - { - (tok)[i] = '-'; - } - } - } - - *filename = calloc(1024, sizeof(char)); - strcpy(*filename, tok); - free(content_disposition); + DEBUG("HTTP Redirect: %s %s\n", new_url, *redirect_url == '/' ? "relative" : "absolute"); + goto redirect; + case HTTP_UNACCEPTABLE: + DEBUG("HTTP 406 Unacceptable; Accept: %s\n", acceptable_mime_types); + throw_error(ZIP_NOT_AVAILABLE, ERROR_LEVEL_WARNING); + return httpcCloseContext(&context); + case SERVER_IS_MISBEHAVING: + DEBUG("Server is misbehaving (provided resource with incorrect MIME)\n"); + throw_error(ZIP_NOT_AVAILABLE, ERROR_LEVEL_WARNING); + return httpcCloseContext(&context); + case HTTPC_ERROR: + DEBUG("httpc error\n"); + throw_error("Error in HTTPC sysmodule.\nIf you are seeing this, please contact an Anemone developer\non the ThemePlaza Discord.", ERROR_LEVEL_ERROR); + quit = true; + return httpcCloseContext(&context); + case HTTP_NOT_FOUND: + case HTTP_GONE: + // TODO: check if we're looking at a TP URL, if we are, suggest that it might be missing + const char * http_error = parse == HTTP_NOT_FOUND ? "404 Not Found" : "410 Gone"; + DEBUG("HTTP %s; URL: %s\n", http_error, url); + char err_buf[0x69]; + snprintf(err_buf, 0x69, "HTTP %s\nCheck that the URL is correct.", http_error); + throw_error(err_buf, ERROR_LEVEL_WARNING); + return httpcCloseContext(&context); + case HTTP_UNAUTHORIZED: + case HTTP_FORBIDDEN: + case HTTP_PROXY_UNAUTHORIZED: + DEBUG("HTTP %u: device not authenticated\n", parse); + char err_buf[0x69]; + snprintf(err_buf, 0x69, "HTTP %s\nContact the site administrator.", parse == HTTP_UNAUTHORIZED + ? "401 Unauthorized" + : parse == HTTP_FORBIDDEN + ? "403 Forbidden" + : "407 Proxy Authentication Required"); + throw_error(err_buf, ERROR_LEVEL_WARNING); + return httpcCloseContext(&context); + case HTTP_URI_TOO_LONG: + DEBUG("HTTP 414; URL is too long, maybe too many redirects?\n"); + throw_error("HTTP 414 URI Too Long\nThe QR code points to a really long URL.\nContact the site administrator.", ERROR_LEVEL_WARNING); + return httpcCloseContext(&context); + case HTTP_IM_A_TEAPOT: + DEBUG("HTTP 418 I'm a teapot\n"); + throw_error("HTTP 418 I'm a teapot\nContact the site administrator.", ERROR_LEVEL_WARNING); + return httpcCloseContext(&context); + case HTTP_UPGRADE_REQUIRED: + DEBUG("HTTP 426; HTTP/2 required\n"); + throw_error("HTTP 426 Upgrade Required\nThe 3DS does not support this website.\nContact the site administrator.", ERROR_LEVEL_WARNING); + return httpcCloseContext(&context); + case HTTP_LEGAL_REASONS: + DEBUG("HTTP 451; URL: %s\n", url); + throw_error("HTTP 451 Unavailable for Legal Reasons\nSome entity is preventing access\nto the host server for legal reasons.", ERROR_LEVEL_WARNING); + return httpcCloseContext(&context); + case HTTP_INTERNAL_SERVER_ERROR: + DEBUG("HTTP 500\n"); + throw_error("HTTP 500 Internal Server Error\nContact the site administrator.", ERROR_LEVEL_WARNING); + return httpcCloseContext(&context); + case HTTP_BAD_GATEWAY: + DEBUG("HTTP 502\n"); + throw_error("HTTP 502 Bad Gateway\nContact the site administrator.", ERROR_LEVEL_WARNING); + return httpcCloseContext(&context); + case HTTP_SERVICE_UNAVAILABLE: + DEBUG("HTTP 503\n"); + throw_error("HTTP 503 Service Unavailable\nContact the site administrator.", ERROR_LEVEL_WARNING); + return httpcCloseContext(&context); + case HTTP_GATEWAY_TIMEOUT: + DEBUG("HTTP 504\n"); + throw_error("HTTP 504 Gateway Timeout\nContact the site administrator.", ERROR_LEVEL_WARNING); + return httpcCloseContext(&context); + default: + { + DEBUG("HTTP %u\n", parse); + char err_buf[0x69]; + snprintf(err_buf, 0x69, "HTTP %u\nIf you believe this is unexpected, please\ncontact the site administrator.", parse); + throw_error(err_buf, ERROR_LEVEL_WARNING); + return httpcCloseContext(&context); + } } - do { - ret = httpcDownloadData(&context, (*(u8**)buf) + size, 0x1000, &read_size); + if (filename) + { + if (parse != NO_FILENAME) + *filename = _header.filename; + else + { + // TODO: swkbd stuff + // needs to be heap allocated only because the call site is expected to free it + *filename = malloc(0x20); + sprintf(*filename, "tobesupplied.zip"); + } + } + + u32 chunk_size; + if (_header.file_size) + // the only reason we chunk this at all is for the download bar; + // in terms of efficiency, allocating the full size + // would avoid 3 reallocs whenever the server isn't lying + chunk_size = _header.file_size / 4; + else + chunk_size = 0x80000; + + *buf = NULL; + char * new_buf; + u32 size = 0; + u32 read_size = 0; + + do + { + new_buf = realloc(*buf, size + chunk_size); + if (new_buf == NULL) + { + httpcCloseContext(&context); + free(*buf); + DEBUG("realloc failed in http_get - file possibly too large?\n"); // TODO: report this? + return 0; + } + *buf = new_buf; + + // download exactly chunk_size bytes and toss them into buf. + // size contains the current offset into buf. + ret = httpcDownloadData(&context, (u8*)(*buf) + size, chunk_size, &read_size); size += read_size; - if(content_size && install_type != INSTALL_NONE) - draw_loading_bar(size, content_size, install_type); - - if (ret == (s32)HTTPC_RESULTCODE_DOWNLOADPENDING) - { - last_buf = *buf; - *buf = realloc(*buf, size + 0x1000); - if (*buf == NULL) - { - httpcCloseContext(&context); - free(new_url); - free(last_buf); - DEBUG("NULL\n"); - return 0; - } - } + if (_header.file_size && install_type != INSTALL_NONE) + draw_loading_bar(size, _header.file_size, install_type); } while (ret == (s32)HTTPC_RESULTCODE_DOWNLOADPENDING); + httpcCloseContext(&context); - last_buf = *buf; - *buf = realloc(*buf, size); - if (*buf == NULL) + // shrink to size + new_buf = realloc(*buf, size); + if (new_buf == NULL) { httpcCloseContext(&context); - free(new_url); - free(last_buf); - DEBUG("realloc\n"); + free(*buf); + DEBUG("shrinking realloc failed\n"); // 何? return 0; } - - httpcCloseContext(&context); - free(new_url); + *buf = new_buf; DEBUG("size: %lu\n", size); return size;