From deb8e5bf1860d7f680bc7af44c0179618f125935 Mon Sep 17 00:00:00 2001 From: JordanTheToaster Date: Mon, 5 Aug 2024 23:36:12 +0100 Subject: [PATCH] 3rdparty/rcheevos: Bump to v11.5.0 --- 3rdparty/rcheevos/CHANGELOG.md | 9 + 3rdparty/rcheevos/include/rc_api_info.h | 3 + 3rdparty/rcheevos/include/rc_client.h | 1 + .../include/rc_client_raintegration.h | 3 +- 3rdparty/rcheevos/src/rapi/rc_api_common.c | 7 +- 3rdparty/rcheevos/src/rapi/rc_api_info.c | 5 +- 3rdparty/rcheevos/src/rc_client.c | 435 +++++++--- 3rdparty/rcheevos/src/rc_client_external.h | 10 +- 3rdparty/rcheevos/src/rc_version.h | 2 +- .../rcheevos/src/rcheevos/runtime_progress.c | 2 +- 3rdparty/rcheevos/src/rhash/hash.c | 760 +++++++++++++++++- 11 files changed, 1075 insertions(+), 162 deletions(-) diff --git a/3rdparty/rcheevos/CHANGELOG.md b/3rdparty/rcheevos/CHANGELOG.md index 359666c284..0d183fbcd8 100644 --- a/3rdparty/rcheevos/CHANGELOG.md +++ b/3rdparty/rcheevos/CHANGELOG.md @@ -1,3 +1,12 @@ +# v11.5.0 +* add total_entries to rc_api_fetch_leaderboard_info_response_t +* add RC_CLIENT_RAINTEGRATION_EVENT_MENU_CHANGED event +* modify rc_client_begin_identify_and_load_game and rc_client_begin_change_media to use locally + registered filereader/cdreader for hash resolution when using rc_client_raintegration +* add support for ISO-8601 timestamps in JSON responses +* update RC_CONSOLE_MS_DOS hash logic to support parent archives +* fix infinite loop that sometimes occurs when resetting while progress tracker is onscreen + # v11.4.0 * add RC_CONDITION_REMEMBER and RC_OPERAND_RECALL * add RC_OPERATOR_ADD and RC_OPERATOR_SUB diff --git a/3rdparty/rcheevos/include/rc_api_info.h b/3rdparty/rcheevos/include/rc_api_info.h index b947f256cd..7d6cfa2bea 100644 --- a/3rdparty/rcheevos/include/rc_api_info.h +++ b/3rdparty/rcheevos/include/rc_api_info.h @@ -129,6 +129,9 @@ typedef struct rc_api_fetch_leaderboard_info_response_t { /* The number of items in the entries array */ uint32_t num_entries; + /* The total number of entries on the server */ + uint32_t total_entries; + /* Common server-provided response information */ rc_api_response_t response; } diff --git a/3rdparty/rcheevos/include/rc_client.h b/3rdparty/rcheevos/include/rc_client.h index 1fa50357c5..26db52932d 100644 --- a/3rdparty/rcheevos/include/rc_client.h +++ b/3rdparty/rcheevos/include/rc_client.h @@ -517,6 +517,7 @@ typedef struct rc_client_leaderboard_entry_t { typedef struct rc_client_leaderboard_entry_list_t { rc_client_leaderboard_entry_t* entries; uint32_t num_entries; + uint32_t total_entries; int32_t user_index; } rc_client_leaderboard_entry_list_t; diff --git a/3rdparty/rcheevos/include/rc_client_raintegration.h b/3rdparty/rcheevos/include/rc_client_raintegration.h index 2aa33dfdb4..847c8ad225 100644 --- a/3rdparty/rcheevos/include/rc_client_raintegration.h +++ b/3rdparty/rcheevos/include/rc_client_raintegration.h @@ -39,7 +39,8 @@ enum { RC_CLIENT_RAINTEGRATION_EVENT_TYPE_NONE = 0, RC_CLIENT_RAINTEGRATION_EVENT_MENUITEM_CHECKED_CHANGED = 1, /* [menu_item] checked changed */ RC_CLIENT_RAINTEGRATION_EVENT_HARDCORE_CHANGED = 2, /* hardcore was enabled or disabled */ - RC_CLIENT_RAINTEGRATION_EVENT_PAUSE = 3 /* emulated system should be paused */ + RC_CLIENT_RAINTEGRATION_EVENT_PAUSE = 3, /* emulated system should be paused */ + RC_CLIENT_RAINTEGRATION_EVENT_MENU_CHANGED = 4 /* one or more items were added/removed from the menu and it should be rebuilt */ }; typedef struct rc_client_raintegration_event_t { diff --git a/3rdparty/rcheevos/src/rapi/rc_api_common.c b/3rdparty/rcheevos/src/rapi/rc_api_common.c index f96daedced..0b9d1f26b3 100644 --- a/3rdparty/rcheevos/src/rapi/rc_api_common.c +++ b/3rdparty/rcheevos/src/rapi/rc_api_common.c @@ -864,8 +864,11 @@ int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* if (*field->value_start == '\"') { memset(&tm, 0, sizeof(tm)); - if (sscanf_s(field->value_start + 1, "%d-%d-%d %d:%d:%d", - &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6) { + if (sscanf_s(field->value_start + 1, "%d-%d-%d %d:%d:%d", /* DB format "2013-10-20 22:12:21" */ + &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6 || + /* NOTE: relies on sscanf stopping when it sees a non-digit after the seconds. could be 'Z', '.', '+', or '-' */ + sscanf_s(field->value_start + 1, "%d-%d-%dT%d:%d:%d", /* ISO format "2013-10-20T22:12:21.000000Z */ + &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6) { tm.tm_mon--; /* 0-based */ tm.tm_year -= 1900; /* 1900 based */ diff --git a/3rdparty/rcheevos/src/rapi/rc_api_info.c b/3rdparty/rcheevos/src/rapi/rc_api_info.c index 6f3822a2d0..0339f256d4 100644 --- a/3rdparty/rcheevos/src/rapi/rc_api_info.c +++ b/3rdparty/rcheevos/src/rapi/rc_api_info.c @@ -189,7 +189,8 @@ int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboa RC_JSON_NEW_FIELD("LBAuthor"), RC_JSON_NEW_FIELD("LBCreated"), RC_JSON_NEW_FIELD("LBUpdated"), - RC_JSON_NEW_FIELD("Entries") /* array */ + RC_JSON_NEW_FIELD("Entries"), /* array */ + RC_JSON_NEW_FIELD("TotalEntries") /* unused fields RC_JSON_NEW_FIELD("GameTitle"), RC_JSON_NEW_FIELD("ConsoleID"), @@ -235,6 +236,8 @@ int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboa return RC_MISSING_VALUE; if (!rc_json_get_required_datetime(&response->updated, &response->response, &leaderboarddata_fields[9], "LBUpdated")) return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->total_entries, &response->response, &leaderboarddata_fields[11], "TotalEntries")) + return RC_MISSING_VALUE; if (!leaderboarddata_fields[1].value_end) return RC_MISSING_VALUE; diff --git a/3rdparty/rcheevos/src/rc_client.c b/3rdparty/rcheevos/src/rc_client.c index 17f66a8507..db8ab8bccd 100644 --- a/3rdparty/rcheevos/src/rc_client.c +++ b/3rdparty/rcheevos/src/rc_client.c @@ -78,6 +78,7 @@ typedef struct rc_client_load_state_t #endif } rc_client_load_state_t; +static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state); static void rc_client_begin_fetch_game_data(rc_client_load_state_t* callback_data); static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game); static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message); @@ -1984,12 +1985,101 @@ static void rc_client_fetch_game_data_callback(const rc_api_server_response_t* s rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); } -static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) +static rc_client_game_info_t* rc_client_allocate_game(void) +{ + rc_client_game_info_t* game = (rc_client_game_info_t*)calloc(1, sizeof(*game)); + if (!game) + return NULL; + + rc_buffer_init(&game->buffer); + rc_runtime_init(&game->runtime); + + return game; +} + +static int rc_client_attach_load_state(rc_client_t* client, rc_client_load_state_t* load_state) +{ + if (client->state.load == NULL) { + rc_client_unload_game(client); + client->state.load = load_state; + + if (load_state->game == NULL) { + load_state->game = rc_client_allocate_game(); + if (!load_state->game) { + if (load_state->callback) + load_state->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, load_state->callback_userdata); + + return 0; + } + } + } + else if (client->state.load != load_state) { + /* previous load was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + + return 0; + } + + return 1; +} + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + +static void rc_client_external_load_state_callback(int result, const char* error_message, rc_client_t* client, void* userdata) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)userdata; + int async_aborted; + + client = load_state->client; + async_aborted = rc_client_end_async(client, &load_state->async_handle); + if (async_aborted) { + if (async_aborted != RC_CLIENT_ASYNC_DESTROYED) { + RC_CLIENT_LOG_VERBOSE(client, "Load aborted during external loading"); + } + + rc_client_unload_game(client); /* unload the game from the external client */ + rc_client_free_load_state(load_state); + return; + } + + if (result != RC_OK) { + rc_client_load_error(load_state, result, error_message); + return; + } + + rc_mutex_lock(&client->state.mutex); + load_state->progress = (client->state.load == load_state) ? + RC_CLIENT_LOAD_GAME_STATE_DONE : RC_CLIENT_LOAD_GAME_STATE_ABORTED; + client->state.load = NULL; + rc_mutex_unlock(&client->state.mutex); + + if (load_state->progress != RC_CLIENT_LOAD_GAME_STATE_DONE) { + /* previous load state was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else { + /* keep partial game object for media_hash management */ + if (client->state.external_client && client->state.external_client->get_game_info) { + const rc_client_game_t* info = client->state.external_client->get_game_info(); + load_state->game->public_.console_id = info->console_id; + client->game = load_state->game; + load_state->game = NULL; + } + + if (load_state->callback) + load_state->callback(RC_OK, NULL, client, load_state->callback_userdata); + } + + rc_client_free_load_state(load_state); +} + +#endif + +static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state) { - rc_api_fetch_game_data_request_t fetch_game_data_request; rc_client_t* client = load_state->client; - rc_api_request_t request; - int result; if (load_state->hash->game_id == 0) { #ifdef RC_CLIENT_SUPPORTS_HASH @@ -2058,20 +2148,35 @@ static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) #endif /* RC_CLIENT_SUPPORTS_HASH */ if (load_state->hash->game_id == 0) { - rc_client_subset_info_t* subset; +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client) { + if (client->state.external_client->load_unknown_game) { + client->state.external_client->load_unknown_game(load_state->game->public_.hash); + rc_client_load_error(load_state, RC_NO_GAME_LOADED, "Unknown game"); + return; + } + /* no external method specifically for unknown game, just pass the hash through to begin_load_game below */ + } + else { +#endif + /* mimics rc_client_load_unknown_game without allocating a new game object */ + rc_client_subset_info_t* subset; - subset = (rc_client_subset_info_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_subset_info_t)); - memset(subset, 0, sizeof(*subset)); - subset->public_.title = ""; + subset = (rc_client_subset_info_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_subset_info_t)); + memset(subset, 0, sizeof(*subset)); + subset->public_.title = ""; - load_state->game->public_.title = "Unknown Game"; - load_state->game->public_.badge_name = ""; - load_state->game->subsets = subset; - client->game = load_state->game; - load_state->game = NULL; + load_state->game->public_.title = "Unknown Game"; + load_state->game->public_.badge_name = ""; + load_state->game->subsets = subset; + client->game = load_state->game; + load_state->game = NULL; - rc_client_load_error(load_state, RC_NO_GAME_LOADED, "Unknown game"); - return; + rc_client_load_error(load_state, RC_NO_GAME_LOADED, "Unknown game"); + return; +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + } +#endif } } @@ -2083,6 +2188,60 @@ static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) /* done with the hashing code, release the global pointer */ g_hash_client = NULL; +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client) { + if (client->state.external_client->add_game_hash) + client->state.external_client->add_game_hash(load_state->hash->hash, load_state->hash->game_id); + + if (client->state.external_client->begin_load_game) { + rc_client_begin_async(client, &load_state->async_handle); + client->state.external_client->begin_load_game(client, load_state->hash->hash, rc_client_external_load_state_callback, load_state); + } + return; + } +#endif + + rc_client_begin_fetch_game_data(load_state); +} + +void rc_client_load_unknown_game(rc_client_t* client, const char* tried_hashes) +{ + rc_client_subset_info_t* subset; + rc_client_game_info_t* game; + + game = rc_client_allocate_game(); + if (!game) + return; + + subset = (rc_client_subset_info_t*)rc_buffer_alloc(&game->buffer, sizeof(rc_client_subset_info_t)); + memset(subset, 0, sizeof(*subset)); + subset->public_.title = ""; + game->subsets = subset; + + game->public_.title = "Unknown Game"; + game->public_.badge_name = ""; + game->public_.console_id = RC_CONSOLE_UNKNOWN; + + if (strlen(tried_hashes) == 32) { /* only one hash tried, add it to the list */ + rc_client_game_hash_t* game_hash = rc_client_find_game_hash(client, tried_hashes); + game_hash->game_id = 0; + game->public_.hash = game_hash->hash; + } + else { + game->public_.hash = rc_buffer_strcpy(&game->buffer, tried_hashes); + } + + rc_client_unload_game(client); + client->game = game; +} + +static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) +{ + rc_api_fetch_game_data_request_t fetch_game_data_request; + rc_client_t* client = load_state->client; + rc_api_request_t request; + int result; + rc_mutex_lock(&client->state.mutex); result = client->state.user; if (result == RC_CLIENT_USER_STATE_LOGIN_REQUESTED) @@ -2160,7 +2319,7 @@ static void rc_client_identify_game_callback(const rc_api_server_response_t* ser /* previous load state was aborted, load_state was free'd */ } else { - rc_client_begin_fetch_game_data(load_state); + rc_client_process_resolved_hash(load_state); } } @@ -2193,35 +2352,25 @@ rc_client_game_hash_t* rc_client_find_game_hash(rc_client_t* client, const char* return game_hash; } +void rc_client_add_game_hash(rc_client_t* client, const char* hash, uint32_t game_id) +{ + /* store locally, even if passing to external client */ + rc_client_game_hash_t* game_hash = rc_client_find_game_hash(client, hash); + game_hash->game_id = game_id; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->add_game_hash) + client->state.external_client->add_game_hash(hash, game_id); +#endif +} + static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, const char* hash, const char* file_path) { rc_client_t* client = load_state->client; rc_client_game_hash_t* old_hash; - if (client->state.load == NULL) { - rc_client_unload_game(client); - client->state.load = load_state; - - if (load_state->game == NULL) { - load_state->game = (rc_client_game_info_t*)calloc(1, sizeof(*load_state->game)); - if (!load_state->game) { - if (load_state->callback) - load_state->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, load_state->callback_userdata); - - rc_client_free_load_state(load_state); - return NULL; - } - - rc_buffer_init(&load_state->game->buffer); - rc_runtime_init(&load_state->game->runtime); - } - } - else if (client->state.load != load_state) { - /* previous load was aborted */ - if (load_state->callback) - load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); - + if (!rc_client_attach_load_state(client, load_state)) { rc_client_free_load_state(load_state); return NULL; } @@ -2265,7 +2414,7 @@ static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* loa else { RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); - rc_client_begin_fetch_game_data(load_state); + rc_client_process_resolved_hash(load_state); } return (client->state.load == load_state) ? &load_state->async_handle : NULL; @@ -2327,8 +2476,12 @@ rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* cl } #ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client->state.external_client && client->state.external_client->begin_identify_and_load_game) - return client->state.external_client->begin_identify_and_load_game(client, console_id, file_path, data, data_size, callback, callback_userdata); + /* if a add_game_hash handler exists, do the identification locally, then pass the + * resulting game_id/hash to the external client */ + if (client->state.external_client && !client->state.external_client->add_game_hash) { + if (client->state.external_client->begin_identify_and_load_game) + return client->state.external_client->begin_identify_and_load_game(client, console_id, file_path, data, data_size, callback, callback_userdata); + } #endif if (data) { @@ -2471,6 +2624,13 @@ void rc_client_unload_game(rc_client_t* client) #ifdef RC_CLIENT_SUPPORTS_EXTERNAL if (client->state.external_client && client->state.external_client->unload_game) { client->state.external_client->unload_game(); + + /* a game object may have been allocated to manage hashes */ + game = client->game; + client->game = NULL; + if (game != NULL) + rc_client_free_game(game); + return; } #endif @@ -2629,15 +2789,77 @@ static rc_client_async_handle_t* rc_client_begin_change_media_internal(rc_client return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL; } +static rc_client_game_info_t* rc_client_check_pending_media(rc_client_t* client, const rc_client_pending_media_t* media) +{ + rc_client_game_info_t* game; + rc_client_pending_media_t* pending_media = NULL; + + rc_mutex_lock(&client->state.mutex); + if (client->state.load) { + game = client->state.load->game; + if (!game || game->public_.console_id == 0) { + /* still waiting for game data */ + pending_media = client->state.load->pending_media; + if (pending_media) + rc_client_free_pending_media(pending_media); + + pending_media = (rc_client_pending_media_t*)malloc(sizeof(*pending_media)); + if (!pending_media) { + rc_mutex_unlock(&client->state.mutex); + media->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, media->callback_userdata); + return NULL; + } + + memcpy(pending_media, media, sizeof(*pending_media)); + if (media->hash) + pending_media->hash = strdup(media->hash); + +#ifdef RC_CLIENT_SUPPORTS_HASH + if (media->file_path) + pending_media->file_path = strdup(media->file_path); + + if (media->data && media->data_size) { + pending_media->data = (uint8_t*)malloc(media->data_size); + if (!pending_media->data) { + rc_mutex_unlock(&client->state.mutex); + media->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, media->callback_userdata); + return NULL; + } + memcpy(pending_media->data, media->data, media->data_size); + } else { + pending_media->data = NULL; + } +#endif + + client->state.load->pending_media = pending_media; + } + } + else { + game = client->game; + } + rc_mutex_unlock(&client->state.mutex); + + if (!game) { + media->callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, media->callback_userdata); + return NULL; + } + + /* still waiting for game data - don't call callback - it's queued */ + if (pending_media) + return NULL; + + return game; +} + #ifdef RC_CLIENT_SUPPORTS_HASH rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, const char* file_path, const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata) { + rc_client_pending_media_t media; rc_client_game_hash_t* game_hash = NULL; - rc_client_media_hash_t* media_hash; rc_client_game_info_t* game; - rc_client_pending_media_t* pending_media = NULL; + rc_client_media_hash_t* media_hash; uint32_t path_djb2; if (!client) { @@ -2651,55 +2873,21 @@ rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, cons } #ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client->state.external_client && client->state.external_client->begin_change_media) - return client->state.external_client->begin_change_media(client, file_path, data, data_size, callback, callback_userdata); + if (client->state.external_client && !client->state.external_client->begin_change_media_from_hash) { + if (client->state.external_client->begin_change_media) + return client->state.external_client->begin_change_media(client, file_path, data, data_size, callback, callback_userdata); + } #endif - rc_mutex_lock(&client->state.mutex); - if (client->state.load) { - game = client->state.load->game; - if (game->public_.console_id == 0) { - /* still waiting for game data */ - pending_media = client->state.load->pending_media; - if (pending_media) - rc_client_free_pending_media(pending_media); + memset(&media, 0, sizeof(media)); + media.file_path = file_path; + media.data = (uint8_t*)data; + media.data_size = data_size; + media.callback = callback; + media.callback_userdata = callback_userdata; - pending_media = (rc_client_pending_media_t*)calloc(1, sizeof(*pending_media)); - if (!pending_media) { - rc_mutex_unlock(&client->state.mutex); - callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); - return NULL; - } - - pending_media->file_path = strdup(file_path); - pending_media->callback = callback; - pending_media->callback_userdata = callback_userdata; - if (data && data_size) { - pending_media->data_size = data_size; - pending_media->data = (uint8_t*)malloc(data_size); - if (!pending_media->data) { - rc_mutex_unlock(&client->state.mutex); - callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); - return NULL; - } - memcpy(pending_media->data, data, data_size); - } - - client->state.load->pending_media = pending_media; - } - } - else { - game = client->game; - } - rc_mutex_unlock(&client->state.mutex); - - if (!game) { - callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata); - return NULL; - } - - /* still waiting for game data */ - if (pending_media) + game = rc_client_check_pending_media(client, &media); + if (game == NULL) return NULL; /* check to see if we've already hashed this file */ @@ -2749,11 +2937,25 @@ rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, cons rc_mutex_unlock(&client->state.mutex); if (!result) { +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->begin_change_media_from_hash) + return client->state.external_client->begin_change_media_from_hash(client, game_hash->hash, callback, callback_userdata); +#endif + rc_client_change_media_internal(client, game_hash, callback, callback_userdata); return NULL; } } +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client) { + if (client->state.external_client->add_game_hash) + client->state.external_client->add_game_hash(game_hash->hash, game_hash->game_id); + if (client->state.external_client->begin_change_media_from_hash) + return client->state.external_client->begin_change_media_from_hash(client, game_hash->hash, callback, callback_userdata); + } +#endif + return rc_client_begin_change_media_internal(client, game, game_hash, callback, callback_userdata); } @@ -2762,9 +2964,9 @@ rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, cons rc_client_async_handle_t* rc_client_begin_change_media_from_hash(rc_client_t* client, const char* hash, rc_client_callback_t callback, void* callback_userdata) { + rc_client_pending_media_t media; rc_client_game_hash_t* game_hash; rc_client_game_info_t* game; - rc_client_pending_media_t* pending_media = NULL; if (!client) { callback(RC_INVALID_STATE, "client is required", client, callback_userdata); @@ -2782,40 +2984,13 @@ rc_client_async_handle_t* rc_client_begin_change_media_from_hash(rc_client_t* cl } #endif - rc_mutex_lock(&client->state.mutex); - if (client->state.load) { - game = client->state.load->game; - if (game->public_.console_id == 0) { - /* still waiting for game data */ - pending_media = client->state.load->pending_media; - if (pending_media) - rc_client_free_pending_media(pending_media); + memset(&media, 0, sizeof(media)); + media.hash = hash; + media.callback = callback; + media.callback_userdata = callback_userdata; - pending_media = (rc_client_pending_media_t*)calloc(1, sizeof(*pending_media)); - if (!pending_media) { - rc_mutex_unlock(&client->state.mutex); - callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); - return NULL; - } - - pending_media->hash = strdup(hash); - pending_media->callback = callback; - pending_media->callback_userdata = callback_userdata; - - client->state.load->pending_media = pending_media; - } - } else { - game = client->game; - } - rc_mutex_unlock(&client->state.mutex); - - if (!game) { - callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata); - return NULL; - } - - /* still waiting for game data */ - if (pending_media) + game = rc_client_check_pending_media(client, &media); + if (game == NULL) return NULL; /* check to see if we've already hashed this file. */ @@ -2882,7 +3057,7 @@ rc_client_async_handle_t* rc_client_begin_load_subset(rc_client_t* client, uint3 load_state->hash->game_id = subset_id; client->state.load = load_state; - rc_client_begin_fetch_game_data(load_state); + rc_client_process_resolved_hash(load_state); return (client->state.load == load_state) ? &load_state->async_handle : NULL; } @@ -3570,9 +3745,11 @@ static void rc_client_award_achievement(rc_client_t* client, rc_client_achieveme callback_data->client = client; callback_data->id = achievement->public_.id; callback_data->hardcore = client->state.hardcore; - callback_data->game_hash = client->game->public_.hash; callback_data->unlock_time = achievement->public_.unlock_time; + if (client->game) /* may be NULL if this gets called while unloading the game (from another thread - events are raised outside the lock) */ + callback_data->game_hash = client->game->public_.hash; + RC_CLIENT_LOG_INFO_FORMATTED(client, "Awarding achievement %u: %s", achievement->public_.id, achievement->public_.title); rc_client_award_achievement_server_call(callback_data); } @@ -4311,6 +4488,7 @@ static void rc_client_fetch_leaderboard_entries_callback(const rc_api_server_res } list->num_entries = lbinfo_response.num_entries; + list->total_entries = lbinfo_response.total_entries; lbinfo_callback_data->callback(RC_OK, NULL, list, client, lbinfo_callback_data->callback_userdata); } @@ -4797,6 +4975,8 @@ static void rc_client_progress_tracker_timer_elapsed(rc_client_scheduled_callbac static void rc_client_do_frame_update_progress_tracker(rc_client_t* client, rc_client_game_info_t* game) { + /* ASSERT: this should only be called if the mutex is held */ + if (!game->progress_tracker.hide_callback) { game->progress_tracker.hide_callback = (rc_client_scheduled_callback_data_t*) rc_buffer_alloc(&game->buffer, sizeof(rc_client_scheduled_callback_data_t)); @@ -5181,6 +5361,7 @@ void rc_client_idle(rc_client_t* client) else { /* remove the callback from the queue while we process it. callback can requeue if desired */ client->state.scheduled_callbacks = scheduled_callback->next; + scheduled_callback->next = NULL; } } rc_mutex_unlock(&client->state.mutex); @@ -5256,7 +5437,7 @@ static void rc_client_reschedule_callback(rc_client_t* client, continue; } - if (!next || when < next->when) { + if (!next || (when < next->when && when != 0)) { /* insert here */ callback->next = next; *last = callback; diff --git a/3rdparty/rcheevos/src/rc_client_external.h b/3rdparty/rcheevos/src/rc_client_external.h index 82ec1d49d7..520a883e13 100644 --- a/3rdparty/rcheevos/src/rc_client_external.h +++ b/3rdparty/rcheevos/src/rc_client_external.h @@ -38,6 +38,7 @@ typedef const rc_client_subset_t* (RC_CCONV *rc_client_external_get_subset_info_ typedef void (RC_CCONV *rc_client_external_get_user_game_summary_func_t)(rc_client_user_game_summary_t* summary); typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_change_media_func_t)(rc_client_t* client, const char* file_path, const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata); +typedef void (RC_CCONV* rc_client_external_add_game_hash_func_t)(const char* hash, uint32_t game_id); /* NOTE: rc_client_external_create_achievement_list_func_t returns an internal wrapper structure which contains the public list * and a destructor function. */ @@ -124,9 +125,16 @@ typedef struct rc_client_external_t rc_client_external_serialize_progress_func_t serialize_progress; rc_client_external_deserialize_progress_func_t deserialize_progress; + /* VERSION 2 */ + rc_client_external_add_game_hash_func_t add_game_hash; + rc_client_external_set_string_func_t load_unknown_game; + } rc_client_external_t; -#define RC_CLIENT_EXTERNAL_VERSION 1 +#define RC_CLIENT_EXTERNAL_VERSION 2 + +void rc_client_add_game_hash(rc_client_t* client, const char* hash, uint32_t game_id); +void rc_client_load_unknown_game(rc_client_t* client, const char* hash); RC_END_C_DECLS diff --git a/3rdparty/rcheevos/src/rc_version.h b/3rdparty/rcheevos/src/rc_version.h index 3e337b9a24..7ed7301d27 100644 --- a/3rdparty/rcheevos/src/rc_version.h +++ b/3rdparty/rcheevos/src/rc_version.h @@ -8,7 +8,7 @@ RC_BEGIN_C_DECLS #define RCHEEVOS_VERSION_MAJOR 11 -#define RCHEEVOS_VERSION_MINOR 4 +#define RCHEEVOS_VERSION_MINOR 5 #define RCHEEVOS_VERSION_PATCH 0 #define RCHEEVOS_MAKE_VERSION(major, minor, patch) (major * 1000000 + minor * 1000 + patch) diff --git a/3rdparty/rcheevos/src/rcheevos/runtime_progress.c b/3rdparty/rcheevos/src/rcheevos/runtime_progress.c index 3e34d7da74..629f0e376f 100644 --- a/3rdparty/rcheevos/src/rcheevos/runtime_progress.c +++ b/3rdparty/rcheevos/src/rcheevos/runtime_progress.c @@ -809,7 +809,7 @@ uint32_t rc_runtime_progress_size(const rc_runtime_t* runtime, lua_State* L) result = rc_runtime_progress_serialize_internal(&progress); if (result != RC_OK) - return 0; + return result; return progress.offset; } diff --git a/3rdparty/rcheevos/src/rhash/hash.c b/3rdparty/rcheevos/src/rhash/hash.c index 5e27d973f4..9fc32f6324 100644 --- a/3rdparty/rcheevos/src/rhash/hash.c +++ b/3rdparty/rcheevos/src/rhash/hash.c @@ -700,6 +700,12 @@ struct rc_hash_zip_idx uint8_t* data; }; +struct rc_hash_ms_dos_dosz_state +{ + const char* path; + const struct rc_hash_ms_dos_dosz_state* child; +}; + static int rc_hash_zip_idx_sort(const void* a, const void* b) { struct rc_hash_zip_idx *A = (struct rc_hash_zip_idx*)a, *B = (struct rc_hash_zip_idx*)b; @@ -707,9 +713,12 @@ static int rc_hash_zip_idx_sort(const void* a, const void* b) return memcmp(A->data, B->data, len); } -static int rc_hash_zip_file(md5_state_t* md5, void* file_handle) +static int rc_hash_ms_dos_parent(md5_state_t* md5, const struct rc_hash_ms_dos_dosz_state *child, const char* parentname, uint32_t parentname_len); +static int rc_hash_ms_dos_dosc(md5_state_t* md5, const struct rc_hash_ms_dos_dosz_state *dosz); + +static int rc_hash_zip_file(md5_state_t* md5, void* file_handle, const struct rc_hash_ms_dos_dosz_state* dosz) { - uint8_t buf[2048], *alloc_buf, *cdir_start, *cdir_max, *cdir, *hashdata, eocdirhdr_size, cdirhdr_size; + uint8_t buf[2048], *alloc_buf, *cdir_start, *cdir_max, *cdir, *hashdata, eocdirhdr_size, cdirhdr_size, nparents; uint32_t cdir_entry_len; size_t sizeof_idx, indices_offset, alloc_size; int64_t i_file, archive_size, ecdh_ofs, total_files, cdir_size, cdir_ofs; @@ -773,7 +782,7 @@ static int rc_hash_zip_file(md5_state_t* md5, void* file_handle) rc_file_seek(file_handle, ecdh_ofs - 20, SEEK_SET); if (rc_file_read(file_handle, buf, 20) == 20 && RC_ZIP_READ_LE32(buf) == 0x07064b50) /* locator signature */ { - /* Found the locator, now read the actual ZIP64 end of central directory header */ + /* Found the locator, now read the actual ZIP64 end of central directory header */ int64_t ecdh64_ofs = (int64_t)RC_ZIP_READ_LE64(buf + 0x08); if (ecdh64_ofs <= (archive_size - 56)) { @@ -821,7 +830,7 @@ static int rc_hash_zip_file(md5_state_t* md5, void* file_handle) hashindex = hashindices; /* Now process the central directory file records */ - for (i_file = 0, cdir = cdir_start; i_file < total_files && cdir >= cdir_start && cdir <= cdir_max; i_file++, cdir += cdir_entry_len) + for (i_file = nparents = 0, cdir = cdir_start; i_file < total_files && cdir >= cdir_start && cdir <= cdir_max; i_file++, cdir += cdir_entry_len) { const uint8_t *name, *name_end; uint32_t signature = RC_ZIP_READ_LE32(cdir + 0x00); @@ -891,6 +900,27 @@ static int rc_hash_zip_file(md5_state_t* md5, void* file_handle) return rc_hash_error("Encountered invalid entry in ZIP central directory"); } + /* A DOSZ file can contain a special empty .dosz.parent file in its root which means a parent dosz file is used */ + if (dosz && decomp_size == 0 && filename_len > 7 && !strncasecmp((const char*)name + filename_len - 7, ".parent", 7) && !memchr(name, '/', filename_len) && !memchr(name, '\\', filename_len)) + { + /* A DOSZ file can only have one parent file */ + if (nparents++) + { + free(alloc_buf); + return rc_hash_error("Invalid DOSZ file with multiple parents"); + } + + /* If there is an error with the parent DOSZ, abort now */ + if (!rc_hash_ms_dos_parent(md5, dosz, (const char*)name, (filename_len - 7))) + { + free(alloc_buf); + return 0; + } + + /* We don't hash this meta file so a user is free to rename it and the parent file */ + continue; + } + /* Write the pointer and length of the data we record about this file */ hashindex->data = hashdata; hashindex->length = filename_len + 1 + 4 + 8; @@ -935,6 +965,11 @@ static int rc_hash_zip_file(md5_state_t* md5, void* file_handle) md5_append(md5, hashindices->data, (int)hashindices->length); free(alloc_buf); + + /* If this is a .dosz file, check if an associated .dosc file exists */ + if (dosz && !rc_hash_ms_dos_dosc(md5, dosz)) + return 0; + return 1; #undef RC_ZIP_READ_LE16 @@ -944,10 +979,86 @@ static int rc_hash_zip_file(md5_state_t* md5, void* file_handle) #undef RC_ZIP_WRITE_LE64 } +static int rc_hash_ms_dos_parent(md5_state_t* md5, const struct rc_hash_ms_dos_dosz_state *child, const char* parentname, uint32_t parentname_len) +{ + const char *lastfslash = strrchr(child->path, '/'); + const char *lastbslash = strrchr(child->path, '\\'); + const char *lastslash = (lastbslash > lastfslash ? lastbslash : lastfslash); + size_t dir_len = (lastslash ? (lastslash + 1 - child->path) : 0); + char* parent_path = (char*)malloc(dir_len + parentname_len + 1); + struct rc_hash_ms_dos_dosz_state parent; + const struct rc_hash_ms_dos_dosz_state *check; + void* parent_handle; + int parent_res; + + /* Build the path of the parent by combining the directory of the current file with the name */ + if (!parent_path) + return rc_hash_error("Could not allocate temporary buffer"); + + memcpy(parent_path, child->path, dir_len); + memcpy(parent_path + dir_len, parentname, parentname_len); + parent_path[dir_len + parentname_len] = '\0'; + + /* Make sure there is no recursion where a parent DOSZ is an already seen child DOSZ */ + for (check = child->child; check; check = check->child) + { + if (!strcmp(check->path, parent_path)) + { + free(parent_path); + return rc_hash_error("Invalid DOSZ file with recursive parents"); + } + } + + /* Try to open the parent DOSZ file */ + parent_handle = rc_file_open(parent_path); + if (!parent_handle) + { + char message[1024]; + snprintf(message, sizeof(message), "DOSZ parent file '%s' does not exist", parent_path); + free(parent_path); + return rc_hash_error(message); + } + + /* Fully hash the parent DOSZ ahead of the child */ + parent.path = parent_path; + parent.child = child; + parent_res = rc_hash_zip_file(md5, parent_handle, &parent); + rc_file_close(parent_handle); + free(parent_path); + return parent_res; +} + +static int rc_hash_ms_dos_dosc(md5_state_t* md5, const struct rc_hash_ms_dos_dosz_state *dosz) +{ + size_t path_len = strlen(dosz->path); + if (dosz->path[path_len-1] == 'z' || dosz->path[path_len-1] == 'Z') + { + void* file_handle; + char *dosc_path = strdup(dosz->path); + if (!dosc_path) + return rc_hash_error("Could not allocate temporary buffer"); + + /* Swap the z to c and use the same capitalization, hash the file if it exists */ + dosc_path[path_len-1] = (dosz->path[path_len-1] == 'z' ? 'c' : 'C'); + file_handle = rc_file_open(dosc_path); + free(dosc_path); + + if (file_handle) + { + /* Hash the DOSC as a plain zip file (pass NULL as dosz state) */ + int res = rc_hash_zip_file(md5, file_handle, NULL); + rc_file_close(file_handle); + if (!res) + return 0; + } + } + return 1; +} + static int rc_hash_ms_dos(char hash[33], const char* path) { + struct rc_hash_ms_dos_dosz_state dosz; md5_state_t md5; - size_t path_len; int res; void* file_handle = rc_file_open(path); @@ -956,34 +1067,14 @@ static int rc_hash_ms_dos(char hash[33], const char* path) /* hash the main content zip file first */ md5_init(&md5); - res = rc_hash_zip_file(&md5, file_handle); + dosz.path = path; + dosz.child = NULL; + res = rc_hash_zip_file(&md5, file_handle, &dosz); rc_file_close(file_handle); if (!res) return 0; - /* if this is a .dosz file, check if an associated .dosc file exists */ - path_len = strlen(path); - if (path[path_len-1] == 'z' || path[path_len-1] == 'Z') - { - char *dosc_path = strdup(path); - if (!dosc_path) - return rc_hash_error("Could not allocate temporary buffer"); - - /* swap the z to c and use the same capitalization, hash the file if it exists*/ - dosc_path[path_len-1] = (path[path_len-1] == 'z' ? 'c' : 'C'); - file_handle = rc_file_open(dosc_path); - free((void*)dosc_path); - if (file_handle) - { - res = rc_hash_zip_file(&md5, file_handle); - rc_file_close(file_handle); - - if (!res) - return 0; - } - } - return rc_hash_finalize(&md5, hash); } @@ -1463,6 +1554,619 @@ static int rc_hash_n64(char hash[33], const char* path) return rc_hash_finalize(&md5, hash); } +static int rc_hash_nintendo_3ds_ncch(md5_state_t* md5, void* file_handle, uint8_t header[0x200], struct AES_ctx* cia_aes) +{ + struct AES_ctx ncch_aes; + uint8_t* hash_buffer; + uint64_t exefs_offset, exefs_real_size; + uint32_t exefs_buffer_size; + uint8_t primary_key[AES_KEYLEN], secondary_key[AES_KEYLEN]; + uint8_t fixed_key_flag, no_crypto_flag, seed_crypto_flag; + uint8_t crypto_method, secondary_key_x_slot; + uint16_t ncch_version; + uint32_t i; + uint8_t primary_key_y[AES_KEYLEN], program_id[sizeof(uint64_t)]; + uint8_t iv[AES_BLOCKLEN]; + uint8_t exefs_section_name[8]; + uint64_t exefs_section_offset, exefs_section_size; + + exefs_offset = ((uint32_t)header[0x1A3] << 24) | (header[0x1A2] << 16) | (header[0x1A1] << 8) | header[0x1A0]; + exefs_real_size = ((uint32_t)header[0x1A7] << 24) | (header[0x1A6] << 16) | (header[0x1A5] << 8) | header[0x1A4]; + + /* Offset and size are in "media units" (1 media unit = 0x200 bytes) */ + exefs_offset *= 0x200; + exefs_real_size *= 0x200; + + if (exefs_real_size > MAX_BUFFER_SIZE) + exefs_buffer_size = MAX_BUFFER_SIZE; + else + exefs_buffer_size = (uint32_t)exefs_real_size; + + /* This region is technically optional, but it should always be present for executable content (i.e. games) */ + if (exefs_offset == 0 || exefs_real_size == 0) + return rc_hash_error("ExeFS was not available"); + + /* NCCH flag 7 is a bitfield of various crypto related flags */ + fixed_key_flag = header[0x188 + 7] & 0x01; + no_crypto_flag = header[0x188 + 7] & 0x04; + seed_crypto_flag = header[0x188 + 7] & 0x20; + + ncch_version = (header[0x113] << 8) | header[0x112]; + + if (no_crypto_flag == 0) + { + rc_hash_verbose("Encrypted NCCH detected"); + + if (fixed_key_flag != 0) + { + /* Fixed crypto key means all 0s for both keys */ + memset(primary_key, 0, sizeof(primary_key)); + memset(secondary_key, 0, sizeof(secondary_key)); + rc_hash_verbose("Using fixed key crypto"); + } + else + { + if (_3ds_get_ncch_normal_keys_func == NULL) + return rc_hash_error("An encrypted NCCH was detected, but the NCCH normal keys callback was not set"); + + /* Primary key y is just the first 16 bytes of the header */ + memcpy(primary_key_y, header, sizeof(primary_key_y)); + + /* NCCH flag 3 indicates which secondary key x slot is used */ + crypto_method = header[0x188 + 3]; + + switch (crypto_method) + { + case 0x00: + rc_hash_verbose("Using NCCH crypto method v1"); + secondary_key_x_slot = 0x2C; + break; + case 0x01: + rc_hash_verbose("Using NCCH crypto method v2"); + secondary_key_x_slot = 0x25; + break; + case 0x0A: + rc_hash_verbose("Using NCCH crypto method v3"); + secondary_key_x_slot = 0x18; + break; + case 0x0B: + rc_hash_verbose("Using NCCH crypto method v4"); + secondary_key_x_slot = 0x1B; + break; + default: + snprintf((char*)header, 0x200, "Invalid crypto method %02X", (unsigned)crypto_method); + return rc_hash_error((const char*)header); + } + + /* We only need the program id if we're doing seed crypto */ + if (seed_crypto_flag != 0) + { + rc_hash_verbose("Using seed crypto"); + memcpy(program_id, &header[0x118], sizeof(program_id)); + } + + if (_3ds_get_ncch_normal_keys_func(primary_key_y, secondary_key_x_slot, seed_crypto_flag != 0 ? program_id : NULL, primary_key, secondary_key) == 0) + return rc_hash_error("Could not obtain NCCH normal keys"); + } + + switch (ncch_version) + { + case 0: + case 2: + rc_hash_verbose("Detected NCCH version 0/2"); + for (i = 0; i < 8; i++) + { + /* First 8 bytes is the partition id in reverse byte order */ + iv[7 - i] = header[0x108 + i]; + } + + /* Magic number for ExeFS */ + iv[8] = 2; + + /* Rest of the bytes are 0 */ + memset(&iv[9], 0, sizeof(iv) - 9); + break; + case 1: + rc_hash_verbose("Detected NCCH version 1"); + for (i = 0; i < 8; i++) + { + /* First 8 bytes is the partition id in normal byte order */ + iv[i] = header[0x108 + i]; + } + + /* Next 4 bytes are 0 */ + memset(&iv[8], 0, 4); + + /* Last 4 bytes is the ExeFS byte offset in big endian */ + iv[12] = (exefs_offset >> 24) & 0xFF; + iv[13] = (exefs_offset >> 16) & 0xFF; + iv[14] = (exefs_offset >> 8) & 0xFF; + iv[15] = exefs_offset & 0xFF; + break; + default: + snprintf((char*)header, 0x200, "Invalid NCCH version %04X", (unsigned)ncch_version); + return rc_hash_error((const char*)header); + } + } + + /* ASSERT: file position must be +0x200 from start of NCCH (i.e. end of header) */ + exefs_offset -= 0x200; + + if (cia_aes) + { + /* We have to decrypt the data between the header and the ExeFS so the CIA AES state is correct + * when we reach the ExeFS. This decrypted data is not included in the RetroAchievements hash */ + + /* This should never happen in practice, but just in case */ + if (exefs_offset > MAX_BUFFER_SIZE) + return rc_hash_error("Too much data required to decrypt in order to hash"); + + hash_buffer = (uint8_t*)malloc((uint32_t)exefs_offset); + if (!hash_buffer) + { + snprintf((char*)header, 0x200, "Failed to allocate %u bytes", (unsigned)exefs_offset); + return rc_hash_error((const char*)header); + } + + if (rc_file_read(file_handle, hash_buffer, (uint32_t)exefs_offset) != (uint32_t)exefs_offset) + { + free(hash_buffer); + return rc_hash_error("Could not read NCCH data"); + } + + AES_CBC_decrypt_buffer(cia_aes, hash_buffer, (uint32_t)exefs_offset); + free(hash_buffer); + } + else + { + /* No decryption needed, just skip over the in-between data */ + rc_file_seek(file_handle, (int64_t)exefs_offset, SEEK_CUR); + } + + hash_buffer = (uint8_t*)malloc(exefs_buffer_size); + if (!hash_buffer) + { + snprintf((char*)header, 0x200, "Failed to allocate %u bytes", (unsigned)exefs_buffer_size); + return rc_hash_error((const char*)header); + } + + /* Clear out crypto flags to ensure we get the same hash for decrypted and encrypted ROMs */ + memset(&header[0x114], 0, 4); + header[0x188 + 3] = 0; + header[0x188 + 7] &= ~(0x20 | 0x04 | 0x01); + + rc_hash_verbose("Hashing 512 byte NCCH header"); + md5_append(md5, header, 0x200); + + if (verbose_message_callback) + { + snprintf((char*)header, 0x200, "Hashing %u bytes for ExeFS (at NCCH offset %08X%08X)", (unsigned)exefs_buffer_size, (unsigned)(exefs_offset >> 32), (unsigned)exefs_offset); + verbose_message_callback((const char*)header); + } + + if (rc_file_read(file_handle, hash_buffer, exefs_buffer_size) != exefs_buffer_size) + { + free(hash_buffer); + return rc_hash_error("Could not read ExeFS data"); + } + + if (cia_aes) + { + rc_hash_verbose("Performing CIA decryption for ExeFS"); + AES_CBC_decrypt_buffer(cia_aes, hash_buffer, exefs_buffer_size); + } + + if (no_crypto_flag == 0) + { + rc_hash_verbose("Performing NCCH decryption for ExeFS"); + + AES_init_ctx_iv(&ncch_aes, primary_key, iv); + AES_CTR_xcrypt_buffer(&ncch_aes, hash_buffer, 0x200); + + for (i = 0; i < 8; i++) + { + memcpy(exefs_section_name, &hash_buffer[i * 16], sizeof(exefs_section_name)); + exefs_section_offset = ((uint32_t)hash_buffer[i * 16 + 11] << 24) | (hash_buffer[i * 16 + 10] << 16) | (hash_buffer[i * 16 + 9] << 8) | hash_buffer[i * 16 + 8]; + exefs_section_size = ((uint32_t)hash_buffer[i * 16 + 15] << 24) | (hash_buffer[i * 16 + 14] << 16) | (hash_buffer[i * 16 + 13] << 8) | hash_buffer[i * 16 + 12]; + + /* 0 size indicates an unused section */ + if (exefs_section_size == 0) + continue; + + /* Offsets must be aligned by a media unit */ + if (exefs_section_offset & 0x1FF) + return rc_hash_error("ExeFS section offset is misaligned"); + + /* Offset is relative to the end of the header */ + exefs_section_offset += 0x200; + + /* Check against malformed sections */ + if (exefs_section_offset + ((exefs_section_size + 0x1FF) & ~(uint64_t)0x1FF) > (uint64_t)exefs_real_size) + return rc_hash_error("ExeFS section would overflow"); + + if (memcmp(exefs_section_name, "icon", 4) == 0 || memcmp(exefs_section_name, "banner", 6) == 0) + { + /* Align size up by a media unit */ + exefs_section_size = (exefs_section_size + 0x1FF) & ~(uint64_t)0x1FF; + AES_init_ctx(&ncch_aes, primary_key); + } + else + { + /* We don't align size up here, as the padding bytes will use the primary key rather than the secondary key */ + AES_init_ctx(&ncch_aes, secondary_key); + } + + /* In theory, the section offset + size could be greater than the buffer size */ + /* In practice, this likely never occurs, but just in case it does, ignore the section or constrict the size */ + if (exefs_section_offset + exefs_section_size > exefs_buffer_size) + { + if (exefs_section_offset >= exefs_buffer_size) + continue; + + exefs_section_size = exefs_buffer_size - exefs_section_offset; + } + + if (verbose_message_callback) + { + exefs_section_name[7] = '\0'; + snprintf((char*)header, 0x200, "Decrypting ExeFS file %s at ExeFS offset %08X with size %08X", (const char*)exefs_section_name, (unsigned)exefs_section_offset, (unsigned)exefs_section_size); + verbose_message_callback((const char*)header); + } + + AES_CTR_xcrypt_buffer(&ncch_aes, &hash_buffer[exefs_section_offset], exefs_section_size & ~(uint64_t)0xF); + + if (exefs_section_size & 0x1FF) + { + /* Handle padding bytes, these always use the primary key */ + exefs_section_offset += exefs_section_size; + exefs_section_size = 0x200 - (exefs_section_size & 0x1FF); + + if (verbose_message_callback) + { + snprintf((char*)header, 0x200, "Decrypting ExeFS padding at ExeFS offset %08X with size %08X", (unsigned)exefs_section_offset, (unsigned)exefs_section_size); + verbose_message_callback((const char*)header); + } + + /* Align our decryption start to an AES block boundary */ + if (exefs_section_size & 0xF) + { + /* We're a little evil here re-using the IV like this, but this seems to be the best way to deal with this... */ + memcpy(iv, ncch_aes.Iv, sizeof(iv)); + exefs_section_offset &= ~(uint64_t)0xF; + + /* First decrypt these last bytes using the secondary key */ + AES_CTR_xcrypt_buffer(&ncch_aes, &hash_buffer[exefs_section_offset], 0x10 - (exefs_section_size & 0xF)); + + /* Now re-encrypt these bytes using the primary key */ + AES_init_ctx_iv(&ncch_aes, primary_key, iv); + AES_CTR_xcrypt_buffer(&ncch_aes, &hash_buffer[exefs_section_offset], 0x10 - (exefs_section_size & 0xF)); + + /* All of the padding can now be decrypted using the primary key */ + AES_ctx_set_iv(&ncch_aes, iv); + exefs_section_size += 0x10 - (exefs_section_size & 0xF); + } + + AES_init_ctx(&ncch_aes, primary_key); + AES_CTR_xcrypt_buffer(&ncch_aes, &hash_buffer[exefs_section_offset], (size_t)exefs_section_size); + } + } + } + + md5_append(md5, hash_buffer, exefs_buffer_size); + + free(hash_buffer); + return 1; +} + +static uint32_t rc_hash_nintendo_3ds_cia_signature_size(uint8_t header[0x200]) +{ + uint32_t signature_type; + + signature_type = ((uint32_t)header[0] << 24) | (header[1] << 16) | (header[2] << 8) | header[3]; + switch (signature_type) + { + case 0x010000: + case 0x010003: + return 0x200 + 0x3C; + case 0x010001: + case 0x010004: + return 0x100 + 0x3C; + case 0x010002: + case 0x010005: + return 0x3C + 0x40; + default: + snprintf((char*)header, 0x200, "Invalid signature type %08X", (unsigned)signature_type); + return rc_hash_error((const char*)header); + } +} + +static int rc_hash_nintendo_3ds_cia(md5_state_t* md5, void* file_handle, uint8_t header[0x200]) +{ + const uint32_t CIA_HEADER_SIZE = 0x2020; /* Yes, this is larger than the header[0x200], but we only use the beginning of the header */ + const uint64_t CIA_ALIGNMENT_MASK = 64 - 1; /* sizes are aligned by 64 bytes */ + struct AES_ctx aes; + uint8_t iv[AES_BLOCKLEN], normal_key[AES_KEYLEN], title_key[AES_KEYLEN], title_id[sizeof(uint64_t)]; + uint32_t cert_size, tik_size, tmd_size; + int64_t cert_offset, tik_offset, tmd_offset, content_offset; + uint32_t signature_size, i; + uint16_t content_count; + uint8_t common_key_index; + + cert_size = ((uint32_t)header[0x0B] << 24) | (header[0x0A] << 16) | (header[0x09] << 8) | header[0x08]; + tik_size = ((uint32_t)header[0x0F] << 24) | (header[0x0E] << 16) | (header[0x0D] << 8) | header[0x0C]; + tmd_size = ((uint32_t)header[0x13] << 24) | (header[0x12] << 16) | (header[0x11] << 8) | header[0x10]; + + cert_offset = (CIA_HEADER_SIZE + CIA_ALIGNMENT_MASK) & ~CIA_ALIGNMENT_MASK; + tik_offset = (cert_offset + cert_size + CIA_ALIGNMENT_MASK) & ~CIA_ALIGNMENT_MASK; + tmd_offset = (tik_offset + tik_size + CIA_ALIGNMENT_MASK) & ~CIA_ALIGNMENT_MASK; + content_offset = (tmd_offset + tmd_size + CIA_ALIGNMENT_MASK) & ~CIA_ALIGNMENT_MASK; + + /* Check if this CIA is encrypted, if it isn't, we can hash it right away */ + + rc_file_seek(file_handle, tmd_offset, SEEK_SET); + if (rc_file_read(file_handle, header, 4) != 4) + return rc_hash_error("Could not read TMD signature type"); + + signature_size = rc_hash_nintendo_3ds_cia_signature_size(header); + if (signature_size == 0) + return 0; /* rc_hash_nintendo_3ds_cia_signature_size will call rc_hash_error, so we don't need to do so here */ + + rc_file_seek(file_handle, signature_size + 0x9E, SEEK_CUR); + if (rc_file_read(file_handle, header, 2) != 2) + return rc_hash_error("Could not read TMD content count"); + + content_count = (header[0] << 8) | header[1]; + + rc_file_seek(file_handle, 0x9C4 - 0x9E - 2, SEEK_CUR); + for (i = 0; i < content_count; i++) + { + if (rc_file_read(file_handle, header, 0x30) != 0x30) + return rc_hash_error("Could not read TMD content chunk"); + + /* Content index 0 is the main content (i.e. the 3DS executable) */ + if (((header[4] << 8) | header[5]) == 0) + break; + + content_offset += ((uint32_t)header[0xC] << 24) | (header[0xD] << 16) | (header[0xE] << 8) | header[0xF]; + } + + if (i == content_count) + return rc_hash_error("Could not find main content chunk in TMD"); + + if ((header[7] & 1) == 0) + { + /* Not encrypted, we can hash the NCCH immediately */ + rc_file_seek(file_handle, content_offset, SEEK_SET); + if (rc_file_read(file_handle, header, 0x200) != 0x200) + return rc_hash_error("Could not read NCCH header"); + + if (memcmp(&header[0x100], "NCCH", 4) != 0) + { + snprintf((char*)header, 0x200, "NCCH header was not at %08X%08X", (unsigned)(content_offset >> 32), (unsigned)content_offset); + return rc_hash_error((const char*)header); + } + + return rc_hash_nintendo_3ds_ncch(md5, file_handle, header, NULL); + } + + if (_3ds_get_cia_normal_key_func == NULL) + return rc_hash_error("An encrypted CIA was detected, but the CIA normal key callback was not set"); + + /* Acquire the encrypted title key, title id, and common key index from the ticket */ + /* These will be needed to decrypt the title key, and that will be needed to decrypt the CIA */ + + rc_file_seek(file_handle, tik_offset, SEEK_SET); + if (rc_file_read(file_handle, header, 4) != 4) + return rc_hash_error("Could not read ticket signature type"); + + signature_size = rc_hash_nintendo_3ds_cia_signature_size(header); + if (signature_size == 0) + return 0; + + rc_file_seek(file_handle, signature_size, SEEK_CUR); + if (rc_file_read(file_handle, header, 0xB2) != 0xB2) + return rc_hash_error("Could not read ticket data"); + + memcpy(title_key, &header[0x7F], sizeof(title_key)); + memcpy(title_id, &header[0x9C], sizeof(title_id)); + common_key_index = header[0xB1]; + + if (common_key_index > 5) + { + snprintf((char*)header, 0x200, "Invalid common key index %02X", (unsigned)common_key_index); + return rc_hash_error((const char*)header); + } + + if (_3ds_get_cia_normal_key_func(common_key_index, normal_key) == 0) + { + snprintf((char*)header, 0x200, "Could not obtain common key %02X", (unsigned)common_key_index); + return rc_hash_error((const char*)header); + } + + memset(iv, 0, sizeof(iv)); + memcpy(iv, title_id, sizeof(title_id)); + AES_init_ctx_iv(&aes, normal_key, iv); + + /* Finally, decrypt the title key */ + AES_CBC_decrypt_buffer(&aes, title_key, sizeof(title_key)); + + /* Now we can hash the NCCH */ + + rc_file_seek(file_handle, content_offset, SEEK_SET); + if (rc_file_read(file_handle, header, 0x200) != 0x200) + return rc_hash_error("Could not read NCCH header"); + + memset(iv, 0, sizeof(iv)); /* Content index is iv (which is always 0 for main content) */ + AES_init_ctx_iv(&aes, title_key, iv); + AES_CBC_decrypt_buffer(&aes, header, 0x200); + + if (memcmp(&header[0x100], "NCCH", 4) != 0) + { + snprintf((char*)header, 0x200, "NCCH header was not at %08X%08X", (unsigned)(content_offset >> 32), (unsigned)content_offset); + return rc_hash_error((const char*)header); + } + + return rc_hash_nintendo_3ds_ncch(md5, file_handle, header, &aes); +} + +static int rc_hash_nintendo_3ds_3dsx(md5_state_t* md5, void* file_handle, uint8_t header[0x200]) +{ + uint8_t* hash_buffer; + uint32_t header_size, reloc_header_size, code_size; + int64_t code_offset; + + header_size = (header[5] << 8) | header[4]; + reloc_header_size = (header[7] << 8) | header[6]; + code_size = ((uint32_t)header[0x13] << 24) | (header[0x12] << 16) | (header[0x11] << 8) | header[0x10]; + + /* 3 relocation headers are in-between the 3DSX header and code segment */ + code_offset = header_size + reloc_header_size * 3; + + if (code_size > MAX_BUFFER_SIZE) + code_size = MAX_BUFFER_SIZE; + + hash_buffer = (uint8_t*)malloc(code_size); + if (!hash_buffer) + { + snprintf((char*)header, 0x200, "Failed to allocate %u bytes", (unsigned)code_size); + return rc_hash_error((const char*)header); + } + + rc_file_seek(file_handle, code_offset, SEEK_SET); + + if (verbose_message_callback) + { + snprintf((char*)header, 0x200, "Hashing %u bytes for 3DSX (at %08X)", (unsigned)code_size, (unsigned)code_offset); + verbose_message_callback((const char*)header); + } + + if (rc_file_read(file_handle, hash_buffer, code_size) != code_size) + { + free(hash_buffer); + return rc_hash_error("Could not read 3DSX code segment"); + } + + md5_append(md5, hash_buffer, code_size); + + free(hash_buffer); + return 1; +} + +static int rc_hash_nintendo_3ds(char hash[33], const char* path) +{ + md5_state_t md5; + void* file_handle; + uint8_t header[0x200]; /* NCCH and NCSD headers are both 0x200 bytes */ + int64_t header_offset; + + file_handle = rc_file_open(path); + if (!file_handle) + return rc_hash_error("Could not open file"); + + rc_file_seek(file_handle, 0, SEEK_SET); + + /* If we don't have a full header, this is probably not a 3DS ROM */ + if (rc_file_read(file_handle, header, sizeof(header)) != sizeof(header)) + { + rc_file_close(file_handle); + return rc_hash_error("Could not read 3DS ROM header"); + } + + md5_init(&md5); + + if (memcmp(&header[0x100], "NCSD", 4) == 0) + { + /* A NCSD container contains 1-8 NCCH partitions */ + /* The first partition (index 0) is reserved for executable content */ + header_offset = ((uint32_t)header[0x123] << 24) | (header[0x122] << 16) | (header[0x121] << 8) | header[0x120]; + /* Offset is in "media units" (1 media unit = 0x200 bytes) */ + header_offset *= 0x200; + + /* We include the NCSD header in the hash, as that will ensure different versions of a game result in a different hash + * This is due to some revisions / languages only ever changing other NCCH paritions (e.g. the game manual) + */ + rc_hash_verbose("Hashing 512 byte NCSD header"); + md5_append(&md5, header, sizeof(header)); + + if (verbose_message_callback) + { + snprintf((char*)header, sizeof(header), "Detected NCSD header, seeking to NCCH partition at %08X%08X", (unsigned)(header_offset >> 32), (unsigned)header_offset); + verbose_message_callback((const char*)header); + } + + rc_file_seek(file_handle, header_offset, SEEK_SET); + if (rc_file_read(file_handle, header, sizeof(header)) != sizeof(header)) + { + rc_file_close(file_handle); + return rc_hash_error("Could not read 3DS NCCH header"); + } + + if (memcmp(&header[0x100], "NCCH", 4) != 0) + { + rc_file_close(file_handle); + snprintf((char*)header, sizeof(header), "3DS NCCH header was not at %08X%08X", (unsigned)(header_offset >> 32), (unsigned)header_offset); + return rc_hash_error((const char*)header); + } + } + + if (memcmp(&header[0x100], "NCCH", 4) == 0) + { + if (rc_hash_nintendo_3ds_ncch(&md5, file_handle, header, NULL)) + { + rc_file_close(file_handle); + return rc_hash_finalize(&md5, hash); + } + + rc_file_close(file_handle); + return rc_hash_error("Failed to hash 3DS NCCH container"); + } + + /* Couldn't identify either an NCSD or NCCH */ + + /* Try to identify this as a CIA */ + if (header[0] == 0x20 && header[1] == 0x20 && header[2] == 0x00 && header[3] == 0x00) + { + rc_hash_verbose("Detected CIA, attempting to find executable NCCH"); + + if (rc_hash_nintendo_3ds_cia(&md5, file_handle, header)) + { + rc_file_close(file_handle); + return rc_hash_finalize(&md5, hash); + } + + rc_file_close(file_handle); + return rc_hash_error("Failed to hash 3DS CIA container"); + } + + /* This might be a homebrew game, try to detect that */ + if (memcmp(&header[0], "3DSX", 4) == 0) + { + rc_hash_verbose("Detected 3DSX"); + + if (rc_hash_nintendo_3ds_3dsx(&md5, file_handle, header)) + { + rc_file_close(file_handle); + return rc_hash_finalize(&md5, hash); + } + + rc_file_close(file_handle); + return rc_hash_error("Failed to hash 3DS 3DSX container"); + } + + /* Raw ELF marker (AXF/ELF files) */ + if (memcmp(&header[0], "\x7f\x45\x4c\x46", 4) == 0) + { + rc_hash_verbose("Detected AXF/ELF file, hashing entire file"); + + /* Don't bother doing anything fancy here, just hash entire file */ + rc_file_close(file_handle); + return rc_hash_whole_file(hash, path); + } + + rc_file_close(file_handle); + return rc_hash_error("Not a 3DS ROM"); +} + static int rc_hash_nintendo_ds(char hash[33], const char* path) { uint8_t header[512];