/* Copyright (C) 2010-2019 The RetroArch team * * --------------------------------------------------------------------------------------- * The following license statement only applies to this file (manual_content_scan.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 "msg_hash.h" #include "list_special.h" #include "core_info.h" #include "file_path_special.h" #include "frontend/frontend_driver.h" #include "manual_content_scan.h" /* Holds all configuration parameters associated * with a manual content scan */ typedef struct { enum manual_content_scan_system_name_type system_name_type; enum manual_content_scan_core_type core_type; char content_dir[PATH_MAX_LENGTH]; char system_name_content_dir[PATH_MAX_LENGTH]; char system_name_database[PATH_MAX_LENGTH]; char system_name_custom[PATH_MAX_LENGTH]; char core_name[PATH_MAX_LENGTH]; char core_path[PATH_MAX_LENGTH]; char file_exts_core[PATH_MAX_LENGTH]; char file_exts_custom[PATH_MAX_LENGTH]; char dat_file_path[PATH_MAX_LENGTH]; bool search_recursively; bool search_archives; bool filter_dat_content; bool overwrite_playlist; bool validate_entries; } scan_settings_t; /* TODO/FIXME - static public global variables */ /* Static settings object * > Provides easy access to settings parameters * when creating associated menu entries * > We are handling this in almost exactly the same * way as the regular global 'static settings_t *configuration_settings;' * object in retroarch.c. This means it is not inherently thread safe, * but this should not be an issue (i.e. regular configuration_settings * are not thread safe, but we only access them when pushing a * task, not in the task thread itself, so all is well) */ static scan_settings_t scan_settings = { MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR, /* system_name_type */ MANUAL_CONTENT_SCAN_CORE_DETECT, /* core_type */ "", /* content_dir */ "", /* system_name_content_dir */ "", /* system_name_database */ "", /* system_name_custom */ "", /* core_name */ "", /* core_path */ "", /* file_exts_core */ "", /* file_exts_custom */ "", /* dat_file_path */ true, /* search_recursively */ false, /* search_archives */ false, /* filter_dat_content */ false, /* overwrite_playlist */ false /* validate_entries */ }; /*****************/ /* Configuration */ /*****************/ /* Pointer access */ /* Returns a pointer to the internal * 'content_dir' string */ char *manual_content_scan_get_content_dir_ptr(void) { return scan_settings.content_dir; } /* Returns a pointer to the internal * 'system_name_custom' string */ char *manual_content_scan_get_system_name_custom_ptr(void) { return scan_settings.system_name_custom; } /* Returns size of the internal * 'system_name_custom' string */ size_t manual_content_scan_get_system_name_custom_size(void) { return sizeof(scan_settings.system_name_custom); } /* Returns a pointer to the internal * 'file_exts_custom' string */ char *manual_content_scan_get_file_exts_custom_ptr(void) { return scan_settings.file_exts_custom; } /* Returns size of the internal * 'file_exts_custom' string */ size_t manual_content_scan_get_file_exts_custom_size(void) { return sizeof(scan_settings.file_exts_custom); } /* Returns a pointer to the internal * 'dat_file_path' string */ char *manual_content_scan_get_dat_file_path_ptr(void) { return scan_settings.dat_file_path; } /* Returns size of the internal * 'dat_file_path' string */ size_t manual_content_scan_get_dat_file_path_size(void) { return sizeof(scan_settings.dat_file_path); } /* Returns a pointer to the internal * 'search_recursively' bool */ bool *manual_content_scan_get_search_recursively_ptr(void) { return &scan_settings.search_recursively; } /* Returns a pointer to the internal * 'search_archives' bool */ bool *manual_content_scan_get_search_archives_ptr(void) { return &scan_settings.search_archives; } /* Returns a pointer to the internal * 'filter_dat_content' bool */ bool *manual_content_scan_get_filter_dat_content_ptr(void) { return &scan_settings.filter_dat_content; } /* Returns a pointer to the internal * 'overwrite_playlist' bool */ bool *manual_content_scan_get_overwrite_playlist_ptr(void) { return &scan_settings.overwrite_playlist; } /* Returns a pointer to the internal * 'validate_entries' bool */ bool *manual_content_scan_get_validate_entries_ptr(void) { return &scan_settings.validate_entries; } /* Sanitisation */ /* Sanitises file extensions list string: * > Removes period (full stop) characters * > Converts to lower case * > Trims leading/trailing whitespace */ static void manual_content_scan_scrub_file_exts(char *file_exts) { if (string_is_empty(file_exts)) return; string_remove_all_chars(file_exts, '.'); string_to_lower(file_exts); string_trim_whitespace(file_exts); } /* Removes invalid characters from * 'system_name_custom' string */ void manual_content_scan_scrub_system_name_custom(void) { char *scrub_char_pointer = NULL; if (string_is_empty(scan_settings.system_name_custom)) return; /* Scrub characters that are not cross-platform * and/or violate the No-Intro filename standard: * http://datomatic.no-intro.org/stuff/The%20Official%20No-Intro%20Convention%20(20071030).zip * Replace these characters with underscores */ while ((scrub_char_pointer = strpbrk(scan_settings.system_name_custom, "&*/:`\"<>?\\|"))) *scrub_char_pointer = '_'; } /* Removes period (full stop) characters from * 'file_exts_custom' string and converts to * lower case */ void manual_content_scan_scrub_file_exts_custom(void) { manual_content_scan_scrub_file_exts(scan_settings.file_exts_custom); } /* Checks 'dat_file_path' string and resets it * if invalid */ enum manual_content_scan_dat_file_path_status manual_content_scan_validate_dat_file_path(void) { enum manual_content_scan_dat_file_path_status dat_file_path_status = MANUAL_CONTENT_SCAN_DAT_FILE_UNSET; /* Check if 'dat_file_path' has been set */ if (!string_is_empty(scan_settings.dat_file_path)) { uint64_t file_size; /* Check if path itself is valid */ if (logiqx_dat_path_is_valid(scan_settings.dat_file_path, &file_size)) { uint64_t free_memory = frontend_driver_get_free_memory(); dat_file_path_status = MANUAL_CONTENT_SCAN_DAT_FILE_OK; /* DAT files can be *very* large... * Try to enforce sane behaviour by requiring * the system to have an amount of free memory * at least twice the size of the DAT file... * > Note that desktop (and probably mobile) * platforms should always have enough memory * for this - we're really only protecting the * console ports here */ if (free_memory > 0) { if (free_memory < (2 * file_size)) dat_file_path_status = MANUAL_CONTENT_SCAN_DAT_FILE_TOO_LARGE; } /* This is an annoying condition - it means the * current platform doesn't have a 'free_memory' * implementation... * Have to make some assumptions in this case: * > Typically the lowest system RAM of a supported * platform in 32MB * > Propose that (2 * file_size) should be no more * than 1/4 of this total RAM value */ else if ((2 * file_size) > (8 * 1048576)) dat_file_path_status = MANUAL_CONTENT_SCAN_DAT_FILE_TOO_LARGE; } else dat_file_path_status = MANUAL_CONTENT_SCAN_DAT_FILE_INVALID; } /* Reset 'dat_file_path' if status is anything other * that 'OK' */ if (dat_file_path_status != MANUAL_CONTENT_SCAN_DAT_FILE_OK) scan_settings.dat_file_path[0] = '\0'; return dat_file_path_status; } /* Menu setters */ /* Sets content directory for next manual scan * operation. * Returns true if content directory is valid. */ bool manual_content_scan_set_menu_content_dir(const char *content_dir) { size_t len; const char *dir_name = NULL; /* Sanity check */ if (string_is_empty(content_dir)) goto error; if (!path_is_directory(content_dir)) goto error; /* Copy directory path to settings struct */ strlcpy( scan_settings.content_dir, content_dir, sizeof(scan_settings.content_dir)); /* Remove trailing slash, if required */ len = strlen(scan_settings.content_dir); if (len <= 0) goto error; if (scan_settings.content_dir[len - 1] == PATH_DEFAULT_SLASH_C()) scan_settings.content_dir[len - 1] = '\0'; /* Handle case where path was a single slash... */ if (string_is_empty(scan_settings.content_dir)) goto error; /* Get directory name (used as system name * when scan_settings.system_name_type == * MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR) */ dir_name = path_basename(scan_settings.content_dir); if (string_is_empty(dir_name)) goto error; /* Copy directory name to settings struct */ strlcpy( scan_settings.system_name_content_dir, dir_name, sizeof(scan_settings.system_name_content_dir)); return true; error: /* Directory is invalid - reset internal * content directory and associated 'directory' * system name */ scan_settings.content_dir[0] = '\0'; scan_settings.system_name_content_dir[0] = '\0'; return false; } /* Sets system name for the next manual scan * operation. * Returns true if system name is valid. * NOTE: * > Only sets 'system_name_type' and 'system_name_database' * > 'system_name_content_dir' and 'system_name_custom' are * (by necessity) handled elsewhere * > This may look fishy, but it's not - it's a menu-specific * function, and this is simply the cleanest way to handle * the setting... */ bool manual_content_scan_set_menu_system_name( enum manual_content_scan_system_name_type system_name_type, const char *system_name) { /* Sanity check */ if (system_name_type > MANUAL_CONTENT_SCAN_SYSTEM_NAME_DATABASE) goto error; /* Cache system name 'type' */ scan_settings.system_name_type = system_name_type; /* Check if we are using a non-database name */ if ((scan_settings.system_name_type == MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR) || (scan_settings.system_name_type == MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM)) scan_settings.system_name_database[0] = '\0'; else { /* We are using a database name... */ if (string_is_empty(system_name)) goto error; /* Copy database name to settings struct */ strlcpy( scan_settings.system_name_database, system_name, sizeof(scan_settings.system_name_database)); } return true; error: /* Input parameters are invalid - reset internal * 'system_name_type' and 'system_name_database' */ scan_settings.system_name_type = MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR; scan_settings.system_name_database[0] = '\0'; return false; } /* Sets core name for the next manual scan * operation (+ core path and other associated * parameters). * Returns true if core name is valid. */ bool manual_content_scan_set_menu_core_name( enum manual_content_scan_core_type core_type, const char *core_name) { /* Sanity check */ if (core_type > MANUAL_CONTENT_SCAN_CORE_SET) goto error; /* Cache core 'type' */ scan_settings.core_type = core_type; /* Check if we are using core autodetection */ if (scan_settings.core_type == MANUAL_CONTENT_SCAN_CORE_DETECT) { scan_settings.core_name[0] = '\0'; scan_settings.core_path[0] = '\0'; scan_settings.file_exts_core[0] = '\0'; } else { core_info_list_t *core_info_list = NULL; core_info_t *core_info = NULL; bool core_found = false; size_t i; /* We are using a manually set core... */ if (string_is_empty(core_name)) goto error; /* Get core list */ core_info_get_list(&core_info_list); if (!core_info_list) goto error; /* Search for the specified core name */ for (i = 0; i < core_info_list->count; i++) { core_info = NULL; core_info = core_info_get(core_info_list, i); if (core_info) { if (string_is_equal(core_info->display_name, core_name)) { /* Core has been found */ core_found = true; /* Copy core path to settings struct */ if (string_is_empty(core_info->path)) goto error; strlcpy( scan_settings.core_path, core_info->path, sizeof(scan_settings.core_path)); /* Copy core name to settings struct */ strlcpy( scan_settings.core_name, core_info->display_name, sizeof(scan_settings.core_name)); /* Copy supported extensions to settings * struct, if required */ if (!string_is_empty(core_info->supported_extensions)) { strlcpy( scan_settings.file_exts_core, core_info->supported_extensions, sizeof(scan_settings.file_exts_core)); /* Core info extensions are delimited by * vertical bars. For internal consistency, * replace them with spaces */ string_replace_all_chars(scan_settings.file_exts_core, '|', ' '); /* Apply standard scrubbing/clean-up * (should not be required, but must handle the * case where a core info file is incorrectly * formatted) */ manual_content_scan_scrub_file_exts(scan_settings.file_exts_core); } else scan_settings.file_exts_core[0] = '\0'; break; } } } /* Sanity check */ if (!core_found) goto error; } return true; error: /* Input parameters are invalid - reset internal * core values */ scan_settings.core_type = MANUAL_CONTENT_SCAN_CORE_DETECT; scan_settings.core_name[0] = '\0'; scan_settings.core_path[0] = '\0'; scan_settings.file_exts_core[0] = '\0'; return false; } /* Sets all parameters for the next manual scan * operation according the to recorded values in * the specified playlist. * Returns MANUAL_CONTENT_SCAN_PLAYLIST_REFRESH_OK * if playlist contains a valid scan record. */ enum manual_content_scan_playlist_refresh_status manual_content_scan_set_menu_from_playlist(playlist_t *playlist, const char *path_content_database, bool show_hidden_files) { const char *playlist_path = NULL; const char *playlist_file = NULL; const char *content_dir = NULL; const char *core_name = NULL; const char *file_exts = NULL; const char *dat_file_path = NULL; bool search_recursively = false; bool search_archives = false; bool filter_dat_content = false; bool overwrite_playlist = false; #ifdef HAVE_LIBRETRODB struct string_list *rdb_list = NULL; #endif enum manual_content_scan_system_name_type system_name_type = MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR; enum manual_content_scan_core_type core_type = MANUAL_CONTENT_SCAN_CORE_DETECT; enum manual_content_scan_playlist_refresh_status playlist_status = MANUAL_CONTENT_SCAN_PLAYLIST_REFRESH_OK; char system_name[PATH_MAX_LENGTH]; system_name[0] = '\0'; if (!playlist_scan_refresh_enabled(playlist)) { playlist_status = MANUAL_CONTENT_SCAN_PLAYLIST_REFRESH_MISSING_CONFIG; goto end; } /* Read scan parameters from playlist */ playlist_path = playlist_get_conf_path(playlist); content_dir = playlist_get_scan_content_dir(playlist); core_name = playlist_get_default_core_name(playlist); file_exts = playlist_get_scan_file_exts(playlist); dat_file_path = playlist_get_scan_dat_file_path(playlist); search_recursively = playlist_get_scan_search_recursively(playlist); search_archives = playlist_get_scan_search_archives(playlist); filter_dat_content = playlist_get_scan_filter_dat_content(playlist); overwrite_playlist = playlist_get_scan_overwrite_playlist(playlist); /* Determine system name (playlist basename * without extension) */ if (string_is_empty(playlist_path)) { /* Cannot happen, but would constitute a * 'system name' error */ playlist_status = MANUAL_CONTENT_SCAN_PLAYLIST_REFRESH_INVALID_SYSTEM_NAME; goto end; } if ((playlist_file = path_basename(playlist_path))) { strlcpy(system_name, playlist_file, sizeof(system_name)); path_remove_extension(system_name); } if (string_is_empty(system_name)) { /* Cannot happen, but would constitute a * 'system name' error */ playlist_status = MANUAL_CONTENT_SCAN_PLAYLIST_REFRESH_INVALID_SYSTEM_NAME; goto end; } /* Set content directory */ if (!manual_content_scan_set_menu_content_dir(content_dir)) { playlist_status = MANUAL_CONTENT_SCAN_PLAYLIST_REFRESH_INVALID_CONTENT_DIR; goto end; } /* Set system name */ #ifdef HAVE_LIBRETRODB /* > If platform has database support, get names * of all installed database files */ rdb_list = dir_list_new_special( path_content_database, DIR_LIST_DATABASES, NULL, show_hidden_files); if (rdb_list && rdb_list->size) { size_t i; /* Loop over database files */ for (i = 0; i < rdb_list->size; i++) { const char *rdb_path = rdb_list->elems[i].data; const char *rdb_file = NULL; char rdb_name[PATH_MAX_LENGTH]; rdb_name[0] = '\0'; /* Sanity check */ if (string_is_empty(rdb_path)) continue; rdb_file = path_basename(rdb_path); if (string_is_empty(rdb_file)) continue; /* Remove file extension */ strlcpy(rdb_name, rdb_file, sizeof(rdb_name)); path_remove_extension(rdb_name); if (string_is_empty(rdb_name)) continue; /* Check whether playlist system name * matches current database file */ if (string_is_equal(system_name, rdb_name)) { system_name_type = MANUAL_CONTENT_SCAN_SYSTEM_NAME_DATABASE; break; } } } string_list_free(rdb_list); #endif /* > If system name does not match a database * file, then check whether it matches the * content directory name */ if (system_name_type != MANUAL_CONTENT_SCAN_SYSTEM_NAME_DATABASE) { /* system_name_type is set to * MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR * by default - so if a match is found just * reset 'custom name' field */ if (string_is_equal(system_name, scan_settings.system_name_content_dir)) scan_settings.system_name_custom[0] = '\0'; else { /* Playlist is using a custom system name */ system_name_type = MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM; strlcpy(scan_settings.system_name_custom, system_name, sizeof(scan_settings.system_name_custom)); } } if (!manual_content_scan_set_menu_system_name( system_name_type, system_name)) { playlist_status = MANUAL_CONTENT_SCAN_PLAYLIST_REFRESH_INVALID_SYSTEM_NAME; goto end; } /* Set core path/name */ if ( !string_is_empty(core_name) && !string_is_equal(core_name, FILE_PATH_DETECT)) core_type = MANUAL_CONTENT_SCAN_CORE_SET; if (!manual_content_scan_set_menu_core_name( core_type, core_name)) { playlist_status = MANUAL_CONTENT_SCAN_PLAYLIST_REFRESH_INVALID_CORE; goto end; } /* Set custom file extensions */ if (string_is_empty(file_exts)) scan_settings.file_exts_custom[0] = '\0'; else { strlcpy(scan_settings.file_exts_custom, file_exts, sizeof(scan_settings.file_exts_custom)); /* File extensions read from playlist should * be correctly formatted, with '|' characters * as delimiters * > For menu purposes, must replace these * delimiters with space characters * > Additionally scrub the resultant string, * to handle the case where a user has * 'corrupted' it by manually tampering with * the playlist file */ string_replace_all_chars(scan_settings.file_exts_custom, '|', ' '); manual_content_scan_scrub_file_exts(scan_settings.file_exts_custom); } /* Set DAT file path */ if (string_is_empty(dat_file_path)) scan_settings.dat_file_path[0] = '\0'; else { strlcpy(scan_settings.dat_file_path, dat_file_path, sizeof(scan_settings.dat_file_path)); switch (manual_content_scan_validate_dat_file_path()) { case MANUAL_CONTENT_SCAN_DAT_FILE_INVALID: playlist_status = MANUAL_CONTENT_SCAN_PLAYLIST_REFRESH_INVALID_DAT_FILE; goto end; case MANUAL_CONTENT_SCAN_DAT_FILE_TOO_LARGE: playlist_status = MANUAL_CONTENT_SCAN_PLAYLIST_REFRESH_DAT_FILE_TOO_LARGE; goto end; default: /* No action required */ break; } } /* Set remaining boolean parameters */ scan_settings.search_recursively = search_recursively; scan_settings.search_archives = search_archives; scan_settings.filter_dat_content = filter_dat_content; scan_settings.overwrite_playlist = overwrite_playlist; /* When refreshing a playlist: * > We always validate entries in the * existing file */ scan_settings.validate_entries = true; end: return playlist_status; } /* Menu getters */ /* Fetches content directory for next manual scan * operation. * Returns true if content directory is valid. */ bool manual_content_scan_get_menu_content_dir(const char **content_dir) { if (!content_dir) return false; if (string_is_empty(scan_settings.content_dir)) return false; *content_dir = scan_settings.content_dir; return true; } /* Fetches system name for the next manual scan operation. * Returns true if system name is valid. * NOTE: This corresponds to the 'System Name' value * displayed in menus - this is not identical to the * actual system name used when generating the playlist */ bool manual_content_scan_get_menu_system_name(const char **system_name) { if (system_name) { switch (scan_settings.system_name_type) { case MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR: *system_name = msg_hash_to_str( MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_SYSTEM_NAME_USE_CONTENT_DIR); return true; case MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM: *system_name = msg_hash_to_str( MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_SYSTEM_NAME_USE_CUSTOM); return true; case MANUAL_CONTENT_SCAN_SYSTEM_NAME_DATABASE: if (!string_is_empty(scan_settings.system_name_database)) { *system_name = scan_settings.system_name_database; return true; } break; default: break; } } return false; } /* Fetches core name for the next manual scan operation. * Returns true if core name is valid. */ bool manual_content_scan_get_menu_core_name(const char **core_name) { if (core_name) { switch (scan_settings.core_type) { case MANUAL_CONTENT_SCAN_CORE_DETECT: *core_name = msg_hash_to_str( MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_CORE_NAME_DETECT); return true; case MANUAL_CONTENT_SCAN_CORE_SET: if (!string_is_empty(scan_settings.core_name)) { *core_name = scan_settings.core_name; return true; } break; default: break; } } return false; } /* Menu utility functions */ /* Creates a list of all possible 'system name' menu * strings, for use in 'menu_displaylist' drop-down * lists and 'menu_cbs_left/right' * > Returns NULL in the event of failure * > Returned string list must be free()'d */ struct string_list *manual_content_scan_get_menu_system_name_list( const char *path_content_database, bool show_hidden_files) { union string_list_elem_attr attr; struct string_list *name_list = string_list_new(); /* Sanity check */ if (!name_list) return NULL; attr.i = 0; /* Add 'use content directory' entry */ if (!string_list_append(name_list, msg_hash_to_str( MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_SYSTEM_NAME_USE_CONTENT_DIR), attr)) goto error; /* Add 'use custom' entry */ if (!string_list_append(name_list, msg_hash_to_str( MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_SYSTEM_NAME_USE_CUSTOM), attr)) goto error; #ifdef HAVE_LIBRETRODB /* If platform has database support, get names * of all installed database files */ { /* Note: dir_list_new_special() is well behaved - the * returned string list will only include database * files (i.e. don't have to check for directories, * or verify file extensions) */ struct string_list *rdb_list = dir_list_new_special( path_content_database, DIR_LIST_DATABASES, NULL, show_hidden_files); if (rdb_list && rdb_list->size) { unsigned i; /* Ensure database list is in alphabetical order */ dir_list_sort(rdb_list, true); /* Loop over database files */ for (i = 0; i < rdb_list->size; i++) { const char *rdb_path = rdb_list->elems[i].data; const char *rdb_file = NULL; char rdb_name[PATH_MAX_LENGTH]; rdb_name[0] = '\0'; /* Sanity check */ if (string_is_empty(rdb_path)) continue; rdb_file = path_basename(rdb_path); if (string_is_empty(rdb_file)) continue; /* Remove file extension */ strlcpy(rdb_name, rdb_file, sizeof(rdb_name)); path_remove_extension(rdb_name); if (string_is_empty(rdb_name)) continue; /* Add database name to list */ if (!string_list_append(name_list, rdb_name, attr)) goto error; } } /* Clean up */ string_list_free(rdb_list); } #endif return name_list; error: if (name_list) string_list_free(name_list); return NULL; } /* Creates a list of all possible 'core name' menu * strings, for use in 'menu_displaylist' drop-down * lists and 'menu_cbs_left/right' * > Returns NULL in the event of failure * > Returned string list must be free()'d */ struct string_list *manual_content_scan_get_menu_core_name_list(void) { struct string_list *name_list = string_list_new(); core_info_list_t *core_info_list = NULL; union string_list_elem_attr attr; /* Sanity check */ if (!name_list) goto error; attr.i = 0; /* Add 'DETECT' entry */ if (!string_list_append(name_list, msg_hash_to_str( MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_CORE_NAME_DETECT), attr)) goto error; /* Get core list */ core_info_get_list(&core_info_list); if (core_info_list) { size_t i; core_info_t *core_info = NULL; /* Sort cores alphabetically */ core_info_qsort(core_info_list, CORE_INFO_LIST_SORT_DISPLAY_NAME); /* Loop through cores */ for (i = 0; i < core_info_list->count; i++) { if ((core_info = core_info_get(core_info_list, i))) { if (string_is_empty(core_info->display_name)) continue; /* Add core name to list */ if (!string_list_append(name_list, core_info->display_name, attr)) goto error; } } } return name_list; error: if (name_list) string_list_free(name_list); return NULL; } /****************/ /* Task Helpers */ /****************/ /* Parses current manual content scan settings, * and extracts all information required to configure * a manual content scan task. * Returns false if current settings are invalid. */ bool manual_content_scan_get_task_config( manual_content_scan_task_config_t *task_config, const char *path_dir_playlist ) { size_t len; if (!task_config) return false; /* Ensure all 'task_config' strings are * correctly initialised */ task_config->playlist_file[0] = '\0'; task_config->content_dir[0] = '\0'; task_config->system_name[0] = '\0'; task_config->database_name[0] = '\0'; task_config->core_name[0] = '\0'; task_config->core_path[0] = '\0'; task_config->file_exts[0] = '\0'; task_config->dat_file_path[0] = '\0'; /* Get content directory */ if (string_is_empty(scan_settings.content_dir)) return false; if (!path_is_directory(scan_settings.content_dir)) return false; strlcpy( task_config->content_dir, scan_settings.content_dir, sizeof(task_config->content_dir)); /* Get system name */ switch (scan_settings.system_name_type) { case MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR: if (string_is_empty(scan_settings.system_name_content_dir)) return false; strlcpy( task_config->system_name, scan_settings.system_name_content_dir, sizeof(task_config->system_name)); break; case MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM: if (string_is_empty(scan_settings.system_name_custom)) return false; strlcpy( task_config->system_name, scan_settings.system_name_custom, sizeof(task_config->system_name)); break; case MANUAL_CONTENT_SCAN_SYSTEM_NAME_DATABASE: if (string_is_empty(scan_settings.system_name_database)) return false; strlcpy( task_config->system_name, scan_settings.system_name_database, sizeof(task_config->system_name)); break; default: return false; } /* Now we have a valid system name, can generate * a 'database' name... */ len = strlcpy( task_config->database_name, task_config->system_name, sizeof(task_config->database_name)); strlcpy(task_config->database_name + len, ".lpl", sizeof(task_config->database_name) - len); /* ...which can in turn be used to generate the * playlist path */ if (string_is_empty(path_dir_playlist)) return false; fill_pathname_join_special( task_config->playlist_file, path_dir_playlist, task_config->database_name, sizeof(task_config->playlist_file)); if (string_is_empty(task_config->playlist_file)) return false; /* Get core name and path */ switch (scan_settings.core_type) { case MANUAL_CONTENT_SCAN_CORE_DETECT: task_config->core_set = false; break; case MANUAL_CONTENT_SCAN_CORE_SET: task_config->core_set = true; if (string_is_empty(scan_settings.core_name)) return false; if (string_is_empty(scan_settings.core_path)) return false; strlcpy( task_config->core_name, scan_settings.core_name, sizeof(task_config->core_name)); strlcpy( task_config->core_path, scan_settings.core_path, sizeof(task_config->core_path)); break; default: return false; } /* Get file extensions list */ task_config->file_exts_custom_set = false; if (!string_is_empty(scan_settings.file_exts_custom)) { task_config->file_exts_custom_set = true; strlcpy( task_config->file_exts, scan_settings.file_exts_custom, sizeof(task_config->file_exts)); } else if (scan_settings.core_type == MANUAL_CONTENT_SCAN_CORE_SET) if (!string_is_empty(scan_settings.file_exts_core)) strlcpy( task_config->file_exts, scan_settings.file_exts_core, sizeof(task_config->file_exts)); /* Our extension lists are space delimited * > dir_list_new() expects vertical bar * delimiters, so find and replace */ if (!string_is_empty(task_config->file_exts)) string_replace_all_chars(task_config->file_exts, ' ', '|'); /* Get DAT file path */ if (!string_is_empty(scan_settings.dat_file_path)) { if (!logiqx_dat_path_is_valid(scan_settings.dat_file_path, NULL)) return false; strlcpy( task_config->dat_file_path, scan_settings.dat_file_path, sizeof(task_config->dat_file_path)); } /* Copy 'search recursively' setting */ task_config->search_recursively = scan_settings.search_recursively; /* Copy 'search inside archives' setting */ task_config->search_archives = scan_settings.search_archives; /* Copy 'DAT file filter' setting */ task_config->filter_dat_content = scan_settings.filter_dat_content; /* Copy 'overwrite playlist' setting */ task_config->overwrite_playlist = scan_settings.overwrite_playlist; /* Copy 'validate_entries' setting */ task_config->validate_entries = scan_settings.validate_entries; return true; } /* Creates a list of all valid content in the specified * content directory * > Returns NULL in the event of failure * > Returned string list must be free()'d */ struct string_list *manual_content_scan_get_content_list( manual_content_scan_task_config_t *task_config) { bool filter_exts; bool include_compressed; struct string_list *dir_list = NULL; /* Sanity check */ if (!task_config) goto error; if (string_is_empty(task_config->content_dir)) goto error; /* Check whether files should be filtered by * extension */ filter_exts = !string_is_empty(task_config->file_exts); /* Check whether compressed files should be * included in the directory list * > If compressed files are already listed in * the 'file_exts' string, they will be included * automatically * > If we don't have a 'file_exts' list, then all * files must be included regardless of type * > If user has enabled 'search inside archives', * then compressed files must of course be included */ include_compressed = (!filter_exts || task_config->search_archives); /* Get directory listing * > Exclude directories and hidden files */ dir_list = dir_list_new( task_config->content_dir, filter_exts ? task_config->file_exts : NULL, false, /* include_dirs */ false, /* include_hidden */ include_compressed, task_config->search_recursively ); /* Sanity check */ if (!dir_list) goto error; if (dir_list->size < 1) goto error; /* Ensure list is in alphabetical order * > Not strictly required, but task status * messages will be unintuitive if we leave * the order 'random' */ dir_list_sort(dir_list, true); return dir_list; error: if (dir_list) string_list_free(dir_list); return NULL; } /* Converts specified content path string to 'real' * file path for use in playlists - i.e. handles * identification of content *inside* archive files. * Returns false if specified content is invalid. */ static bool manual_content_scan_get_playlist_content_path( manual_content_scan_task_config_t *task_config, const char *content_path, int content_type, char *s, size_t len) { size_t _len; struct string_list *archive_list = NULL; /* Sanity check */ if (!task_config || string_is_empty(content_path)) return false; if (!path_is_valid(content_path)) return false; /* In all cases, base content path must be * copied to @s */ _len = strlcpy(s, content_path, len); /* Check whether this is an archive file * requiring special attention... */ if ( (content_type == RARCH_COMPRESSED_ARCHIVE) && task_config->search_archives) { bool filter_exts = !string_is_empty(task_config->file_exts); const char *archive_file = NULL; /* Important note: * > If an archive file of a particular type is * included in the task_config->file_exts list, * dir_list_new() will assign it a file type of * RARCH_PLAIN_FILE * > Thus we will only reach this point if * (a) We are not filtering by extension * (b) This is an archive file type *not* * already included in the supported * extensions list * > These guarantees substantially reduce the * complexity of the following code... */ /* Get archive file contents */ if (!(archive_list = file_archive_get_file_list( content_path, filter_exts ? task_config->file_exts : NULL))) goto error; if (archive_list->size < 1) goto error; /* Get first file contained in archive */ dir_list_sort(archive_list, true); archive_file = archive_list->elems[0].data; if (string_is_empty(archive_file)) goto error; /* Have to take care to ensure that we don't make * a mess of arcade content... * > If we are filtering by extension, then the * archive file itself is *not* valid content, * so link to the first file inside the archive * > If we are not filtering by extension, then: * - If archive contains one valid file, assume * it is a compressed ROM * - If archive contains multiple files, have to * assume it is MAME/FBA-style content, where * only the archive itself is valid */ if (filter_exts || (archive_list->size == 1)) { /* Build path to file inside archive */ s[ _len] = '#'; s[++_len] = '\0'; strlcpy(s + _len, archive_file, len - _len); } string_list_free(archive_list); } return true; error: if (archive_list) string_list_free(archive_list); return false; } /* Extracts content 'label' (name) from content path * > If a DAT file is specified, performs a lookup * of content file name in an attempt to find a * valid 'description' string. * Returns false if specified content is invalid. */ static bool manual_content_scan_get_playlist_content_label( const char *content_path, logiqx_dat_t *dat_file, bool filter_dat_content, char *content_label, size_t len) { /* Sanity check */ if (string_is_empty(content_path)) return false; /* In most cases, content label is just the * filename without extension */ fill_pathname(content_label, path_basename(content_path), "", len); if (string_is_empty(content_label)) return false; /* Check if a DAT file has been specified */ if (dat_file) { bool content_found = false; logiqx_dat_game_info_t game_info; /* Search for current content * > If content is not listed in DAT file, * use existing filename without extension */ if (logiqx_dat_search(dat_file, content_label, &game_info)) { /* BIOS files should always be skipped */ if (game_info.is_bios) return false; /* Only include 'runnable' content */ if (!game_info.is_runnable) return false; /* Copy game description */ if (!string_is_empty(game_info.description)) { strlcpy(content_label, game_info.description, len); content_found = true; } } /* If we are applying a DAT file filter, * unlisted content should be skipped */ if (!content_found && filter_dat_content) return false; } return true; } /* Adds specified content to playlist, if not already * present */ void manual_content_scan_add_content_to_playlist( manual_content_scan_task_config_t *task_config, playlist_t *playlist, const char *content_path, int content_type, logiqx_dat_t *dat_file) { char playlist_content_path[PATH_MAX_LENGTH]; /* Sanity check */ if (!task_config || !playlist) return; /* Get 'actual' content path */ if (!manual_content_scan_get_playlist_content_path( task_config, content_path, content_type, playlist_content_path, sizeof(playlist_content_path))) return; /* Check whether content is already included * in playlist */ if (!playlist_entry_exists(playlist, playlist_content_path)) { struct playlist_entry entry = {0}; char label[PATH_MAX_LENGTH]; label[0] = '\0'; /* Get entry label */ if (!manual_content_scan_get_playlist_content_label( playlist_content_path, dat_file, task_config->filter_dat_content, label, sizeof(label))) return; /* Configure playlist entry * > The push function reads our entry as const, * so these casts are safe */ entry.path = (char*)playlist_content_path; entry.label = label; entry.core_path = (char*)FILE_PATH_DETECT; entry.core_name = (char*)FILE_PATH_DETECT; entry.crc32 = (char*)"00000000|crc"; entry.db_name = task_config->database_name; entry.entry_slot = 0; /* Add entry to playlist */ playlist_push(playlist, &entry); } }