RetroArch/manual_content_scan.c
2020-01-04 12:04:00 +01:00

1126 lines
34 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
{
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];
enum manual_content_scan_system_name_type system_name_type;
enum manual_content_scan_core_type core_type;
bool search_archives;
bool overwrite_playlist;
} scan_settings_t;
/* 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 = {
"", /* 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 */
MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR, /* system_name_type */
MANUAL_CONTENT_SCAN_CORE_DETECT, /* core_type */
false, /* search_archives */
false /* overwrite_playlist */
};
/*****************/
/* Configuration */
/*****************/
/* Pointer access */
/* 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_archives' bool */
bool *manual_content_scan_get_search_archives_ptr(void)
{
return &scan_settings.search_archives;
}
/* Returns a pointer to the internal
* 'overwrite_playlist' bool */
bool *manual_content_scan_get_overwrite_playlist_ptr(void)
{
return &scan_settings.overwrite_playlist;
}
/* 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)
{
const char *dir_name = NULL;
size_t len;
/* 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)
{
if (scan_settings.content_dir[len - 1] == path_default_slash_c())
scan_settings.content_dir[len - 1] = '\0';
}
else
goto error;
/* 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;
}
/* 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)
return false;
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))
return false;
else
{
*system_name = scan_settings.system_name_database;
return true;
}
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)
return false;
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))
return false;
else
{
*core_name = scan_settings.core_name;
return true;
}
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)
{
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);
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)
{
core_info_t *core_info = NULL;
size_t i;
/* 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++)
{
core_info = NULL;
core_info = core_info_get(core_info_list, i);
if (core_info)
{
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
)
{
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... */
strlcpy(
task_config->database_name,
task_config->system_name,
sizeof(task_config->database_name));
strlcat(
task_config->database_name,
file_path_str(FILE_PATH_LPL_EXTENSION),
sizeof(task_config->database_name));
/* ...which can in turn be used to generate the
* playlist path */
if (string_is_empty(path_dir_playlist))
return false;
fill_pathname_join(
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 */
if (!string_is_empty(scan_settings.file_exts_custom))
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 inside archives' setting */
task_config->search_archives = scan_settings.search_archives;
/* Copy 'overwrite playlist' setting */
task_config->overwrite_playlist = scan_settings.overwrite_playlist;
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)
{
struct string_list *dir_list = NULL;
bool filter_exts;
bool include_compressed;
/* 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
* > Scan recursively */
dir_list = dir_list_new(
task_config->content_dir,
filter_exts ? task_config->file_exts : NULL,
false, /* include_dirs */
false, /* include_hidden */
include_compressed,
true /* recursive */
);
/* 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 *playlist_content_path, 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 playlist_content_path */
strlcpy(playlist_content_path, 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 */
archive_list = file_archive_get_file_list(
content_path, filter_exts ? task_config->file_exts : NULL);
if (!archive_list)
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 */
strlcat(playlist_content_path, "#", len);
strlcat(playlist_content_path, archive_file, 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 and content is an
* archive, 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,
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_short_pathname_representation(
content_label, content_path, len);
if (string_is_empty(content_label))
return false;
/* Check if a DAT file has been specified */
if (dat_file)
{
/* DAT files are only relevant for arcade
* content. We have no idea what kind of
* content we are dealing with here, but
* since arcade ROMs are always archives
* we can at least filter by file type... */
if (path_is_compressed_file(content_path))
{
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);
}
}
}
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,
bool fuzzy_archive_match)
{
char playlist_content_path[PATH_MAX_LENGTH];
playlist_content_path[0] = '\0';
/* 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, fuzzy_archive_match))
{
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,
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*)"DETECT";
entry.core_name = (char*)"DETECT";
entry.crc32 = (char*)"00000000|crc";
entry.db_name = task_config->database_name;
/* Add entry to playlist */
playlist_push(playlist, &entry, fuzzy_archive_match);
}
}