3rdparty/rcheevos: Bump to v11.5.0

This commit is contained in:
JordanTheToaster 2024-08-05 23:36:12 +01:00 committed by lightningterror
parent fe3eab5815
commit deb8e5bf18
11 changed files with 1075 additions and 162 deletions

View File

@ -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

View File

@ -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;
}

View File

@ -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;

View File

@ -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 {

View File

@ -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 */

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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)

View File

@ -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;
}

View File

@ -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 <base>.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];