/* Copyright (C) 2010-2019 The RetroArch team * * --------------------------------------------------------------------------------------- * The following license statement only applies to this file (core_updater_list.c). * --------------------------------------------------------------------------------------- * * Permission is hereby granted, free of charge, * to any person obtaining a copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include #include #include #include #include #include "file_path_special.h" #include "core_info.h" #include "core_updater_list.h" /* Holds all entries in a core updater list */ struct core_updater_list { size_t size; size_t capacity; core_updater_list_entry_t *entries; }; /* Cached ('global') core updater list */ static core_updater_list_t *core_list_cached = NULL; /**************************************/ /* Initialisation / De-Initialisation */ /**************************************/ /* Frees contents of specified core updater * list entry */ static void core_updater_list_free_entry(core_updater_list_entry_t *entry) { if (!entry) return; if (entry->remote_filename) { free(entry->remote_filename); entry->remote_filename = NULL; } if (entry->remote_core_path) { free(entry->remote_core_path); entry->remote_core_path = NULL; } if (entry->local_core_path) { free(entry->local_core_path); entry->local_core_path = NULL; } if (entry->local_info_path) { free(entry->local_info_path); entry->local_info_path = NULL; } if (entry->display_name) { free(entry->display_name); entry->display_name = NULL; } if (entry->description) { free(entry->description); entry->description = NULL; } if (entry->licenses_list) { string_list_free(entry->licenses_list); entry->licenses_list = NULL; } } /* Creates a new, empty core updater list. * Returns a handle to a new core_updater_list_t object * on success, otherwise returns NULL. */ core_updater_list_t *core_updater_list_init(size_t max_size) { core_updater_list_t *core_list = NULL; core_updater_list_entry_t *entries = NULL; /* Sanity check */ if (max_size < 1) return NULL; /* Create core updater list */ core_list = (core_updater_list_t*)malloc(sizeof(*core_list)); if (!core_list) return NULL; /* Create entries array */ entries = (core_updater_list_entry_t*)calloc(max_size, sizeof(*entries)); if (!entries) { free(core_list); return NULL; } /* Initial configuration */ core_list->size = 0; core_list->capacity = max_size; core_list->entries = entries; return core_list; } /* Resets (removes all entries of) specified core * updater list */ void core_updater_list_reset(core_updater_list_t *core_list) { size_t i; if (!core_list) return; for (i = 0; i < core_list->size; i++) { core_updater_list_entry_t *entry = &core_list->entries[i]; if (entry) core_updater_list_free_entry(entry); } core_list->size = 0; } /* Frees specified core updater list */ void core_updater_list_free(core_updater_list_t *core_list) { size_t i; if (!core_list) return; for (i = 0; i < core_list->size; i++) { core_updater_list_entry_t *entry = &core_list->entries[i]; if (entry) core_updater_list_free_entry(entry); } free(core_list->entries); core_list->entries = NULL; free(core_list); } /***************/ /* Cached List */ /***************/ /* Creates a new, empty cached core updater list * (i.e. 'global' list). * Returns false in the event of an error. */ bool core_updater_list_init_cached(size_t max_size) { /* Free any existing cached core updater list */ if (core_list_cached) { core_updater_list_free(core_list_cached); core_list_cached = NULL; } core_list_cached = core_updater_list_init(max_size); if (!core_list_cached) return false; return true; } /* Fetches cached core updater list */ core_updater_list_t *core_updater_list_get_cached(void) { if (core_list_cached) return core_list_cached; return NULL; } /* Frees cached core updater list */ void core_updater_list_free_cached(void) { core_updater_list_free(core_list_cached); core_list_cached = NULL; } /***********/ /* Getters */ /***********/ /* Returns number of entries in core updater list */ size_t core_updater_list_size(core_updater_list_t *core_list) { if (!core_list) return 0; return core_list->size; } /* Returns maximum allowed number of entries in core * updater list */ size_t core_updater_list_capacity(core_updater_list_t *core_list) { if (!core_list) return 0; return core_list->capacity; } /* Fetches core updater list entry corresponding * to the specified entry index. * Returns false if index is invalid. */ bool core_updater_list_get_index( core_updater_list_t *core_list, size_t idx, const core_updater_list_entry_t **entry) { if (!core_list || !entry) return false; if (idx >= core_list->size) return false; if (entry) *entry = &core_list->entries[idx]; return true; } /* Fetches core updater list entry corresponding * to the specified remote core filename. * Returns false if core is not found. */ bool core_updater_list_get_filename( core_updater_list_t *core_list, const char *remote_filename, const core_updater_list_entry_t **entry) { size_t i; if (!core_list || string_is_empty(remote_filename)) return false; if (core_list->size < 1) return false; /* Search for specified filename */ for (i = 0; i < core_list->size; i++) { core_updater_list_entry_t *current_entry = &core_list->entries[i]; if (string_is_empty(current_entry->remote_filename)) continue; if (string_is_equal(remote_filename, current_entry->remote_filename)) { if (entry) *entry = current_entry; return true; } } return false; } /* Fetches core updater list entry corresponding * to the specified core. * Returns false if core is not found. */ bool core_updater_list_get_core( core_updater_list_t *core_list, const char *local_core_path, const core_updater_list_entry_t **entry) { size_t i; char real_core_path[PATH_MAX_LENGTH]; real_core_path[0] = '\0'; if (!core_list || !entry || string_is_empty(local_core_path)) return false; if (core_list->size < 1) return false; /* Resolve absolute pathname of local_core_path */ strlcpy(real_core_path, local_core_path, sizeof(real_core_path)); path_resolve_realpath(real_core_path, sizeof(real_core_path), true); if (string_is_empty(real_core_path)) return false; /* Search for specified core */ for (i = 0; i < core_list->size; i++) { core_updater_list_entry_t *current_entry = &core_list->entries[i]; if (string_is_empty(current_entry->local_core_path)) continue; #ifdef _WIN32 /* Handle case-insensitive operating systems*/ if (string_is_equal_noncase(real_core_path, current_entry->local_core_path)) { #else if (string_is_equal(real_core_path, current_entry->local_core_path)) { #endif if (entry) *entry = current_entry; return true; } } return false; } /***********/ /* Setters */ /***********/ /* Parses date string and adds contents to * specified core updater list entry */ static bool core_updater_list_set_date( core_updater_list_entry_t *entry, const char *date_str) { struct string_list *date_list = NULL; if (!entry || string_is_empty(date_str)) goto error; /* Split date string into component values */ date_list = string_split(date_str, "-"); if (!date_list) goto error; /* Date string must have 3 values: * [year] [month] [day] */ if (date_list->size < 3) goto error; /* Convert date string values */ entry->date.year = string_to_unsigned(date_list->elems[0].data); entry->date.month = string_to_unsigned(date_list->elems[1].data); entry->date.day = string_to_unsigned(date_list->elems[2].data); /* Clean up */ string_list_free(date_list); return true; error: if (date_list) string_list_free(date_list); return false; } /* Parses crc string and adds value to * specified core updater list entry */ static bool core_updater_list_set_crc( core_updater_list_entry_t *entry, const char *crc_str) { uint32_t crc; if (!entry || string_is_empty(crc_str)) return false; crc = (uint32_t)string_hex_to_unsigned(crc_str); if (crc == 0) return false; entry->crc = crc; return true; } /* Parses core filename string and adds all * associated paths to the specified core * updater list entry */ static bool core_updater_list_set_paths( core_updater_list_entry_t *entry, const char *path_dir_libretro, const char *path_libretro_info, const char *network_buildbot_url, const char *filename_str) { char *last_underscore = NULL; char *tmp_url = NULL; char remote_core_path[PATH_MAX_LENGTH]; char local_core_path[PATH_MAX_LENGTH]; char local_info_path[PATH_MAX_LENGTH]; bool is_archive; remote_core_path[0] = '\0'; local_core_path[0] = '\0'; local_info_path[0] = '\0'; if (!entry || string_is_empty(filename_str)) return false; if (string_is_empty(path_dir_libretro) || string_is_empty(path_libretro_info) || string_is_empty(network_buildbot_url)) return false; /* Check whether remote file is an archive */ is_archive = path_is_compressed_file(filename_str); /* remote_filename */ if (entry->remote_filename) { free(entry->remote_filename); entry->remote_filename = NULL; } entry->remote_filename = strdup(filename_str); /* remote_core_path */ fill_pathname_join( remote_core_path, network_buildbot_url, filename_str, sizeof(remote_core_path)); /* > Apply proper URL encoding (messy...) */ tmp_url = strdup(remote_core_path); remote_core_path[0] = '\0'; net_http_urlencode_full( remote_core_path, tmp_url, sizeof(remote_core_path)); if (tmp_url) free(tmp_url); if (entry->remote_core_path) { free(entry->remote_core_path); entry->remote_core_path = NULL; } entry->remote_core_path = strdup(remote_core_path); /* local_core_path */ fill_pathname_join( local_core_path, path_dir_libretro, filename_str, sizeof(local_core_path)); if (is_archive) path_remove_extension(local_core_path); path_resolve_realpath(local_core_path, sizeof(local_core_path), true); if (entry->local_core_path) { free(entry->local_core_path); entry->local_core_path = NULL; } entry->local_core_path = strdup(local_core_path); /* local_info_path */ fill_pathname_join_noext( local_info_path, path_libretro_info, filename_str, sizeof(local_info_path)); if (is_archive) path_remove_extension(local_info_path); /* > Remove any non-standard core filename * additions (i.e. info files end with * '_libretro' but core files may have * a platform specific addendum, * e.g. '_android')*/ last_underscore = (char*)strrchr(local_info_path, '_'); if (!string_is_empty(last_underscore)) if (!string_is_equal(last_underscore, "_libretro")) *last_underscore = '\0'; /* > Add proper file extension */ strlcat( local_info_path, file_path_str(FILE_PATH_CORE_INFO_EXTENSION), sizeof(local_info_path)); if (entry->local_info_path) { free(entry->local_info_path); entry->local_info_path = NULL; } entry->local_info_path = strdup(local_info_path); return true; } /* Reads info file associated with core and * adds relevant information to updater list * entry */ static bool core_updater_list_set_core_info( core_updater_list_entry_t *entry, const char *local_info_path, const char *filename_str) { core_updater_info_t *core_info = NULL; if (!entry || string_is_empty(local_info_path) || string_is_empty(filename_str)) return false; /* Clear any existing core info */ if (entry->display_name) { free(entry->display_name); entry->display_name = NULL; } if (entry->description) { free(entry->description); entry->description = NULL; } if (entry->licenses_list) { /* Note: We can safely leave this as NULL if * the core info file is invalid */ string_list_free(entry->licenses_list); entry->licenses_list = NULL; } entry->is_experimental = false; /* Read core info file * > Note: It's a bit rubbish that we have to * read the actual core info files here... * Would be better to cache this globally * (at present, we only cache info for * *installed* cores...) */ core_info = core_info_get_core_updater_info(local_info_path); if (core_info) { /* display_name + is_experimental */ if (!string_is_empty(core_info->display_name)) { entry->display_name = strdup(core_info->display_name); entry->is_experimental = core_info->is_experimental; } else { /* If display name is blank, use core filename and * assume core is experimental (i.e. all 'fit for consumption' * cores must have a valid/complete core info file) */ entry->display_name = strdup(filename_str); entry->is_experimental = true; } /* description */ if (!string_is_empty(core_info->description)) entry->description = strdup(core_info->description); else entry->description = strdup(""); /* licenses_list */ if (!string_is_empty(core_info->licenses)) entry->licenses_list = string_split(core_info->licenses, "|"); /* Clean up */ core_info_free_core_updater_info(core_info); } else { /* If info file is missing, use core filename and * assume core is experimental (i.e. all 'fit for consumption' * cores must have a valid/complete core info file) */ entry->display_name = strdup(filename_str); entry->is_experimental = true; entry->description = strdup(""); } return true; } /* Adds entry to the end of the specified core * updater list * NOTE: Entry string values are passed by * reference - *do not free the entry passed * to this function* */ static bool core_updater_list_push_entry( core_updater_list_t *core_list, core_updater_list_entry_t *entry) { core_updater_list_entry_t *list_entry = NULL; if (!core_list || !entry) return false; /* Ensure there is enough space for the new entry */ if (core_list->capacity <= core_list->size) return false; /* Get handle of new entry inside list */ list_entry = &core_list->entries[core_list->size]; if (!list_entry) return false; /* Ensure list entry is empty */ core_updater_list_free_entry(list_entry); /* Assign paths */ list_entry->remote_filename = entry->remote_filename; list_entry->remote_core_path = entry->remote_core_path; list_entry->local_core_path = entry->local_core_path; list_entry->local_info_path = entry->local_info_path; /* Assign core info */ list_entry->display_name = entry->display_name; list_entry->description = entry->description; list_entry->licenses_list = entry->licenses_list; list_entry->is_experimental = entry->is_experimental; /* Copy crc */ list_entry->crc = entry->crc; /* Copy date */ memcpy(&list_entry->date, &entry->date, sizeof(core_updater_list_date_t)); /* Increment list size */ core_list->size++; return true; } /* Parses the contents of a single buildbot * core listing and adds it to the specified * core updater list */ static void core_updater_list_add_entry( core_updater_list_t *core_list, const char *path_dir_libretro, const char *path_libretro_info, const char *network_buildbot_url, struct string_list *network_core_entry_list) { const char *date_str = NULL; const char *crc_str = NULL; const char *filename_str = NULL; core_updater_list_entry_t entry = {0}; if (!core_list || !network_core_entry_list) goto error; /* > Listings must have 3 entries: * [date] [crc] [filename] */ if (network_core_entry_list->size < 3) goto error; /* Get handles of the individual listing strings */ date_str = network_core_entry_list->elems[0].data; crc_str = network_core_entry_list->elems[1].data; filename_str = network_core_entry_list->elems[2].data; if (string_is_empty(date_str) || string_is_empty(crc_str) || string_is_empty(filename_str)) goto error; /* Check whether core file is already included * in the list (this is *not* an error condition, * it just means we can skip the current listing) */ if (core_updater_list_get_filename(core_list, filename_str, NULL)) goto error; /* Parse individual listing strings */ if (!core_updater_list_set_date(&entry, date_str)) goto error; if (!core_updater_list_set_crc(&entry, crc_str)) goto error; if (!core_updater_list_set_paths( &entry, path_dir_libretro, path_libretro_info, network_buildbot_url, filename_str)) goto error; if (!core_updater_list_set_core_info( &entry, entry.local_info_path, filename_str)) goto error; /* Add entry to list */ if (!core_updater_list_push_entry(core_list, &entry)) goto error; return; error: /* This is not a *fatal* error - it just * means one of the following: * - The current line of entry text received * from the buildbot is broken somehow * (could be the case that the network buffer * wasn't large enough to cache the entire * string, so the last line was truncated) * - The core updater list struct is at capacity * In either case, the current entry is discarded * and we move on to the next one * (network transfers are fishy business, so we * choose to ignore this sort of error - don't * want the whole fetch to fail because of a * trivial glitch...) */ core_updater_list_free_entry(&entry); } /* Core updater list qsort helper function */ static int core_updater_list_qsort_func( const core_updater_list_entry_t *a, const core_updater_list_entry_t *b) { if (!a || !b) return 0; if (string_is_empty(a->display_name) || string_is_empty(b->display_name)) return 0; return strcasecmp(a->display_name, b->display_name); } /* Sorts core updater list into alphabetical order */ static void core_updater_list_qsort(core_updater_list_t *core_list) { if (!core_list) return; if (core_list->size < 2) return; qsort( core_list->entries, core_list->size, sizeof(core_updater_list_entry_t), (int (*)(const void *, const void *)) core_updater_list_qsort_func); } /* Reads the contents of a buildbot core list * network request into the specified * core_updater_list_t object. * Returns false in the event of an error. */ bool core_updater_list_parse_network_data( core_updater_list_t *core_list, const char *path_dir_libretro, const char *path_libretro_info, const char *network_buildbot_url, const char *data, size_t len) { struct string_list *network_core_list = NULL; struct string_list *network_core_entry_list = NULL; char *data_buf = NULL; size_t i; /* Sanity check */ if (!core_list || string_is_empty(data) || (len < 1)) goto error; /* We're populating a list 'from scratch' - remove * any existing entries */ core_updater_list_reset(core_list); /* Input data string is not terminated - have * to copy it to a temporary buffer... */ data_buf = (char*)malloc((len + 1) * sizeof(char)); if (!data_buf) goto error; memcpy(data_buf, data, len * sizeof(char)); data_buf[len] = '\0'; /* Split network listing request into lines */ network_core_list = string_split(data_buf, "\n"); if (!network_core_list) goto error; if (network_core_list->size < 1) goto error; /* Temporary data buffer is no longer required */ free(data_buf); data_buf = NULL; /* Loop over lines */ for (i = 0; i < network_core_list->size; i++) { const char *line = network_core_list->elems[i].data; if (string_is_empty(line)) continue; /* Split line into listings info components */ network_core_entry_list = string_split(line, " "); /* Parse listings info and add to core updater * list */ core_updater_list_add_entry( core_list, path_dir_libretro, path_libretro_info, network_buildbot_url, network_core_entry_list); /* Clean up */ string_list_free(network_core_entry_list); network_core_entry_list = NULL; } /* Sanity check */ if (core_list->size < 1) goto error; /* Clean up */ string_list_free(network_core_list); /* Sort completed list */ core_updater_list_qsort(core_list); return true; error: if (network_core_list) string_list_free(network_core_list); if (network_core_entry_list) string_list_free(network_core_entry_list); if (data_buf) free(data_buf); return false; }