RetroArch/manual_content_scan.c
2023-08-16 01:14:50 +02:00

1389 lines
43 KiB
C

/* 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 <file/file_path.h>
#include <file/archive_file.h>
#include <string/stdstring.h>
#include <lists/dir_list.h>
#include <retro_miscellaneous.h>
#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);
}
}