mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-10 03:21:30 -10:00
database_info_get_current_name() returns NULL when the database handle/list is missing, and may also yield a NULL element data pointer. That NULL was passed unguarded into path_basename_nocompression() and then fill_pathname(), where strrchr()/strlcpy() dereference it and crash. This is the top crash in the iOS analytics for 1.22.2 (build 497), reported at task_database.c:862 with the faulting frame symbolicated to fill_pathname+16 (the leading strlcpy(s, in_path, len), with the inlined path_basename_nocompression()/find_last_slash()/strrchr() attributed to the nearest exported symbol). ~1000 affected devices over the last year across both iPhone and iPad. Fixes: - database_info_list_iterate_found_match(): fold db_path into the existing precondition guard. A NULL db_path yields no meaningful playlist filename, so skip the entry (SCAN_VERDICT_ERROR) exactly like the OOM case already handled there. - task_database_check_serial_and_crc(): same NULL name funnels into path_basename_nocompression()/string_starts_with(); guard the name once before use.
2650 lines
94 KiB
C
2650 lines
94 KiB
C
/* RetroArch - A frontend for libretro.
|
|
* Copyright (C) 2011-2017 - Daniel De Matteis
|
|
* Copyright (C) 2014-2017 - Jean-André Santoni
|
|
* Copyright (C) 2016-2019 - Brad Parker
|
|
*
|
|
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
|
* of the GNU General Public License as published by the Free Software Found-
|
|
* ation, either version 3 of the License, or (at your option) any later version.
|
|
*
|
|
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
|
* PURPOSE. See the GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with RetroArch.
|
|
* If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <math.h>
|
|
#include <compat/strcasestr.h>
|
|
#include <compat/strl.h>
|
|
#include <retro_miscellaneous.h>
|
|
#include <string/stdstring.h>
|
|
#include <lists/dir_list.h>
|
|
#include <lists/string_list.h>
|
|
#include <file/file_path.h>
|
|
#include <formats/logiqx_dat.h>
|
|
#include <formats/m3u_file.h>
|
|
#include <encodings/crc32.h>
|
|
#include <streams/interface_stream.h>
|
|
#include "tasks_internal.h"
|
|
|
|
#include "../core_info.h"
|
|
#include "../database_info.h"
|
|
#include "../manual_content_scan.h"
|
|
|
|
#include "../file_path_special.h"
|
|
#include "../msg_hash.h"
|
|
#include "../playlist.h"
|
|
#include "../configuration.h"
|
|
#include "../ui/ui_companion_driver.h"
|
|
#ifdef HAVE_MENU
|
|
#include "../menu/menu_driver.h"
|
|
#endif
|
|
#include "../runloop.h"
|
|
#include "../retroarch.h"
|
|
#include "../verbosity.h"
|
|
#include "task_database_cue.h"
|
|
|
|
#define MAX_DATABASE_COUNT 256
|
|
|
|
/* Scan result structure for accumulating identification results */
|
|
typedef struct scan_result
|
|
{
|
|
char *entry_path; /* Full path to the ROM file */
|
|
char *entry_label; /* Display label (usually game name) */
|
|
char *db_crc; /* CRC or serial identifier */
|
|
char *db_name; /* Database/playlist name (e.g., "Sega - Genesis.lpl") */
|
|
char *archive_name; /* Archive entry name if inside zip, NULL otherwise */
|
|
} scan_result_t;
|
|
|
|
/* Result accumulation infrastructure */
|
|
typedef struct scan_results
|
|
{
|
|
scan_result_t *results; /* Dynamic array of results */
|
|
size_t count; /* Number of results */
|
|
size_t capacity; /* Allocated capacity */
|
|
} scan_results_t;
|
|
|
|
/* Helper functions for result accumulation */
|
|
static bool scan_results_init(scan_results_t *sr, size_t initial_capacity)
|
|
{
|
|
sr->results = (scan_result_t*)malloc(initial_capacity * sizeof(scan_result_t));
|
|
if (!sr->results)
|
|
return false;
|
|
sr->count = 0;
|
|
sr->capacity = initial_capacity;
|
|
return true;
|
|
}
|
|
|
|
static bool scan_results_ensure_capacity(scan_results_t *sr)
|
|
{
|
|
if (sr->count >= sr->capacity)
|
|
{
|
|
size_t new_capacity = sr->capacity * 2;
|
|
scan_result_t *new_results = (scan_result_t*)realloc(
|
|
sr->results, new_capacity * sizeof(scan_result_t));
|
|
if (!new_results)
|
|
return false;
|
|
sr->results = new_results;
|
|
sr->capacity = new_capacity;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool scan_results_add(scan_results_t *sr,
|
|
const char *entry_path, const char *entry_label,
|
|
const char *db_crc, const char *db_name, const char *archive_name)
|
|
{
|
|
scan_result_t *result;
|
|
|
|
if (!scan_results_ensure_capacity(sr))
|
|
return false;
|
|
|
|
result = &sr->results[sr->count];
|
|
result->entry_path = strdup(entry_path);
|
|
result->entry_label = strdup(entry_label);
|
|
result->db_crc = strdup(db_crc);
|
|
result->db_name = strdup(db_name);
|
|
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Adding scan result %d: %s %s\n",sr->count, entry_path, db_name);
|
|
#endif
|
|
|
|
result->archive_name = archive_name ? strdup(archive_name) : NULL;
|
|
|
|
if ( !result->entry_path || !result->entry_label
|
|
|| !result->db_crc || !result->db_name)
|
|
{
|
|
/* Allocation failed, cleanup */
|
|
if (result->entry_path)
|
|
free(result->entry_path);
|
|
if (result->entry_label)
|
|
free(result->entry_label);
|
|
if (result->db_crc)
|
|
free(result->db_crc);
|
|
if (result->db_name)
|
|
free(result->db_name);
|
|
if (result->archive_name)
|
|
free(result->archive_name);
|
|
return false;
|
|
}
|
|
|
|
sr->count++;
|
|
return true;
|
|
}
|
|
|
|
static void scan_results_free(scan_results_t *sr)
|
|
{
|
|
size_t i;
|
|
for (i = 0; i < sr->count; i++)
|
|
{
|
|
if (sr->results[i].entry_path)
|
|
free(sr->results[i].entry_path);
|
|
if (sr->results[i].entry_label)
|
|
free(sr->results[i].entry_label);
|
|
if (sr->results[i].db_crc)
|
|
free(sr->results[i].db_crc);
|
|
if (sr->results[i].db_name)
|
|
free(sr->results[i].db_name);
|
|
if (sr->results[i].archive_name)
|
|
free(sr->results[i].archive_name);
|
|
}
|
|
if (sr->results)
|
|
free(sr->results);
|
|
sr->results = NULL;
|
|
sr->count = 0;
|
|
sr->capacity = 0;
|
|
}
|
|
|
|
enum db_state_flags_enum
|
|
{
|
|
DB_STATE_FLAG_HAS_SERIAL = (1 << 0),
|
|
DB_STATE_FLAG_HAS_CRC = (1 << 1),
|
|
DB_STATE_FLAG_HAS_SIZE = (1 << 2),
|
|
DB_STATE_FLAG_MATCHED = (1 << 3)
|
|
};
|
|
|
|
typedef struct database_state_handle
|
|
{
|
|
database_info_list_t *info;
|
|
struct string_list *list;
|
|
uint8_t *buf;
|
|
size_t list_index;
|
|
size_t entry_index;
|
|
uint32_t crc;
|
|
uint32_t archive_crc;
|
|
uint64_t size;
|
|
uint64_t archive_size;
|
|
char archive_name[512]; /* TODO/FIXME - check size */
|
|
char serial[4096]; /* TODO/FIXME - check size */
|
|
int64_t min_sizes[MAX_DATABASE_COUNT];
|
|
int64_t max_sizes[MAX_DATABASE_COUNT];
|
|
uint8_t flags[MAX_DATABASE_COUNT];
|
|
} database_state_handle_t;
|
|
|
|
enum db_flags_enum
|
|
{
|
|
DB_HANDLE_FLAG_IS_DIRECTORY = (1 << 0),
|
|
DB_HANDLE_FLAG_SCAN_STARTED = (1 << 1),
|
|
DB_HANDLE_FLAG_SCAN_WITHOUT_CORE_MATCH = (1 << 2),
|
|
DB_HANDLE_FLAG_SHOW_HIDDEN_FILES = (1 << 3),
|
|
DB_HANDLE_FLAG_USE_FIRST_MATCH_ONLY = (1 << 4),
|
|
DB_HANDLE_FLAG_DO_MENU_REFRESH = (1 << 5)
|
|
};
|
|
|
|
enum manual_scan_status
|
|
{
|
|
MANUAL_SCAN_BEGIN = 0,
|
|
MANUAL_SCAN_ITERATE_CLEAN,
|
|
DATABASE_SCAN_ITERATE_START,
|
|
DATABASE_SCAN_ITERATE_CONTENT,
|
|
DATABASE_SCAN_ITERATE_NEXT,
|
|
MANUAL_SCAN_ITERATE_CONTENT,
|
|
MANUAL_SCAN_ITERATE_M3U,
|
|
MANUAL_SCAN_END
|
|
};
|
|
|
|
typedef struct manual_scan_handle
|
|
{
|
|
manual_content_scan_task_config_t *task_config;
|
|
playlist_t *playlist;
|
|
struct string_list *file_exts_list;
|
|
struct string_list *content_list;
|
|
logiqx_dat_t *dat_file;
|
|
struct string_list *m3u_list;
|
|
playlist_config_t playlist_config; /* size_t alignment */
|
|
size_t playlist_size;
|
|
size_t playlist_index;
|
|
size_t content_list_index;
|
|
size_t m3u_index;
|
|
enum manual_scan_status status; /* merged, the other status is in dbinfo */
|
|
scan_results_t scan_results;
|
|
char *playlist_directory;
|
|
#ifdef HAVE_LIBRETRODB
|
|
char *content_database_path;
|
|
database_info_handle_t *handle;
|
|
database_state_handle_t state;
|
|
uint8_t flags;
|
|
#endif
|
|
} manual_scan_handle_t;
|
|
|
|
enum scan_verdict
|
|
{
|
|
SCAN_VERDICT_CONTINUE = 0,
|
|
SCAN_VERDICT_MATCHED_DB,
|
|
SCAN_VERDICT_ARCHIVE_CONTENTS_ADDED,
|
|
SCAN_VERDICT_NO_DB_MATCH,
|
|
SCAN_VERDICT_ERROR
|
|
};
|
|
|
|
static void increase_content_list_index(manual_scan_handle_t *manual_scan)
|
|
{
|
|
/* Skip any entries pruned by cue/gdi filters */
|
|
do
|
|
{
|
|
manual_scan->content_list_index++;
|
|
}
|
|
while (manual_scan->content_list_index < manual_scan->content_list->size &&
|
|
!manual_scan->content_list->elems[manual_scan->content_list_index].data);
|
|
}
|
|
|
|
#ifdef HAVE_LIBRETRODB
|
|
|
|
static const char *database_info_get_current_name(
|
|
database_state_handle_t *handle)
|
|
{
|
|
if (!handle || !handle->list)
|
|
return NULL;
|
|
return handle->list->elems[handle->list_index].data;
|
|
}
|
|
|
|
static const char *database_info_get_current_element_name(
|
|
struct string_list *handle, size_t ptr)
|
|
{
|
|
if (!handle || !handle->elems)
|
|
return NULL;
|
|
/* Don't skip pruned entries, otherwise iteration
|
|
* ends prematurely */
|
|
if (!handle->elems[ptr].data)
|
|
return "";
|
|
return handle->elems[ptr].data;
|
|
}
|
|
#endif
|
|
static void task_database_scan_console_output(const char *label, const char *db_name, bool add)
|
|
{
|
|
char string[32];
|
|
const char *prefix = (add) ? "++" : (db_name) ? "==" : "??";
|
|
const char *no_color = getenv("NO_COLOR");
|
|
bool color = (no_color && no_color[0] != '0') ? false : true;
|
|
|
|
/* Colorize prefix (add = green, dupe = yellow, not found = red) */
|
|
#ifdef _WIN32
|
|
HANDLE con = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
if (color && con != INVALID_HANDLE_VALUE)
|
|
{
|
|
unsigned red = FOREGROUND_RED;
|
|
unsigned green = FOREGROUND_GREEN;
|
|
unsigned yellow = FOREGROUND_RED | FOREGROUND_GREEN;
|
|
unsigned reset = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE;
|
|
size_t _len = strlcpy(string, " ", sizeof(string));
|
|
_len += strlcpy(string + _len, prefix, sizeof(string) - _len);
|
|
_len += strlcpy(string + _len, " ", sizeof(string) - _len);
|
|
SetConsoleTextAttribute(con, (add) ? green : (db_name) ? yellow : red);
|
|
WriteConsole(con, string, _len, NULL, NULL);
|
|
SetConsoleTextAttribute(con, reset);
|
|
}
|
|
#else
|
|
if (color)
|
|
{
|
|
const char *red = "\x1B[31m";
|
|
const char *green = "\x1B[32m";
|
|
const char *yellow = "\x1B[33m";
|
|
const char *reset = "\x1B[0m";
|
|
size_t _len = 0;
|
|
if (add)
|
|
_len += strlcpy(string + _len, green, sizeof(string) - _len);
|
|
else
|
|
_len += strlcpy(string + _len, (db_name) ? yellow : red, sizeof(string) - _len);
|
|
_len += strlcpy(string + _len, " ", sizeof(string) - _len);
|
|
_len += strlcpy(string + _len, prefix, sizeof(string) - _len);
|
|
_len += strlcpy(string + _len, " ", sizeof(string) - _len);
|
|
strlcpy(string + _len, reset, sizeof(string) - _len);
|
|
fputs(string, stdout);
|
|
}
|
|
#endif
|
|
else
|
|
{
|
|
size_t _len = strlcpy(string, " ", sizeof(string));
|
|
_len += strlcpy(string + _len, prefix, sizeof(string) - _len);
|
|
strlcpy(string + _len, " ", sizeof(string) - _len);
|
|
fputs(string, stdout);
|
|
}
|
|
|
|
if (!db_name)
|
|
printf("\"%s\"\n", label);
|
|
else
|
|
printf("\"%s / %s\"\n", db_name, label);
|
|
}
|
|
#ifdef HAVE_LIBRETRODB
|
|
static enum scan_verdict task_database_iterate_start(retro_task_t *task,
|
|
database_info_handle_t *db,
|
|
const char *name)
|
|
{
|
|
char msg[128];
|
|
const char *basename_path = (name && *name)
|
|
? path_basename_nocompression(name) : "";
|
|
manual_scan_handle_t *manual_scan = NULL;
|
|
|
|
msg[0] = '\0';
|
|
|
|
if (!task)
|
|
return SCAN_VERDICT_ERROR;
|
|
|
|
if (!(manual_scan = (manual_scan_handle_t*)task->state))
|
|
return SCAN_VERDICT_ERROR;
|
|
|
|
if (basename_path && *basename_path)
|
|
snprintf(msg, sizeof(msg),
|
|
STRING_REP_USIZE "/" STRING_REP_USIZE ": \"%s\"...\n",
|
|
manual_scan->content_list_index + 1,
|
|
(size_t)manual_scan->content_list->size,
|
|
basename_path);
|
|
|
|
if (*msg)
|
|
{
|
|
task_free_title(task);
|
|
task_set_title(task, strdup(msg));
|
|
if (manual_scan->content_list->size != 0)
|
|
task_set_progress(task,
|
|
roundf((float)manual_scan->content_list_index /
|
|
((float)manual_scan->content_list->size / 100.0f)));
|
|
RARCH_LOG("[Scanner] %s", msg);
|
|
if (retroarch_override_setting_is_set(RARCH_OVERRIDE_SETTING_DATABASE_SCAN, NULL))
|
|
printf("%s", msg);
|
|
}
|
|
|
|
db->status = DATABASE_STATUS_ITERATE;
|
|
|
|
return SCAN_VERDICT_CONTINUE;
|
|
}
|
|
|
|
static void task_database_cue_prune(struct string_list *list,
|
|
const char *name)
|
|
{
|
|
size_t i;
|
|
char path[PATH_MAX_LENGTH];
|
|
intfstream_t *fd = intfstream_open_file(name,
|
|
RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE);
|
|
|
|
if (!fd)
|
|
return;
|
|
|
|
path[0] = '\0';
|
|
|
|
while (cue_next_file(fd, name, path, sizeof(path)))
|
|
{
|
|
/* change in filtering: start from 0 */
|
|
for (i = 0; i < list->size; ++i)
|
|
{
|
|
if (list->elems[i].data
|
|
&& string_is_equal(path, list->elems[i].data))
|
|
{
|
|
RARCH_DBG("[Scanner] Pruning file referenced by CUE: \"%s\".\n", path);
|
|
free(list->elems[i].data);
|
|
list->elems[i].data = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
intfstream_close(fd);
|
|
free(fd);
|
|
}
|
|
#endif
|
|
|
|
/* Remove disc indicators from title string */
|
|
/* Helper function to validate if a string is a valid disc indicator
|
|
* Valid formats:
|
|
* - Single/double digit: 0-99
|
|
* - Single letter: A-Z
|
|
* - Roman numerals: I, II, III, IV, V, VI, VII, VIII, IX, X, etc.
|
|
* - "X of Y" format: 1 of 2, 01 of 10, etc.
|
|
*/
|
|
static bool is_valid_disc_indicator(const char *str, size_t len)
|
|
{
|
|
const char *p = str;
|
|
const char *end = str + len;
|
|
|
|
if (len == 0 || len > 10) /* Sanity check */
|
|
return false;
|
|
|
|
/* Check for single letter (A-Z) */
|
|
if (len == 1 && isalpha((unsigned char)*p))
|
|
return true;
|
|
|
|
/* Check for 1-2 digit number (0-99) */
|
|
if (len <= 2 && isdigit((unsigned char)*p))
|
|
{
|
|
p++;
|
|
if (p == end)
|
|
return true; /* Single digit */
|
|
if (isdigit((unsigned char)*p) && p + 1 == end)
|
|
return true; /* Double digit */
|
|
return false;
|
|
}
|
|
|
|
/* Check for "X of Y" pattern where X and Y are 1-2 digits */
|
|
if (len >= 5 && isdigit((unsigned char)*p))
|
|
{
|
|
/* Parse first number (1-2 digits) */
|
|
p++;
|
|
if (p < end && isdigit((unsigned char)*p))
|
|
p++;
|
|
|
|
/* Check for " of " */
|
|
if (p + 4 <= end && strncmp(p, " of ", 4) == 0)
|
|
{
|
|
p += 4;
|
|
/* Parse second number (1-2 digits) */
|
|
if (p < end && isdigit((unsigned char)*p))
|
|
{
|
|
p++;
|
|
if (p < end && isdigit((unsigned char)*p))
|
|
p++;
|
|
if (p == end)
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Check for Roman numerals (I, II, III, IV, V, VI, VII, VIII, IX, X, etc.) */
|
|
/* Valid Roman numeral chars: I, V, X (we'll be conservative) */
|
|
if (len >= 1 && len <= 4)
|
|
{
|
|
bool all_roman = true;
|
|
const char *roman_p = str;
|
|
while (roman_p < end)
|
|
{
|
|
char c = *roman_p;
|
|
if (c != 'I' && c != 'V' && c != 'X')
|
|
{
|
|
all_roman = false;
|
|
break;
|
|
}
|
|
roman_p++;
|
|
}
|
|
if (all_roman)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static void remove_disc_indicators(char *title, size_t len)
|
|
{
|
|
char *disc_pos = NULL;
|
|
|
|
/* Search for common disc patterns */
|
|
if ( (disc_pos = strstr(title, " (Disc "))
|
|
|| (disc_pos = strstr(title, " (disc "))
|
|
|| (disc_pos = strstr(title, " (Disk "))
|
|
|| (disc_pos = strstr(title, " (disk "))
|
|
/* Tape and floppy releases usually do not follow naming convention, so skip the space */
|
|
|| (disc_pos = strstr(title, "(Tape "))
|
|
|| (disc_pos = strstr(title, "(tape "))
|
|
|| (disc_pos = strstr(title, "(Side "))
|
|
|| (disc_pos = strstr(title, "(side ")))
|
|
{
|
|
/* Find the closing parenthesis */
|
|
char *end_pos = strchr(disc_pos, ')');
|
|
if (end_pos)
|
|
{
|
|
/* Extract the disc indicator text (between " (Disc " and ")") */
|
|
const char *indicator_start = disc_pos + 7; /* Skip " (Disc " */
|
|
size_t indicator_len = end_pos - indicator_start;
|
|
|
|
/* Validate this is actually a disc indicator, not arbitrary text */
|
|
if (is_valid_disc_indicator(indicator_start, indicator_len))
|
|
{
|
|
/* Truncate at the disc indicator */
|
|
*disc_pos = '\0';
|
|
/* Remove trailing whitespace */
|
|
string_trim_whitespace_right(title);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void task_database_iterate_m3u(
|
|
manual_scan_handle_t *_db,
|
|
const char *m3u_path)
|
|
{
|
|
size_t i, j;
|
|
bool found_match = false;
|
|
char first_matched_db[NAME_MAX_LENGTH];
|
|
char first_matched_crc[128];
|
|
char collapsed_title[NAME_MAX_LENGTH];
|
|
m3u_file_t *m3u_file = NULL;
|
|
|
|
first_matched_db[0] = '\0';
|
|
first_matched_crc[0] = '\0';
|
|
collapsed_title[0] = '\0';
|
|
|
|
/* Open M3U file */
|
|
if (!(m3u_file = m3u_file_init(m3u_path)))
|
|
{
|
|
RARCH_ERR("[Scanner] Failed to open M3U file: \"%s\".\n", m3u_path);
|
|
return;
|
|
}
|
|
|
|
/* Scan each referenced file and check if it's in scan_results */
|
|
for (i = 0; i < m3u_file_get_size(m3u_file); i++)
|
|
{
|
|
m3u_file_entry_t *entry = NULL;
|
|
const char *ref_path = NULL;
|
|
|
|
if (!m3u_file_get_entry(m3u_file, i, &entry))
|
|
continue;
|
|
|
|
ref_path = entry->full_path;
|
|
if (!ref_path || !*ref_path)
|
|
continue;
|
|
|
|
/* Look for this file in scan results */
|
|
for (j = 0; j < _db->scan_results.count; j++)
|
|
{
|
|
scan_result_t *result = &_db->scan_results.results[j];
|
|
char result_path_resolved[PATH_MAX_LENGTH];
|
|
|
|
result_path_resolved[0] = '\0';
|
|
|
|
if (!result->entry_path)
|
|
continue;
|
|
|
|
/* Resolve the scan result path to absolute form for comparison */
|
|
strlcpy(result_path_resolved, result->entry_path,
|
|
sizeof(result_path_resolved));
|
|
path_resolve_realpath(result_path_resolved,
|
|
sizeof(result_path_resolved), false);
|
|
|
|
if (string_is_equal(ref_path, result_path_resolved))
|
|
{
|
|
/* Found a match! */
|
|
if (!found_match)
|
|
{
|
|
/* First match - save the info */
|
|
found_match = true;
|
|
strlcpy(first_matched_db, result->db_name,
|
|
sizeof(first_matched_db));
|
|
strlcpy(first_matched_crc, result->db_crc,
|
|
sizeof(first_matched_crc));
|
|
strlcpy(collapsed_title, result->entry_label,
|
|
sizeof(collapsed_title));
|
|
|
|
/* Remove disc indicator from title */
|
|
remove_disc_indicators(collapsed_title,
|
|
sizeof(collapsed_title));
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Collapsing m3u entry: %s %s -> %s\n",
|
|
result->entry_label, result->db_name, collapsed_title);
|
|
#endif
|
|
/* Mark this result for removal */
|
|
/* We'll remove it by setting entry_path to NULL */
|
|
/* and compacting the array later */
|
|
if (result->entry_path)
|
|
{
|
|
free(result->entry_path);
|
|
result->entry_path = NULL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
m3u_file_free(m3u_file);
|
|
|
|
/* If we found at least one match, add M3U entry */
|
|
if (found_match)
|
|
{
|
|
if (!scan_results_add(&_db->scan_results, m3u_path, collapsed_title,
|
|
first_matched_crc, first_matched_db, NULL))
|
|
{
|
|
RARCH_ERR("[Scanner] Failed to add M3U result: \"%s\".\n", m3u_path);
|
|
}
|
|
else
|
|
{
|
|
RARCH_LOG("[Scanner] Matched M3U \"%s\" to \"%s\".\n",
|
|
collapsed_title, first_matched_db);
|
|
}
|
|
}
|
|
|
|
/* Compact scan_results to remove NULL entries */
|
|
{
|
|
size_t write_idx = 0;
|
|
for (i = 0; i < _db->scan_results.count; i++)
|
|
{
|
|
if (_db->scan_results.results[i].entry_path != NULL)
|
|
{
|
|
if (write_idx != i)
|
|
_db->scan_results.results[write_idx] =
|
|
_db->scan_results.results[i];
|
|
write_idx++;
|
|
}
|
|
else
|
|
{
|
|
/* Free any remaining allocated fields */
|
|
if (_db->scan_results.results[i].entry_label)
|
|
free(_db->scan_results.results[i].entry_label);
|
|
if (_db->scan_results.results[i].db_crc)
|
|
free(_db->scan_results.results[i].db_crc);
|
|
if (_db->scan_results.results[i].db_name)
|
|
free(_db->scan_results.results[i].db_name);
|
|
if (_db->scan_results.results[i].archive_name)
|
|
free(_db->scan_results.results[i].archive_name);
|
|
}
|
|
}
|
|
_db->scan_results.count = write_idx;
|
|
}
|
|
}
|
|
|
|
#ifdef HAVE_LIBRETRODB
|
|
static void gdi_prune(struct string_list *list, const char *name)
|
|
{
|
|
size_t i;
|
|
char path[PATH_MAX_LENGTH];
|
|
intfstream_t *fd = intfstream_open_file(name,
|
|
RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE);
|
|
|
|
if (!fd)
|
|
return;
|
|
|
|
path[0] = '\0';
|
|
|
|
while (gdi_next_file(fd, name, path, sizeof(path)))
|
|
{
|
|
/* change in filtering */
|
|
for (i = 0; i < list->size; ++i)
|
|
{
|
|
if (list->elems[i].data
|
|
&& string_is_equal(path, list->elems[i].data))
|
|
{
|
|
RARCH_DBG("[Scanner] Pruning file referenced by GDI: \"%s\".\n", path);
|
|
free(list->elems[i].data);
|
|
list->elems[i].data = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
free(fd);
|
|
}
|
|
|
|
static enum msg_file_type extension_to_file_type(const char *ext)
|
|
{
|
|
char ext_lower[6];
|
|
strlcpy(ext_lower, ext, sizeof(ext_lower));
|
|
string_to_lower(ext_lower);
|
|
|
|
if (
|
|
memcmp(ext_lower, "7z", 3) == 0
|
|
|| memcmp(ext_lower, "zip", 4) == 0
|
|
|| memcmp(ext_lower, "apk", 4) == 0
|
|
|| memcmp(ext_lower, "zst", 4) == 0
|
|
)
|
|
return FILE_TYPE_COMPRESSED;
|
|
if (memcmp(ext_lower, "cue", 4) == 0)
|
|
return FILE_TYPE_CUE;
|
|
if (memcmp(ext_lower, "gdi", 4) == 0)
|
|
return FILE_TYPE_GDI;
|
|
if (memcmp(ext_lower, "iso", 4) == 0)
|
|
return FILE_TYPE_ISO;
|
|
if (memcmp(ext_lower, "chd", 4) == 0)
|
|
return FILE_TYPE_CHD;
|
|
if (memcmp(ext_lower, "wbfs", 5) == 0)
|
|
return FILE_TYPE_WBFS;
|
|
if (memcmp(ext_lower, "rvz", 4) == 0)
|
|
return FILE_TYPE_RVZ;
|
|
if (memcmp(ext_lower, "wia", 4) == 0)
|
|
return FILE_TYPE_WIA;
|
|
if (memcmp(ext_lower, "pbp", 4) == 0)
|
|
return FILE_TYPE_PBP;
|
|
if (memcmp(ext_lower, "lutro", 6) == 0)
|
|
return FILE_TYPE_LUTRO;
|
|
return FILE_TYPE_NONE;
|
|
}
|
|
|
|
static int task_database_iterate_playlist(
|
|
manual_scan_handle_t *_db,
|
|
database_state_handle_t *db_state,
|
|
database_info_handle_t *db, const char *name)
|
|
{
|
|
switch (extension_to_file_type(path_get_extension(name)))
|
|
{
|
|
case FILE_TYPE_COMPRESSED:
|
|
#ifdef HAVE_COMPRESSION
|
|
db->type = DATABASE_TYPE_CRC_LOOKUP;
|
|
/* first check crc of archive itself */
|
|
return intfstream_file_get_crc_and_size(name,
|
|
0, INT64_MAX, &db_state->archive_crc,
|
|
&db_state->archive_size);
|
|
#else
|
|
break;
|
|
#endif
|
|
case FILE_TYPE_CUE:
|
|
task_database_cue_prune(_db->content_list, name);
|
|
db_state->serial[0] = '\0';
|
|
if (task_database_cue_get_serial(name, db_state->serial,
|
|
sizeof(db_state->serial),&db_state->size))
|
|
db->type = DATABASE_TYPE_SERIAL_LOOKUP;
|
|
else
|
|
{
|
|
db->type = DATABASE_TYPE_CRC_LOOKUP;
|
|
db_state->serial[0] = '\0';
|
|
RARCH_DBG("[Scanner] CUE file serial not detected, fallback to crc.\n");
|
|
return task_database_cue_get_crc_and_size(name, &db_state->crc, &db_state->size);
|
|
}
|
|
break;
|
|
case FILE_TYPE_GDI:
|
|
gdi_prune(_db->content_list, name);
|
|
db_state->serial[0] = '\0';
|
|
if (task_database_gdi_get_serial(name, db_state->serial,
|
|
sizeof(db_state->serial),&db_state->size))
|
|
db->type = DATABASE_TYPE_SERIAL_LOOKUP;
|
|
else
|
|
{
|
|
db->type = DATABASE_TYPE_CRC_LOOKUP;
|
|
db_state->serial[0] = '\0';
|
|
RARCH_DBG("[Scanner] GDI file serial not detected, fallback to crc.\n");
|
|
return task_database_gdi_get_crc_and_size(name, &db_state->crc, &db_state->size);
|
|
}
|
|
break;
|
|
/* Consider WBFS, RVZ and WIA files similar to ISO files. */
|
|
case FILE_TYPE_WBFS:
|
|
case FILE_TYPE_RVZ:
|
|
case FILE_TYPE_WIA:
|
|
db_state->serial[0] = '\0';
|
|
intfstream_file_get_serial(name, 0, INT64_MAX, db_state->serial, sizeof(db_state->serial),&db_state->size);
|
|
db->type = DATABASE_TYPE_SERIAL_LOOKUP;
|
|
break;
|
|
case FILE_TYPE_ISO:
|
|
db_state->serial[0] = '\0';
|
|
intfstream_file_get_serial(name, 0, INT64_MAX, db_state->serial, sizeof(db_state->serial),&db_state->size);
|
|
db->type = DATABASE_TYPE_SERIAL_LOOKUP_SIZEHINT;
|
|
break;
|
|
case FILE_TYPE_CHD:
|
|
db_state->serial[0] = '\0';
|
|
if (task_database_chd_get_serial(name, db_state->serial, sizeof(db_state->serial),&db_state->size))
|
|
db->type = DATABASE_TYPE_SERIAL_LOOKUP;
|
|
else
|
|
{
|
|
db->type = DATABASE_TYPE_CRC_LOOKUP;
|
|
db_state->serial[0] = '\0';
|
|
RARCH_DBG("[Scanner] CHD file serial not detected, fallback to crc.\n");
|
|
return task_database_chd_get_crc_and_size(name, &db_state->crc, &db_state->size);
|
|
}
|
|
break;
|
|
case FILE_TYPE_PBP:
|
|
db_state->serial[0] = '\0';
|
|
if (task_database_pbp_get_serial(name, db_state->serial, sizeof(db_state->serial),&db_state->size))
|
|
db->type = DATABASE_TYPE_SERIAL_LOOKUP;
|
|
else
|
|
{
|
|
db->type = DATABASE_TYPE_CRC_LOOKUP;
|
|
db_state->serial[0] = '\0';
|
|
RARCH_DBG("[Scanner] PBP file serial not detected, fallback to crc.\n");
|
|
return intfstream_file_get_crc_and_size(name, 0, INT64_MAX, &db_state->crc, &db_state->size);
|
|
}
|
|
break;
|
|
case FILE_TYPE_LUTRO:
|
|
db->type = DATABASE_TYPE_ITERATE_LUTRO;
|
|
break;
|
|
default:
|
|
db_state->serial[0] = '\0';
|
|
db->type = DATABASE_TYPE_CRC_LOOKUP;
|
|
return intfstream_file_get_crc_and_size(name, 0, INT64_MAX, &db_state->crc, &db_state->size);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
#endif
|
|
static bool add_files_from_archive(manual_scan_handle_t *_db,
|
|
const char *path)
|
|
{
|
|
bool archive_added = false;
|
|
struct string_list *archive_list =
|
|
file_archive_get_file_list(path,
|
|
(*_db->task_config->file_exts) ? _db->task_config->file_exts : NULL);
|
|
|
|
if (archive_list && archive_list->size > 0)
|
|
{
|
|
unsigned i;
|
|
size_t _len = strlen(path);
|
|
|
|
for (i = 0; i < archive_list->size; i++)
|
|
{
|
|
archive_added = true;
|
|
if (_len + strlen(archive_list->elems[i].data)
|
|
+ 1 < PATH_MAX_LENGTH)
|
|
{
|
|
char new_path[PATH_MAX_LENGTH];
|
|
strlcpy(new_path, path, sizeof(new_path));
|
|
new_path[_len] = '#';
|
|
strlcpy(new_path + _len + 1,
|
|
archive_list->elems[i].data,
|
|
sizeof(new_path) - _len);
|
|
string_list_append(_db->content_list, new_path,
|
|
archive_list->elems[i].attr);
|
|
}
|
|
else
|
|
string_list_append(_db->content_list, path,
|
|
archive_list->elems[i].attr);
|
|
}
|
|
string_list_free(archive_list);
|
|
}
|
|
return archive_added;
|
|
}
|
|
#ifdef HAVE_LIBRETRODB
|
|
static enum scan_verdict database_info_list_iterate_end_no_match(
|
|
manual_scan_handle_t *_db,
|
|
database_info_handle_t *db,
|
|
database_state_handle_t *db_state,
|
|
const char *path,
|
|
bool path_contains_compressed_file)
|
|
{
|
|
bool archive_added = false;
|
|
/* Reached end of database list,
|
|
* CRC match probably didn't succeed. */
|
|
if (retroarch_override_setting_is_set(
|
|
RARCH_OVERRIDE_SETTING_DATABASE_SCAN, NULL))
|
|
task_database_scan_console_output(path, NULL, false);
|
|
|
|
/* If this was a compressed file and no match in the database
|
|
* list was found then expand the search list to include the
|
|
* archive's contents. */
|
|
if (!path_contains_compressed_file && path_is_compressed_file(path) && _db->task_config->search_archives)
|
|
{
|
|
archive_added=add_files_from_archive(_db, path);
|
|
}
|
|
else
|
|
RARCH_LOG("[Scanner] No match for: \"%s\" (%s %08X).\n", path,
|
|
db_state->serial, db_state->crc);
|
|
|
|
db_state->list_index = 0;
|
|
db_state->entry_index = 0;
|
|
db_state->size = 0;
|
|
db_state->archive_size = 0;
|
|
db_state->serial[0] = '\0';
|
|
|
|
if (db_state->crc != 0)
|
|
db_state->crc = 0;
|
|
|
|
if (db_state->archive_crc != 0)
|
|
db_state->archive_crc = 0;
|
|
|
|
return archive_added ? SCAN_VERDICT_ARCHIVE_CONTENTS_ADDED : SCAN_VERDICT_NO_DB_MATCH;
|
|
}
|
|
|
|
static int database_info_list_iterate_new(
|
|
database_state_handle_t *db_state,
|
|
const char *query)
|
|
{
|
|
const char *new_database = database_info_get_current_name(db_state);
|
|
|
|
if (db_state->info)
|
|
{
|
|
database_info_list_free(db_state->info);
|
|
free(db_state->info);
|
|
}
|
|
db_state->info = database_info_list_new_filtered(
|
|
new_database, query, DB_EXTRACT_SCAN_FIELDS);
|
|
return 0;
|
|
}
|
|
|
|
static enum scan_verdict database_info_list_iterate_found_match(
|
|
manual_scan_handle_t *_db,
|
|
database_state_handle_t *db_state,
|
|
database_info_handle_t *db,
|
|
const char *archive_name
|
|
)
|
|
{
|
|
char entry_lbl[128];
|
|
char db_playlist_base_str[NAME_MAX_LENGTH];
|
|
/* TODO/FIXME - heap allocations are done here to avoid
|
|
* running out of stack space on systems with a limited stack size.
|
|
* We should use less fullsize paths in the future so that we don't
|
|
* need to have all these big char arrays here */
|
|
size_t str_len = PATH_MAX_LENGTH * sizeof(char);
|
|
char* db_crc = (char*)malloc(str_len); /* this is needlessly large */
|
|
char* entry_path_str = (char*)malloc(str_len);
|
|
char *hash = NULL;
|
|
const char *db_path =
|
|
database_info_get_current_name(db_state);
|
|
const char *entry_path =
|
|
database_info_get_current_element_name(_db->content_list, _db->content_list_index);
|
|
database_info_t *db_info_entry =
|
|
&db_state->info->list[db_state->entry_index];
|
|
|
|
/* NULL-check both mallocs: the 'db_crc[0] = ...' /
|
|
* 'entry_path_str[0] = ...' writes below NULL-deref on OOM.
|
|
* free(NULL) in the teardown at the end of the function is
|
|
* safe, so we can bail early; clean up whichever succeeded
|
|
* and return SCAN_VERDICT_ERROR so the scanner continues
|
|
* with the next content entry rather than silently
|
|
* mismatching.
|
|
*
|
|
* db_path is also checked here: database_info_get_current_name()
|
|
* returns NULL when the database handle/list is missing (and the
|
|
* matched element's data may itself be NULL). A NULL db_path is
|
|
* fed straight into path_basename_nocompression()/fill_pathname()
|
|
* below, where strrchr()/strlcpy() dereference it and crash. With
|
|
* no database name there is no meaningful playlist filename to
|
|
* build, so treat it like the OOM case and skip this entry. */
|
|
if (!db_crc || !entry_path_str || !db_path)
|
|
{
|
|
free(db_crc);
|
|
free(entry_path_str);
|
|
return SCAN_VERDICT_ERROR;
|
|
}
|
|
|
|
db_crc[0] = '\0';
|
|
entry_path_str[0] = '\0';
|
|
|
|
fill_pathname(db_playlist_base_str,
|
|
path_basename_nocompression(db_path), ".lpl", sizeof(db_playlist_base_str));
|
|
|
|
if (*db_state->serial)
|
|
{
|
|
size_t _len = strlcpy(db_crc, db_state->serial, str_len);
|
|
strlcpy(db_crc + _len,
|
|
"|serial",
|
|
str_len - _len);
|
|
}
|
|
else
|
|
snprintf(db_crc, str_len, "%08lX|crc",
|
|
(unsigned long)db_info_entry->crc32);
|
|
|
|
if (entry_path)
|
|
strlcpy(entry_path_str, entry_path, str_len);
|
|
|
|
/* Use database name for label if found,
|
|
* otherwise use filename without extension */
|
|
if (db_info_entry->name && *db_info_entry->name)
|
|
{
|
|
/* Use the archive as path instead of the file inside the archive
|
|
* if the file is a multidisk game, because database entry
|
|
* matches with the last disk, which is never bootable */
|
|
char *delim = (char*)strchr(entry_path_str, '#');
|
|
|
|
if (delim && strcasestr(entry_path_str, " (Disk "))
|
|
*delim = '\0';
|
|
|
|
strlcpy(entry_lbl, db_info_entry->name, sizeof(entry_lbl));
|
|
}
|
|
else if (entry_path && *entry_path)
|
|
{
|
|
char *delim = (char*)strchr(entry_path, '#');
|
|
|
|
if (delim)
|
|
*delim = '\0';
|
|
fill_pathname(entry_lbl,
|
|
path_basename_nocompression(entry_path), "", sizeof(entry_lbl));
|
|
|
|
RARCH_LOG("[Scanner] Faulty match for: \"%s\", CRC: 0x%08X.\n", entry_path_str, db_state->crc);
|
|
}
|
|
|
|
if (archive_name && *archive_name)
|
|
fill_pathname_join_delim(entry_path_str,
|
|
entry_path_str, archive_name, '#', str_len);
|
|
|
|
if (core_info_database_match_archive_member(
|
|
db_state->list->elems[db_state->list_index].data)
|
|
&& (hash = strchr(entry_path_str, '#')))
|
|
*hash = '\0';
|
|
|
|
/* Accumulate result instead of immediately updating playlist */
|
|
|
|
if (!scan_results_add(&_db->scan_results, entry_path_str, entry_lbl, db_crc,
|
|
_db->task_config->omit_db_reference ? _db->task_config->dat_file_path : db_playlist_base_str,
|
|
archive_name))
|
|
RARCH_ERR("[Scanner] Failed to add result for: \"%s\".\n", entry_lbl);
|
|
|
|
database_info_list_free(db_state->info);
|
|
free(db_state->info);
|
|
|
|
db_state->info = NULL;
|
|
db_state->crc = 0;
|
|
db_state->archive_crc = 0;
|
|
db_state->size = 0;
|
|
db_state->archive_size = 0;
|
|
db_state->serial[0] = '\0';
|
|
|
|
/* Move database to start since we are likely to match against it
|
|
again */
|
|
if (db_state->list_index != 0)
|
|
{
|
|
struct string_list_elem entry =
|
|
db_state->list->elems[db_state->list_index];
|
|
uint64_t min = db_state->min_sizes[db_state->list_index];
|
|
uint64_t max = db_state->max_sizes[db_state->list_index];
|
|
uint8_t flag = db_state->flags[db_state->list_index];
|
|
memmove(&db_state->list->elems[1],
|
|
&db_state->list->elems[0],
|
|
sizeof(entry) * db_state->list_index);
|
|
memmove(&db_state->min_sizes[1],
|
|
&db_state->min_sizes[0],
|
|
sizeof(min) * db_state->list_index);
|
|
memmove(&db_state->max_sizes[1],
|
|
&db_state->max_sizes[0],
|
|
sizeof(max) * db_state->list_index);
|
|
memmove(&db_state->flags[1],
|
|
&db_state->flags[0],
|
|
sizeof(flag) * db_state->list_index);
|
|
|
|
db_state->list->elems[0] = entry;
|
|
db_state->min_sizes[0] = min;
|
|
db_state->max_sizes[0] = max;
|
|
db_state->flags[0] = flag;
|
|
db_state->flags[0] |= DB_STATE_FLAG_MATCHED;
|
|
}
|
|
|
|
free(db_crc);
|
|
free(entry_path_str);
|
|
return SCAN_VERDICT_MATCHED_DB;
|
|
}
|
|
|
|
/* End of entries in database info list and didn't find a
|
|
* match, go to the next database. */
|
|
static enum scan_verdict database_info_list_iterate_next(
|
|
database_state_handle_t *db_state)
|
|
{
|
|
db_state->list_index++;
|
|
db_state->entry_index = 0;
|
|
|
|
database_info_list_free(db_state->info);
|
|
free(db_state->info);
|
|
db_state->info = NULL;
|
|
|
|
return SCAN_VERDICT_CONTINUE;
|
|
}
|
|
|
|
static void task_database_fill_db_min_max(database_state_handle_t *db_state)
|
|
{
|
|
char query[50];
|
|
query[0] = '\0';
|
|
|
|
snprintf(query, sizeof(query), "{size:min(0)}");
|
|
database_info_list_iterate_new(db_state, query);
|
|
|
|
if (db_state->info->count > 0)
|
|
{
|
|
db_state->min_sizes[db_state->list_index] = db_state->info->list[db_state->info->count-1].size;
|
|
snprintf(query, sizeof(query), "{size:max(0)}");
|
|
database_info_list_iterate_new(db_state, query);
|
|
|
|
if (db_state->info->count > 0)
|
|
{
|
|
size_t i;
|
|
db_state->max_sizes[db_state->list_index] = db_state->info->list[db_state->info->count-1].size;
|
|
db_state->flags[db_state->list_index] |= DB_STATE_FLAG_HAS_SIZE;
|
|
for(i=0 ; i < db_state->info->count; i++)
|
|
{
|
|
if ( db_state->info->list[i].serial
|
|
&& strlen(db_state->info->list[i].serial)>0)
|
|
db_state->flags[db_state->list_index] |= DB_STATE_FLAG_HAS_SERIAL;
|
|
if (db_state->info->list[i].crc32 > 0)
|
|
db_state->flags[db_state->list_index] |= DB_STATE_FLAG_HAS_CRC;
|
|
}
|
|
}
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Queried min/max, values %ld / %ld, size %s serial %s crc %s\n",
|
|
db_state->min_sizes[db_state->list_index],
|
|
db_state->max_sizes[db_state->list_index],
|
|
db_state->flags[db_state->list_index] & DB_STATE_FLAG_HAS_SIZE ? "yes" : "no",
|
|
db_state->flags[db_state->list_index] & DB_STATE_FLAG_HAS_SERIAL ? "yes" : "no",
|
|
db_state->flags[db_state->list_index] & DB_STATE_FLAG_HAS_CRC ? "yes" : "no");
|
|
#endif
|
|
}
|
|
/* Unsuccessful query (no size info), use placeholder */
|
|
else
|
|
{
|
|
db_state->min_sizes[db_state->list_index] = -1;
|
|
db_state->max_sizes[db_state->list_index] = -1;
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Queried min/max, size field not found.\n");
|
|
#endif
|
|
}
|
|
db_state->entry_index = 0;
|
|
}
|
|
|
|
static enum scan_verdict task_database_iterate_crc_lookup(
|
|
manual_scan_handle_t *_db,
|
|
database_state_handle_t *db_state,
|
|
database_info_handle_t *db,
|
|
const char *name,
|
|
const char *archive_entry,
|
|
bool path_contains_compressed_file)
|
|
{
|
|
if ( !db_state->list
|
|
|| (unsigned)db_state->list_index == (unsigned)db_state->list->size
|
|
|| ( _db->flags & DB_HANDLE_FLAG_USE_FIRST_MATCH_ONLY &&
|
|
db_state->list_index > 0 &&
|
|
db_state->flags[0] & DB_STATE_FLAG_MATCHED))
|
|
return database_info_list_iterate_end_no_match(_db, db, db_state, name,
|
|
path_contains_compressed_file);
|
|
|
|
/* Archive did not contain a CRC for this entry,
|
|
* or the file is empty. */
|
|
if (!db_state->crc)
|
|
{
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Extra crc check 1: %x %d / %x %d %s\n",
|
|
db_state->crc, db_state->size, db_state->archive_crc, db_state->archive_size,
|
|
path_contains_compressed_file ? "compressed:true" : "compressed:false");
|
|
#endif
|
|
db_state->crc = file_archive_get_file_crc32_and_size(name, &db_state->size);
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Extra crc check 2: %x %d / %x %d.\n",
|
|
db_state->crc, db_state->size, db_state->archive_crc, db_state->archive_size);
|
|
#endif
|
|
if (!db_state->crc)
|
|
return database_info_list_iterate_next(db_state);
|
|
}
|
|
|
|
/* If size boundaries are not filled for this DB, run the queries */
|
|
if (db_state->min_sizes[db_state->list_index] == 0)
|
|
task_database_fill_db_min_max(db_state);
|
|
|
|
if (db_state->min_sizes[db_state->list_index] > 0)
|
|
{
|
|
/* Examining zip file main entry (archive size filled, but no indication of compressed file) */
|
|
if ( !path_contains_compressed_file && db_state->archive_size > 0)
|
|
{
|
|
if ( ( db_state->min_sizes[db_state->list_index] > (int64_t) db_state->archive_size
|
|
&& db_state->min_sizes[db_state->list_index] > (int64_t) db_state->size )
|
|
|| ( db_state->max_sizes[db_state->list_index] < (int64_t) db_state->archive_size
|
|
&& db_state->max_sizes[db_state->list_index] < (int64_t) db_state->size ))
|
|
{
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Skipping DB, neither archive nor uncompressed size %ld/%ld is in range.\n",
|
|
db_state->archive_size, db_state->size);
|
|
#endif
|
|
return database_info_list_iterate_next(db_state);
|
|
}
|
|
}
|
|
/* Any other case (non-archive file, or a file inside the archive */
|
|
else if ( db_state->size > 0
|
|
&& ( db_state->min_sizes[db_state->list_index] > (int64_t) db_state->size
|
|
|| db_state->max_sizes[db_state->list_index] < (int64_t) db_state->size))
|
|
{
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Skipping DB, file size %ld not in range.\n", db_state->size);
|
|
#endif
|
|
return database_info_list_iterate_next(db_state);
|
|
}
|
|
else if (db_state->size == 0)
|
|
{
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Zero-length file, skipping database match\n");
|
|
#endif
|
|
return database_info_list_iterate_next(db_state);
|
|
}
|
|
|
|
}
|
|
|
|
if (db_state->entry_index == 0)
|
|
{
|
|
char query[50];
|
|
|
|
query[0] = '\0';
|
|
|
|
if (!(_db->flags & DB_HANDLE_FLAG_SCAN_WITHOUT_CORE_MATCH))
|
|
{
|
|
/* don't scan files that can't be in this database.
|
|
*
|
|
* Could be because of:
|
|
* - A matching core missing
|
|
* - Incompatible file extension */
|
|
if (!core_info_database_supports_content_path(
|
|
db_state->list->elems[db_state->list_index].data, name))
|
|
return database_info_list_iterate_next(db_state);
|
|
|
|
if (!path_contains_compressed_file)
|
|
{
|
|
if (core_info_database_match_archive_member(
|
|
db_state->list->elems[db_state->list_index].data))
|
|
return database_info_list_iterate_next(db_state);
|
|
}
|
|
}
|
|
|
|
snprintf(query, sizeof(query),
|
|
"{crc:or(b\"%08lX\",b\"%08lX\")}",
|
|
(unsigned long)db_state->crc, (unsigned long)db_state->archive_crc);
|
|
|
|
database_info_list_iterate_new(db_state, query);
|
|
}
|
|
|
|
if (db_state->info)
|
|
{
|
|
database_info_t *db_info_entry =
|
|
&db_state->info->list[db_state->entry_index];
|
|
|
|
/* When scanning an archive, "first" file crc32 is also checked. */
|
|
if (db_info_entry && db_info_entry->crc32)
|
|
{
|
|
if (db_state->archive_crc == db_info_entry->crc32)
|
|
return database_info_list_iterate_found_match(
|
|
_db,
|
|
db_state, db, NULL);
|
|
if (db_state->crc == db_info_entry->crc32)
|
|
return database_info_list_iterate_found_match(
|
|
_db,
|
|
db_state, db, archive_entry);
|
|
}
|
|
}
|
|
|
|
db_state->entry_index++;
|
|
|
|
if (db_state->info)
|
|
{
|
|
if (db_state->entry_index >= db_state->info->count)
|
|
return database_info_list_iterate_next(db_state);
|
|
}
|
|
|
|
/* If we haven't reached the end of the database list yet,
|
|
* continue iterating. */
|
|
if (db_state->list_index < db_state->list->size)
|
|
return SCAN_VERDICT_CONTINUE;
|
|
|
|
database_info_list_free(db_state->info);
|
|
|
|
if (db_state->info)
|
|
{
|
|
free(db_state->info);
|
|
db_state->info = NULL;
|
|
}
|
|
|
|
return SCAN_VERDICT_NO_DB_MATCH;
|
|
}
|
|
|
|
/* There is a Lutro database, but without crc/serial, so all .lutro files will be recognized. */
|
|
static int task_database_iterate_playlist_lutro(
|
|
manual_scan_handle_t *_db,
|
|
database_state_handle_t *db_state,
|
|
database_info_handle_t *db,
|
|
const char *path)
|
|
{
|
|
char game_title[NAME_MAX_LENGTH];
|
|
fill_pathname(game_title,
|
|
path_basename(path), "", sizeof(game_title));
|
|
|
|
/* Skip if strict scan was asked with specific database */
|
|
if ((_db->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_STRICT)
|
|
&& *_db->task_config->database_name
|
|
&& memcmp(_db->task_config->database_name, "Lutro", 6) != 0 )
|
|
return SCAN_VERDICT_NO_DB_MATCH;
|
|
|
|
scan_results_add(&_db->scan_results,
|
|
path,
|
|
game_title,
|
|
(char*)"00000000|crc",
|
|
"Lutro.lpl",
|
|
""
|
|
);
|
|
|
|
return SCAN_VERDICT_MATCHED_DB;
|
|
}
|
|
|
|
static bool task_database_check_serial_and_crc(
|
|
database_state_handle_t *db_state)
|
|
{
|
|
const char *db_name;
|
|
if (!config_get_ptr()->bools.scan_serial_and_crc)
|
|
return false;
|
|
/* database_info_get_current_name() can return NULL (missing
|
|
* handle/list, or a NULL element). Guard it before it reaches
|
|
* path_basename_nocompression()/string_starts_with(), both of
|
|
* which would dereference the NULL and crash. */
|
|
if (!(db_name = database_info_get_current_name(db_state)))
|
|
return false;
|
|
db_name = path_basename_nocompression(db_name);
|
|
/* the PSP shares serials for disc/download content */
|
|
return (string_starts_with(db_name, "Sony - PlayStation Portable") ||
|
|
string_starts_with(db_name, "Sega - Dreamcast"));
|
|
}
|
|
|
|
static int task_database_iterate_serial_lookup(
|
|
manual_scan_handle_t *_db,
|
|
database_state_handle_t *db_state,
|
|
database_info_handle_t *db, const char *name,
|
|
bool path_contains_compressed_file,
|
|
bool size_hint_allowed)
|
|
{
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Serial check, list_idx %d/%d, entry_idx %d.\n",
|
|
db_state->list_index, db_state->list->size, db_state->entry_index);
|
|
#endif
|
|
|
|
if (
|
|
!db_state->list
|
|
|| (unsigned)db_state->list_index == (unsigned)db_state->list->size
|
|
|| ( _db->flags & DB_HANDLE_FLAG_USE_FIRST_MATCH_ONLY
|
|
&& db_state->list_index > 0
|
|
&& db_state->flags[0] & DB_STATE_FLAG_MATCHED)
|
|
)
|
|
return database_info_list_iterate_end_no_match(_db, db, db_state, name,
|
|
path_contains_compressed_file);
|
|
|
|
/* If size boundaries are not filled for this DB, run the queries */
|
|
if (db_state->min_sizes[db_state->list_index] == 0)
|
|
task_database_fill_db_min_max(db_state);
|
|
|
|
if (db_state->min_sizes[db_state->list_index] > 0)
|
|
{
|
|
if (!(db_state->flags[db_state->list_index] & DB_STATE_FLAG_HAS_SERIAL))
|
|
{
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Skipping DB, no serials here.\n");
|
|
#endif
|
|
return database_info_list_iterate_next(db_state);
|
|
}
|
|
|
|
/* Size check is conditional - it is unreliable in
|
|
* case of multitrack formats as serial is always
|
|
* in the first track, which may not be the actual game data.
|
|
* Same for those compressed image formats that are not
|
|
* supported by VFS. */
|
|
|
|
/* Examining ZIP file main entry (archive size filled, but
|
|
no indication of compressed file) */
|
|
if ( size_hint_allowed
|
|
&& !path_contains_compressed_file
|
|
&& db_state->archive_size > 0)
|
|
{
|
|
if ( ( db_state->min_sizes[db_state->list_index] > (int64_t) db_state->archive_size
|
|
&& db_state->min_sizes[db_state->list_index] > (int64_t) db_state->size )
|
|
|| ( db_state->max_sizes[db_state->list_index] < (int64_t) db_state->archive_size
|
|
&& db_state->max_sizes[db_state->list_index] < (int64_t) db_state->size ))
|
|
{
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Skipping DB, neither archive nor uncompressed size %ld/%ld is in range.\n",
|
|
db_state->archive_size, db_state->size);
|
|
#endif
|
|
return database_info_list_iterate_next(db_state);
|
|
}
|
|
}
|
|
/* Any other case (non-archive file, or a file inside the archive */
|
|
else if ( size_hint_allowed && db_state->size > 0
|
|
&& ( db_state->min_sizes[db_state->list_index] > (int64_t) db_state->size
|
|
|| db_state->max_sizes[db_state->list_index] < (int64_t) db_state->size))
|
|
{
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Skipping DB, file size %ld not in range.\n", db_state->size);
|
|
#endif
|
|
return database_info_list_iterate_next(db_state);
|
|
}
|
|
}
|
|
|
|
if (db_state->entry_index == 0)
|
|
{
|
|
size_t _len;
|
|
char query[50];
|
|
char *serial_buf = bin_to_hex_alloc(
|
|
(uint8_t*)db_state->serial,
|
|
strlen(db_state->serial) * sizeof(uint8_t));
|
|
|
|
if (!serial_buf)
|
|
return SCAN_VERDICT_ERROR;
|
|
|
|
_len = strlcpy(query, "{'serial': b'", sizeof(query));
|
|
_len += strlcpy(query + _len, serial_buf, sizeof(query) - _len);
|
|
query[ _len] = '\'';
|
|
query[++_len] = '}';
|
|
query[++_len] = '\0';
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Serial orig / decoded: \"%s\" / \"%s\".\n", db_state->serial, serial_buf);
|
|
#endif
|
|
database_info_list_iterate_new(db_state, query);
|
|
|
|
free(serial_buf);
|
|
}
|
|
|
|
if (db_state->info)
|
|
{
|
|
while (db_state->entry_index <= db_state->info->count)
|
|
{
|
|
database_info_t *db_info_entry = &db_state->info->list[
|
|
db_state->entry_index];
|
|
|
|
if (db_info_entry && db_info_entry->serial)
|
|
{
|
|
if (string_is_equal(db_state->serial, db_info_entry->serial))
|
|
{
|
|
if (task_database_check_serial_and_crc(db_state))
|
|
{
|
|
if (db_state->crc == 0)
|
|
{
|
|
if (extension_to_file_type(path_get_extension(name)) == FILE_TYPE_GDI)
|
|
task_database_gdi_get_crc_and_size(name, &db_state->crc, &db_state->size);
|
|
else
|
|
intfstream_file_get_crc_and_size(name, 0, INT64_MAX, &db_state->crc, &db_state->size);
|
|
}
|
|
|
|
if (db_state->crc == db_info_entry->crc32)
|
|
return database_info_list_iterate_found_match(_db,
|
|
db_state, db, NULL);
|
|
}
|
|
else
|
|
return database_info_list_iterate_found_match(_db,
|
|
db_state, db, NULL);
|
|
}
|
|
}
|
|
db_state->entry_index++;
|
|
}
|
|
}
|
|
|
|
if (db_state->info)
|
|
{
|
|
if (db_state->entry_index >= db_state->info->count)
|
|
return database_info_list_iterate_next(db_state);
|
|
}
|
|
|
|
/* If we haven't reached the end of the database list yet,
|
|
* continue iterating. */
|
|
if (db_state->list_index < db_state->list->size)
|
|
return SCAN_VERDICT_CONTINUE;
|
|
|
|
database_info_list_free(db_state->info);
|
|
free(db_state->info);
|
|
db_state->info = NULL;
|
|
return SCAN_VERDICT_NO_DB_MATCH;
|
|
}
|
|
|
|
static int task_database_iterate(
|
|
manual_scan_handle_t *_db,
|
|
const char *name,
|
|
database_state_handle_t *db_state,
|
|
database_info_handle_t *db,
|
|
bool path_contains_compressed_file)
|
|
{
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Type %d, \"%s\" against \"%s\".\n", db->type, name, database_info_get_current_name(db_state));
|
|
RARCH_DBG("[Scanner] Size: min %ld actual %ld max %ld.\n", db_state->min_sizes[db_state->list_index], db_state->size, db_state->max_sizes[db_state->list_index]);
|
|
#endif
|
|
switch (db->type)
|
|
{
|
|
case DATABASE_TYPE_ITERATE:
|
|
if (task_database_iterate_playlist(_db, db_state, db, name))
|
|
return SCAN_VERDICT_CONTINUE;
|
|
else
|
|
return SCAN_VERDICT_ERROR;
|
|
|
|
case DATABASE_TYPE_ITERATE_ARCHIVE:
|
|
#ifdef HAVE_COMPRESSION
|
|
return task_database_iterate_crc_lookup(
|
|
_db, db_state, db, name, db_state->archive_name,
|
|
path_contains_compressed_file);
|
|
#else
|
|
return SCAN_VERDICT_NO_DB_MATCH;
|
|
#endif
|
|
case DATABASE_TYPE_ITERATE_LUTRO:
|
|
return task_database_iterate_playlist_lutro(_db, db_state, db, name);
|
|
case DATABASE_TYPE_SERIAL_LOOKUP:
|
|
return task_database_iterate_serial_lookup(_db, db_state, db, name,
|
|
path_contains_compressed_file, false);
|
|
case DATABASE_TYPE_SERIAL_LOOKUP_SIZEHINT:
|
|
return task_database_iterate_serial_lookup(_db, db_state, db, name,
|
|
path_contains_compressed_file, true);
|
|
case DATABASE_TYPE_CRC_LOOKUP:
|
|
return task_database_iterate_crc_lookup(_db, db_state, db, name, NULL,
|
|
path_contains_compressed_file);
|
|
case DATABASE_TYPE_NONE:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return SCAN_VERDICT_ERROR;
|
|
}
|
|
|
|
static void task_database_cleanup_state(database_state_handle_t *db_state)
|
|
{
|
|
if (!db_state)
|
|
return;
|
|
|
|
if (db_state->buf)
|
|
free(db_state->buf);
|
|
db_state->buf = NULL;
|
|
}
|
|
#endif
|
|
/* Batch update playlists from accumulated scan results */
|
|
static void scan_results_batch_update_playlists(scan_results_t *sr,
|
|
manual_scan_handle_t* manual_scan, bool single_playlist)
|
|
{
|
|
size_t i;
|
|
const char *current_playlist = NULL;
|
|
playlist_t *playlist = NULL;
|
|
unsigned added_count = 0;
|
|
size_t str_len = PATH_MAX_LENGTH * sizeof(char);
|
|
char *db_playlist_path = (char*)malloc(str_len);
|
|
|
|
if (!db_playlist_path)
|
|
{
|
|
RARCH_ERR("[Scanner] Failed to allocate memory for batch playlist update.\n");
|
|
return;
|
|
}
|
|
|
|
RARCH_LOG("[Scanner] Batch updating playlists with %u results...\n",
|
|
(unsigned)sr->count);
|
|
|
|
if (single_playlist)
|
|
{
|
|
current_playlist = manual_scan->task_config->playlist_file;
|
|
playlist = manual_scan->playlist;
|
|
}
|
|
/* Process results, grouping by playlist */
|
|
for (i = 0; i < sr->count; i++)
|
|
{
|
|
scan_result_t *result = &sr->results[i];
|
|
char db_name_noext[PATH_MAX_LENGTH];
|
|
|
|
strlcpy(db_name_noext, result->db_name, sizeof(db_name_noext));
|
|
path_remove_extension(db_name_noext);
|
|
|
|
/* Check if we need to switch to a different playlist */
|
|
if (!single_playlist && (!current_playlist || !string_is_equal(current_playlist, result->db_name)))
|
|
{
|
|
/* Write and close previous playlist if any */
|
|
if (playlist)
|
|
{
|
|
RARCH_LOG("[Scanner] Added %u entries to \"%s\".\n", added_count, current_playlist);
|
|
playlist_write_file(playlist);
|
|
playlist_free(playlist);
|
|
playlist = NULL;
|
|
added_count = 0;
|
|
}
|
|
|
|
/* Open new playlist - if not fixed, use database name */
|
|
if (!*manual_scan->task_config->playlist_file)
|
|
{
|
|
current_playlist = result->db_name;
|
|
db_playlist_path[0] = '\0';
|
|
if (manual_scan->playlist_directory && *manual_scan->playlist_directory)
|
|
fill_pathname_join_special(db_playlist_path, manual_scan->playlist_directory,
|
|
result->db_name, str_len);
|
|
playlist_config_set_path(&manual_scan->playlist_config, db_playlist_path);
|
|
}
|
|
else
|
|
{
|
|
current_playlist = manual_scan->task_config->playlist_file;
|
|
playlist_config_set_path(&manual_scan->playlist_config, current_playlist);
|
|
}
|
|
|
|
playlist = playlist_init(&manual_scan->playlist_config);
|
|
|
|
/* Set default core, if required */
|
|
if (manual_scan->task_config->core_set)
|
|
{
|
|
playlist_set_default_core_path(playlist,
|
|
manual_scan->task_config->core_path);
|
|
playlist_set_default_core_name(playlist,
|
|
manual_scan->task_config->core_name);
|
|
}
|
|
|
|
/* Record remaining scan parameters to enable
|
|
* subsequent 'refresh playlist' operations */
|
|
playlist_set_scan_content_dir(playlist,
|
|
manual_scan->task_config->content_dir);
|
|
playlist_set_scan_file_exts(playlist,
|
|
manual_scan->task_config->file_exts_custom_set ?
|
|
manual_scan->task_config->file_exts : NULL);
|
|
if (manual_scan->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_DAT_LOOSE ||
|
|
manual_scan->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_DAT_STRICT)
|
|
playlist_set_scan_dat_file_path(playlist,
|
|
manual_scan->task_config->dat_file_path);
|
|
playlist_set_scan_database_name(playlist,
|
|
manual_scan->task_config->database_name);
|
|
playlist_set_scan_search_recursively(playlist,
|
|
manual_scan->task_config->search_recursively);
|
|
playlist_set_scan_search_archives(playlist,
|
|
manual_scan->task_config->search_archives);
|
|
playlist_set_scan_filter_dat_content(playlist,
|
|
manual_scan->task_config->filter_dat_content);
|
|
playlist_set_scan_overwrite_playlist(playlist,
|
|
manual_scan->task_config->overwrite_playlist);
|
|
playlist_set_scan_db_usage(playlist,
|
|
manual_scan->task_config->db_usage);
|
|
playlist_set_scan_omit_db_ref(playlist,
|
|
manual_scan->task_config->omit_db_reference);
|
|
|
|
if (!playlist)
|
|
{
|
|
RARCH_ERR("[Scanner] Failed to open playlist: \"%s\".\n", result->db_name);
|
|
continue;
|
|
}
|
|
|
|
RARCH_LOG("[Scanner] Processing playlist: \"%s\".\n", result->db_name);
|
|
}
|
|
|
|
/* Add entry to playlist if it doesn't already exist */
|
|
/* ...except for M3U, since the processing occurs at the end,
|
|
we overwrite any previous m3u entry (which has same file,
|
|
but less descriptive label, database, crc */
|
|
if (playlist &&
|
|
(!playlist_entry_exists(playlist, result->entry_path) ||
|
|
m3u_file_is_m3u(result->entry_path)))
|
|
{
|
|
struct playlist_entry entry;
|
|
|
|
if(m3u_file_is_m3u(result->entry_path))
|
|
playlist_delete_by_path(playlist, result->entry_path);
|
|
|
|
/* Build entry */
|
|
entry.path = result->entry_path;
|
|
entry.label = result->entry_label;
|
|
entry.core_path = (char*)"DETECT";
|
|
entry.core_name = (char*)"DETECT";
|
|
entry.db_name = result->db_name;
|
|
entry.crc32 = result->db_crc;
|
|
entry.subsystem_ident = NULL;
|
|
entry.subsystem_name = NULL;
|
|
entry.subsystem_roms = NULL;
|
|
entry.entry_slot = 0;
|
|
entry.runtime_hours = 0;
|
|
entry.runtime_minutes = 0;
|
|
entry.runtime_seconds = 0;
|
|
entry.last_played_year = 0;
|
|
entry.last_played_month = 0;
|
|
entry.last_played_day = 0;
|
|
entry.last_played_hour = 0;
|
|
entry.last_played_minute= 0;
|
|
entry.last_played_second= 0;
|
|
|
|
playlist_push(playlist, &entry);
|
|
added_count++;
|
|
|
|
RARCH_LOG("[Scanner] Add \"%s / %s\".\n", db_name_noext, result->entry_label);
|
|
|
|
if (retroarch_override_setting_is_set(RARCH_OVERRIDE_SETTING_DATABASE_SCAN, NULL))
|
|
task_database_scan_console_output(result->entry_label,
|
|
db_name_noext, true);
|
|
}
|
|
/* Entry already exists - output duplicate indicator for CLI scans */
|
|
else if (playlist && retroarch_override_setting_is_set(RARCH_OVERRIDE_SETTING_DATABASE_SCAN, NULL))
|
|
task_database_scan_console_output(result->entry_label,
|
|
db_name_noext, false);
|
|
}
|
|
|
|
/* Write and close final playlist */
|
|
if (playlist)
|
|
{
|
|
RARCH_LOG("[Scanner] Added %u entries to \"%s\".\n", added_count, current_playlist);
|
|
/* Ensure playlist is alphabetically sorted (matches manual scan behavior) */
|
|
playlist_set_sort_mode(playlist, PLAYLIST_SORT_MODE_DEFAULT);
|
|
playlist_qsort(playlist);
|
|
playlist_write_file(playlist);
|
|
if (!string_is_equal(current_playlist, manual_scan->task_config->playlist_file))
|
|
playlist_free(playlist);
|
|
}
|
|
|
|
free(db_playlist_path);
|
|
RARCH_LOG("[Scanner] Batch playlist update complete.\n");
|
|
}
|
|
|
|
#ifdef HAVE_LIBRETRODB
|
|
bool task_push_dbscan(
|
|
const char *playlist_directory, /* always from settings */
|
|
const char *content_database, /* always from settings */
|
|
const char *fullpath,
|
|
bool directory,
|
|
bool db_dir_show_hidden_files, /* always from settings */
|
|
retro_task_callback_t cb)
|
|
{
|
|
manual_content_scan_set_menu_content_dir(fullpath);
|
|
/*manual_content_scan_set_menu_scan_method(MANUAL_CONTENT_SCAN_METHOD_AUTOMATIC);*/
|
|
return task_push_manual_content_scan(false);
|
|
}
|
|
|
|
#endif
|
|
|
|
/* Frees task handle + all constituent objects */
|
|
static void free_manual_content_scan_handle(manual_scan_handle_t *manual_scan)
|
|
{
|
|
if (!manual_scan)
|
|
return;
|
|
|
|
if (manual_scan->task_config)
|
|
{
|
|
free(manual_scan->task_config);
|
|
manual_scan->task_config = NULL;
|
|
}
|
|
|
|
if (manual_scan->playlist)
|
|
{
|
|
playlist_free(manual_scan->playlist);
|
|
manual_scan->playlist = NULL;
|
|
}
|
|
|
|
if (manual_scan->file_exts_list)
|
|
{
|
|
string_list_free(manual_scan->file_exts_list);
|
|
manual_scan->file_exts_list = NULL;
|
|
}
|
|
|
|
if (manual_scan->content_list)
|
|
{
|
|
string_list_free(manual_scan->content_list);
|
|
manual_scan->content_list = NULL;
|
|
}
|
|
|
|
if (manual_scan->m3u_list)
|
|
{
|
|
string_list_free(manual_scan->m3u_list);
|
|
manual_scan->m3u_list = NULL;
|
|
}
|
|
|
|
if (manual_scan->dat_file)
|
|
{
|
|
logiqx_dat_free(manual_scan->dat_file);
|
|
manual_scan->dat_file = NULL;
|
|
}
|
|
|
|
/* Free accumulated scan results */
|
|
scan_results_free(&manual_scan->scan_results);
|
|
|
|
if (manual_scan->playlist_directory && *manual_scan->playlist_directory)
|
|
free(manual_scan->playlist_directory);
|
|
|
|
#ifdef HAVE_LIBRETRODB
|
|
if (1)
|
|
{
|
|
database_state_handle_t *dbstate = &manual_scan->state;
|
|
|
|
if (dbstate)
|
|
{
|
|
if (dbstate->list)
|
|
dir_list_free(dbstate->list);
|
|
}
|
|
|
|
if ( manual_scan->content_database_path
|
|
&& *manual_scan->content_database_path)
|
|
free(manual_scan->content_database_path);
|
|
if (manual_scan->state.buf)
|
|
free(manual_scan->state.buf);
|
|
if (manual_scan->handle)
|
|
free(manual_scan->handle);
|
|
}
|
|
#endif
|
|
|
|
free(manual_scan);
|
|
manual_scan = NULL;
|
|
}
|
|
|
|
static void cb_task_manual_content_scan(
|
|
retro_task_t *task, void *task_data,
|
|
void *user_data, const char *err)
|
|
{
|
|
manual_scan_handle_t *manual_scan = NULL;
|
|
playlist_t *cached_playlist = playlist_get_cached();
|
|
#if defined(HAVE_MENU)
|
|
struct menu_state *menu_st = menu_state_get_ptr();
|
|
if (!task)
|
|
goto end;
|
|
#else
|
|
if (!task)
|
|
return;
|
|
#endif
|
|
|
|
if (!(manual_scan = (manual_scan_handle_t*)task->state))
|
|
{
|
|
#if defined(HAVE_MENU)
|
|
goto end;
|
|
#else
|
|
return;
|
|
#endif
|
|
}
|
|
|
|
/* If the manual content scan task has modified the
|
|
* currently cached playlist, then it must be re-cached
|
|
* (otherwise changes will be lost if the currently
|
|
* cached playlist is saved to disk for any reason...) */
|
|
if (cached_playlist)
|
|
{
|
|
if (string_is_equal(
|
|
manual_scan->playlist_config.path,
|
|
playlist_get_conf_path(cached_playlist)))
|
|
{
|
|
playlist_config_t playlist_config;
|
|
|
|
/* Copy configuration of cached playlist
|
|
* (could use manual_scan->playlist_config,
|
|
* but doing it this way guarantees that
|
|
* the cached playlist is preserved in
|
|
* its original state) */
|
|
if (playlist_config_copy(
|
|
playlist_get_config(cached_playlist),
|
|
&playlist_config))
|
|
{
|
|
playlist_free_cached();
|
|
playlist_init_cached(&playlist_config);
|
|
}
|
|
}
|
|
}
|
|
|
|
#if defined(HAVE_MENU)
|
|
end:
|
|
/* When creating playlists, the playlist tabs of
|
|
* any active menu driver must be refreshed */
|
|
if (
|
|
#ifdef HAVE_LIBRETRODB
|
|
(!manual_scan ||
|
|
(manual_scan->flags & DB_HANDLE_FLAG_DO_MENU_REFRESH)) &&
|
|
#endif
|
|
menu_st->driver_ctx->environ_cb)
|
|
menu_st->driver_ctx->environ_cb(MENU_ENVIRON_RESET_HORIZONTAL_LIST,
|
|
NULL, menu_st->userdata);
|
|
#endif
|
|
}
|
|
|
|
static void task_manual_content_scan_free(retro_task_t *task)
|
|
{
|
|
manual_scan_handle_t *manual_scan = NULL;
|
|
|
|
if (!task)
|
|
return;
|
|
|
|
manual_scan = (manual_scan_handle_t*)task->state;
|
|
|
|
free_manual_content_scan_handle(manual_scan);
|
|
}
|
|
|
|
static void task_manual_content_scan_handler(retro_task_t *task)
|
|
{
|
|
uint8_t flg;
|
|
manual_scan_handle_t *manual_scan = NULL;
|
|
#ifdef HAVE_LIBRETRODB
|
|
database_info_handle_t *dbinfo = NULL;
|
|
database_state_handle_t *dbstate = NULL;
|
|
#endif
|
|
|
|
if (!task)
|
|
goto task_finished;
|
|
|
|
if (!(manual_scan = (manual_scan_handle_t*)task->state))
|
|
goto task_finished;
|
|
|
|
#ifdef HAVE_LIBRETRODB
|
|
dbinfo = manual_scan->handle;
|
|
dbstate = &manual_scan->state;
|
|
#endif
|
|
|
|
flg = task_get_flags(task);
|
|
|
|
if ((flg & RETRO_TASK_FLG_CANCELLED) > 0)
|
|
goto task_finished;
|
|
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Task handler started, status %d\n",manual_scan->status);
|
|
#endif
|
|
|
|
/* Improvements / shortcomings:
|
|
- default extension list could be the core supported list, instead
|
|
- pushing "scan" twice on the same file in the file browser will freeze, if there is still task feedback widget on screen (?)
|
|
this also happened before rework, prob menu related
|
|
- test with desktop menu
|
|
*/
|
|
|
|
switch (manual_scan->status)
|
|
{
|
|
case MANUAL_SCAN_BEGIN:
|
|
{
|
|
|
|
/* Initialize scan results accumulation */
|
|
if (!scan_results_init(&manual_scan->scan_results, 1024))
|
|
{
|
|
RARCH_ERR("[Scanner] Failed to initialize scan results\n");
|
|
goto task_finished;
|
|
}
|
|
|
|
#ifdef HAVE_LIBRETRODB
|
|
if ((manual_scan->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_STRICT ||
|
|
manual_scan->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_LOOSE) &&
|
|
!(manual_scan->flags & DB_HANDLE_FLAG_SCAN_STARTED))
|
|
{
|
|
manual_scan->flags |= DB_HANDLE_FLAG_SCAN_STARTED;
|
|
|
|
if (*manual_scan->task_config->content_dir)
|
|
{
|
|
/* cue, gdi prioritization in sorting */
|
|
if (manual_scan->flags & DB_HANDLE_FLAG_IS_DIRECTORY)
|
|
manual_scan->handle = database_info_dir_init(
|
|
manual_scan->task_config->content_dir, DATABASE_TYPE_ITERATE,
|
|
manual_scan->task_config->file_exts,
|
|
manual_scan->flags & DB_HANDLE_FLAG_SHOW_HIDDEN_FILES,
|
|
manual_scan->task_config->search_recursively,
|
|
manual_scan->task_config->search_archives,
|
|
&manual_scan->content_list);
|
|
else
|
|
manual_scan->handle = database_info_file_init(
|
|
manual_scan->task_config->content_dir, DATABASE_TYPE_ITERATE,
|
|
task, &manual_scan->content_list);
|
|
}
|
|
|
|
if (manual_scan->handle)
|
|
manual_scan->handle->status = DATABASE_STATUS_ITERATE_BEGIN;
|
|
|
|
if (!manual_scan->handle)
|
|
goto task_finished;
|
|
|
|
dbinfo = manual_scan->handle;
|
|
dbstate = &manual_scan->state;
|
|
|
|
if (dbstate && !dbstate->list)
|
|
{
|
|
if (manual_scan->content_database_path && *manual_scan->content_database_path)
|
|
{
|
|
if (manual_scan->task_config->db_selection == MANUAL_CONTENT_SCAN_SELECT_DB_SPECIFIC)
|
|
{
|
|
size_t str_len = PATH_MAX_LENGTH * sizeof(char);
|
|
char* rdb_name = (char*)malloc(str_len);
|
|
char* rdb_fullpath = (char*)malloc(str_len);
|
|
union string_list_elem_attr attr;
|
|
attr.i = 0;
|
|
|
|
/* Bail out if either heap allocation failed.
|
|
* fill_pathname and fill_pathname_join_special
|
|
* both call strlcpy on their destination buffer
|
|
* with no NULL guard, so proceeding with a NULL
|
|
* rdb_name / rdb_fullpath would segfault. Free
|
|
* the one that did succeed (free(NULL) is a
|
|
* no-op so no conditional needed) before taking
|
|
* the task-finished exit. */
|
|
if (!rdb_name || !rdb_fullpath)
|
|
{
|
|
free(rdb_name);
|
|
free(rdb_fullpath);
|
|
goto task_finished;
|
|
}
|
|
|
|
fill_pathname(rdb_name,
|
|
manual_scan->task_config->database_name,
|
|
".rdb", str_len);
|
|
|
|
fill_pathname_join_special(rdb_fullpath,
|
|
manual_scan->content_database_path,
|
|
rdb_name, str_len);
|
|
|
|
dbstate->list = string_list_new();
|
|
if (!dbstate->list)
|
|
{
|
|
/* Earlier code goto'd here without freeing
|
|
* the two buffers above, leaking ~8 KiB on
|
|
* this OOM path. Free them explicitly. */
|
|
free(rdb_name);
|
|
free(rdb_fullpath);
|
|
goto task_finished;
|
|
}
|
|
string_list_append(dbstate->list, rdb_fullpath, attr);
|
|
free(rdb_name);
|
|
free(rdb_fullpath);
|
|
}
|
|
else
|
|
{
|
|
dbstate->list = dir_list_new(
|
|
manual_scan->content_database_path,
|
|
"rdb", false,
|
|
manual_scan->flags & DB_HANDLE_FLAG_SHOW_HIDDEN_FILES,
|
|
false, false);
|
|
}
|
|
}
|
|
|
|
RARCH_LOG("[Scanner] %s\"%s\"...\n", msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_START), manual_scan->content_database_path);
|
|
if (retroarch_override_setting_is_set(RARCH_OVERRIDE_SETTING_DATABASE_SCAN, NULL))
|
|
printf("%s\"%s\"...\n", msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_START), manual_scan->content_database_path);
|
|
}
|
|
dbinfo->status = DATABASE_STATUS_ITERATE_START;
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
/* Get allowed file extensions list */
|
|
if (*manual_scan->task_config->file_exts)
|
|
manual_scan->file_exts_list = string_split(
|
|
manual_scan->task_config->file_exts, "|");
|
|
|
|
/* Get content list */
|
|
if (!(manual_scan->content_list
|
|
= manual_content_scan_get_content_list(
|
|
manual_scan->task_config)))
|
|
{
|
|
const char *_msg = msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_INVALID_CONTENT);
|
|
runloop_msg_queue_push(_msg, strlen(_msg), 1, 100, true, NULL,\
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
goto task_finished;
|
|
}
|
|
}
|
|
|
|
/* Load DAT file, if required */
|
|
if ((manual_scan->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_DAT_STRICT ||
|
|
manual_scan->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_DAT_LOOSE) &&
|
|
*manual_scan->task_config->dat_file_path)
|
|
{
|
|
if (!(manual_scan->dat_file =
|
|
logiqx_dat_init(
|
|
manual_scan->task_config->dat_file_path)))
|
|
{
|
|
const char *_msg = msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_DAT_FILE_LOAD_ERROR);
|
|
runloop_msg_queue_push(_msg, strlen(_msg), 1, 100, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
goto task_finished;
|
|
}
|
|
}
|
|
|
|
/* Open playlist */
|
|
if (manual_scan->task_config->target_is_single_determined_playlist &&
|
|
!(manual_scan->playlist =
|
|
playlist_init(&manual_scan->playlist_config)))
|
|
goto task_finished;
|
|
|
|
/* Reset playlist, if required */
|
|
if (manual_scan->task_config->overwrite_playlist)
|
|
playlist_clear(manual_scan->playlist);
|
|
|
|
/* Get initial playlist size */
|
|
manual_scan->playlist_size =
|
|
playlist_size(manual_scan->playlist);
|
|
|
|
/* Set default core, if required */
|
|
if (manual_scan->task_config->core_set)
|
|
{
|
|
playlist_set_default_core_path(manual_scan->playlist,
|
|
manual_scan->task_config->core_path);
|
|
playlist_set_default_core_name(manual_scan->playlist,
|
|
manual_scan->task_config->core_name);
|
|
}
|
|
|
|
/* Record remaining scan parameters to enable
|
|
* subsequent 'refresh playlist' operations */
|
|
playlist_set_scan_content_dir(manual_scan->playlist,
|
|
manual_scan->task_config->content_dir);
|
|
playlist_set_scan_file_exts(manual_scan->playlist,
|
|
manual_scan->task_config->file_exts_custom_set ?
|
|
manual_scan->task_config->file_exts : NULL);
|
|
if (manual_scan->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_DAT_LOOSE ||
|
|
manual_scan->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_DAT_STRICT)
|
|
playlist_set_scan_dat_file_path(manual_scan->playlist,
|
|
manual_scan->task_config->dat_file_path);
|
|
playlist_set_scan_database_name(manual_scan->playlist,
|
|
manual_scan->task_config->database_name);
|
|
playlist_set_scan_search_recursively(manual_scan->playlist,
|
|
manual_scan->task_config->search_recursively);
|
|
playlist_set_scan_search_archives(manual_scan->playlist,
|
|
manual_scan->task_config->search_archives);
|
|
playlist_set_scan_filter_dat_content(manual_scan->playlist,
|
|
manual_scan->task_config->filter_dat_content);
|
|
playlist_set_scan_overwrite_playlist(manual_scan->playlist,
|
|
manual_scan->task_config->overwrite_playlist);
|
|
playlist_set_scan_db_usage(manual_scan->playlist,
|
|
manual_scan->task_config->db_usage);
|
|
playlist_set_scan_omit_db_ref(manual_scan->playlist,
|
|
manual_scan->task_config->omit_db_reference);
|
|
|
|
/* All good - can start iterating
|
|
* > If playlist has content and 'validate
|
|
* entries' is enabled, go to clean-up phase
|
|
* > Otherwise go straight to content scan phase */
|
|
if (manual_scan->task_config->validate_entries &&
|
|
(manual_scan->playlist_size > 0))
|
|
manual_scan->status = MANUAL_SCAN_ITERATE_CLEAN;
|
|
else
|
|
{
|
|
#ifdef HAVE_LIBRETRODB
|
|
if (manual_scan->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_LOOSE ||
|
|
manual_scan->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_STRICT)
|
|
manual_scan->status = DATABASE_SCAN_ITERATE_START;
|
|
else
|
|
#endif
|
|
manual_scan->status = MANUAL_SCAN_ITERATE_CONTENT;
|
|
}
|
|
}
|
|
break;
|
|
case MANUAL_SCAN_ITERATE_CLEAN:
|
|
{
|
|
const struct playlist_entry *entry = NULL;
|
|
bool delete_entry = false;
|
|
|
|
/* Get current entry */
|
|
playlist_get_index(manual_scan->playlist,
|
|
manual_scan->playlist_index, &entry);
|
|
|
|
if (entry)
|
|
{
|
|
size_t _len;
|
|
const char *entry_file = NULL;
|
|
const char *entry_file_ext = NULL;
|
|
char task_title[128];
|
|
|
|
/* Update progress display */
|
|
task_free_title(task);
|
|
|
|
_len = strlcpy(task_title,
|
|
msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_PLAYLIST_CLEANUP),
|
|
sizeof(task_title));
|
|
|
|
if ( (entry->path && *entry->path)
|
|
&& (entry_file = path_basename(entry->path)))
|
|
strlcpy(task_title + _len,
|
|
entry_file,
|
|
sizeof(task_title) - _len);
|
|
|
|
task_set_title(task, strdup(task_title));
|
|
task_set_progress(task, (manual_scan->playlist_index * 100) /
|
|
manual_scan->playlist_size);
|
|
|
|
/* Check whether playlist content exists on
|
|
* the filesystem */
|
|
if (!playlist_content_path_is_valid(entry->path))
|
|
delete_entry = true;
|
|
/* If file exists, check whether it has a
|
|
* permitted file extension */
|
|
else if ( manual_scan->file_exts_list
|
|
&& (entry_file_ext = path_get_extension(entry->path))
|
|
&& !string_list_find_elem_prefix(
|
|
manual_scan->file_exts_list,
|
|
".", entry_file_ext))
|
|
delete_entry = true;
|
|
|
|
if (delete_entry)
|
|
{
|
|
/* Invalid content - delete entry */
|
|
playlist_delete_index(manual_scan->playlist,
|
|
manual_scan->playlist_index);
|
|
|
|
/* Update playlist_size */
|
|
manual_scan->playlist_size = playlist_size(manual_scan->playlist);
|
|
}
|
|
}
|
|
|
|
/* Increment entry index *if* current entry still
|
|
* exists (i.e. if entry was deleted, current index
|
|
* will already point to the *next* entry) */
|
|
if (!delete_entry)
|
|
manual_scan->playlist_index++;
|
|
|
|
if (manual_scan->playlist_index >=
|
|
manual_scan->playlist_size)
|
|
{
|
|
#ifdef HAVE_LIBRETRODB
|
|
if (manual_scan->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_LOOSE ||
|
|
manual_scan->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_STRICT)
|
|
manual_scan->status = DATABASE_SCAN_ITERATE_START;
|
|
else
|
|
#endif
|
|
manual_scan->status = MANUAL_SCAN_ITERATE_CONTENT;
|
|
}
|
|
}
|
|
break;
|
|
|
|
#ifdef HAVE_LIBRETRODB
|
|
/* Start of main loop. Each file in the list will be checked against all databases,
|
|
unless some scan configuration restricts this.
|
|
This first stage sets up the necessary iterators. */
|
|
case DATABASE_SCAN_ITERATE_START:
|
|
{
|
|
const char *content_path = manual_scan->content_list->elems[
|
|
manual_scan->content_list_index].data;
|
|
|
|
/* Check if this is an M3U file and add to list for post-processing */
|
|
if (m3u_file_is_m3u(content_path))
|
|
{
|
|
union string_list_elem_attr attr;
|
|
attr.i = 0;
|
|
if (manual_scan->m3u_list)
|
|
string_list_append(manual_scan->m3u_list, content_path, attr);
|
|
}
|
|
task_database_cleanup_state(dbstate);
|
|
dbstate->list_index = 0;
|
|
dbstate->entry_index = 0;
|
|
task_database_iterate_start(task, dbinfo, content_path);
|
|
manual_scan->status = DATABASE_SCAN_ITERATE_CONTENT;
|
|
dbinfo->type = DATABASE_TYPE_ITERATE;
|
|
}
|
|
break;
|
|
|
|
/* Content match iteration, reusing earlier autoscan code. */
|
|
case DATABASE_SCAN_ITERATE_CONTENT:
|
|
{
|
|
bool path_contains_compressed_file = false;
|
|
const char *content_path = manual_scan->content_list->elems[
|
|
manual_scan->content_list_index].data;
|
|
enum scan_verdict current_verdict;
|
|
if (!content_path)
|
|
goto task_finished;
|
|
|
|
path_contains_compressed_file = path_contains_compressed_file(content_path);
|
|
/* Reminder - remove this shortcut when serial scan inside zip is solved */
|
|
if (path_contains_compressed_file)
|
|
if (dbinfo->type == DATABASE_TYPE_ITERATE)
|
|
dbinfo->type = DATABASE_TYPE_ITERATE_ARCHIVE;
|
|
|
|
current_verdict = (enum scan_verdict)task_database_iterate(manual_scan, content_path, dbstate, dbinfo,
|
|
path_contains_compressed_file);
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Scan verdict is %d for %s\n", current_verdict, content_path);
|
|
#endif
|
|
switch (current_verdict)
|
|
{
|
|
case SCAN_VERDICT_MATCHED_DB:
|
|
manual_scan->status = DATABASE_SCAN_ITERATE_NEXT;
|
|
break;
|
|
case SCAN_VERDICT_NO_DB_MATCH:
|
|
if (manual_scan->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_LOOSE)
|
|
manual_scan->status = MANUAL_SCAN_ITERATE_CONTENT;
|
|
else
|
|
{
|
|
manual_scan->status = DATABASE_SCAN_ITERATE_NEXT;
|
|
}
|
|
break;
|
|
case SCAN_VERDICT_ARCHIVE_CONTENTS_ADDED:
|
|
manual_scan->status = DATABASE_SCAN_ITERATE_NEXT;
|
|
break;
|
|
case SCAN_VERDICT_ERROR:
|
|
RARCH_ERR("[Scanner] Scanning of content unexpectedly failed for \"%s\"\n", content_path);
|
|
/* fall through */
|
|
case SCAN_VERDICT_CONTINUE:
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case DATABASE_SCAN_ITERATE_NEXT:
|
|
/* skip any pruned entries */
|
|
increase_content_list_index(manual_scan);
|
|
|
|
if (manual_scan->content_list_index < manual_scan->content_list->size)
|
|
{
|
|
dbinfo->status = DATABASE_STATUS_ITERATE_START;
|
|
manual_scan->status = DATABASE_SCAN_ITERATE_START;
|
|
dbinfo->type = DATABASE_TYPE_ITERATE;
|
|
}
|
|
else
|
|
{
|
|
manual_scan->status = MANUAL_SCAN_ITERATE_CONTENT;
|
|
}
|
|
break;
|
|
#endif
|
|
case MANUAL_SCAN_ITERATE_CONTENT:
|
|
{
|
|
const char *content_path = manual_scan->content_list->elems[
|
|
manual_scan->content_list_index].data;
|
|
int content_type = manual_scan->content_list->elems[
|
|
manual_scan->content_list_index].attr.i;
|
|
|
|
if (content_path && *content_path)
|
|
{
|
|
size_t _len;
|
|
char task_title[128];
|
|
const char *content_file = path_basename(content_path);
|
|
/* todo: prob not here? */
|
|
char label[NAME_MAX_LENGTH];
|
|
char playlist_content_path[PATH_MAX_LENGTH];
|
|
|
|
/* Update progress display */
|
|
task_free_title(task);
|
|
|
|
_len = strlcpy(task_title,
|
|
msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_IN_PROGRESS),
|
|
sizeof(task_title));
|
|
|
|
if (content_file && *content_file)
|
|
strlcpy(task_title + _len,
|
|
content_file,
|
|
sizeof(task_title) - _len);
|
|
|
|
task_set_title(task, strdup(task_title));
|
|
task_set_progress(task,
|
|
(manual_scan->content_list_index * 100) /
|
|
manual_scan->content_list->size);
|
|
|
|
/* If "search archives" is enabled, but compressed files are not in the list, *
|
|
* do not add the compressed file itself, just add the contents to the end. *
|
|
* This can also conflict with DAT scanning which looks for zip files typically, *
|
|
* so it is restricted for the full-manual scan case. DB match does its own *
|
|
* archive addition. */
|
|
if (manual_scan->task_config->search_archives &&
|
|
path_is_compressed_file(content_path) &&
|
|
((*manual_scan->task_config->file_exts
|
|
&& string_find_index_substring_string(manual_scan->task_config->file_exts,path_get_extension(content_file)) < 0)
|
|
|| manual_scan->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_NONE))
|
|
{
|
|
add_files_from_archive(manual_scan,content_path);
|
|
}
|
|
else
|
|
{
|
|
/* Add content to playlist */
|
|
/* Get 'actual' content path */
|
|
playlist_content_path[0]='\0';
|
|
if (!manual_content_scan_get_playlist_content_path(
|
|
manual_scan->task_config, content_path, content_type,
|
|
playlist_content_path, sizeof(playlist_content_path)))
|
|
{
|
|
RARCH_WARN("[Scanner] Could not add manual scan result %s\n", content_path);
|
|
playlist_content_path[0]='\0';
|
|
}
|
|
else
|
|
{
|
|
/* Get entry label */
|
|
const char *db_name =
|
|
*manual_scan->task_config->database_name
|
|
? manual_scan->task_config->database_name
|
|
: manual_scan->task_config->dat_file_path;
|
|
label[0] = '\0';
|
|
if (!manual_content_scan_get_playlist_content_label(
|
|
content_path, manual_scan->dat_file,
|
|
manual_scan->task_config->filter_dat_content,
|
|
label, sizeof(label)))
|
|
{
|
|
label[0] = '\0';
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Rejecting item: %s\n",content_path);
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
RARCH_DBG("[Scanner] Adding item: %s\n",content_path);
|
|
scan_results_add(&manual_scan->scan_results,
|
|
content_path, label,
|
|
(char*)"00000000|crc",
|
|
db_name, "");
|
|
}
|
|
}
|
|
}
|
|
/* If this is an M3U file, add it to the
|
|
* M3U list for later processing */
|
|
if (m3u_file_is_m3u(content_path))
|
|
{
|
|
union string_list_elem_attr attr;
|
|
attr.i = 0;
|
|
/* NOTE: If string_list_append() fails, there is
|
|
* really nothing we can do. The M3U file will
|
|
* just be ignored... */
|
|
string_list_append(
|
|
manual_scan->m3u_list, content_path, attr);
|
|
}
|
|
}
|
|
|
|
/* Increment content index, move to the end if finished */
|
|
increase_content_list_index(manual_scan);
|
|
if (manual_scan->content_list_index >=
|
|
manual_scan->content_list->size)
|
|
{
|
|
/* Check whether we have any M3U files
|
|
* to process */
|
|
if (manual_scan->m3u_list->size > 0)
|
|
manual_scan->status = MANUAL_SCAN_ITERATE_M3U;
|
|
else
|
|
manual_scan->status = MANUAL_SCAN_END;
|
|
}
|
|
else
|
|
#ifdef HAVE_LIBRETRODB
|
|
if (manual_scan->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_LOOSE ||
|
|
manual_scan->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_STRICT)
|
|
manual_scan->status = DATABASE_SCAN_ITERATE_START;
|
|
else
|
|
#endif
|
|
manual_scan->status = MANUAL_SCAN_ITERATE_CONTENT;
|
|
}
|
|
break;
|
|
case MANUAL_SCAN_ITERATE_M3U:
|
|
{
|
|
const char *m3u_path = manual_scan->m3u_list->elems[
|
|
manual_scan->m3u_index].data;
|
|
|
|
if (m3u_path && *m3u_path)
|
|
{
|
|
size_t _len;
|
|
char task_title[128];
|
|
const char *m3u_name = path_basename_nocompression(m3u_path);
|
|
|
|
/* Update progress display */
|
|
task_free_title(task);
|
|
|
|
_len = strlcpy(task_title,
|
|
msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_M3U_CLEANUP),
|
|
sizeof(task_title));
|
|
|
|
if (m3u_name && *m3u_name)
|
|
strlcpy(task_title + _len,
|
|
m3u_name,
|
|
sizeof(task_title) - _len);
|
|
|
|
task_set_title(task, strdup(task_title));
|
|
task_set_progress(task, (manual_scan->m3u_index * 100) /
|
|
manual_scan->m3u_list->size);
|
|
|
|
task_database_iterate_m3u(manual_scan, m3u_path);
|
|
}
|
|
|
|
/* Increment M3U file index */
|
|
manual_scan->m3u_index++;
|
|
if (manual_scan->m3u_index >= manual_scan->m3u_list->size)
|
|
manual_scan->status = MANUAL_SCAN_END;
|
|
}
|
|
break;
|
|
case MANUAL_SCAN_END:
|
|
{
|
|
const char *msg = NULL;
|
|
|
|
/* Batch update all playlists with accumulated results */
|
|
if (manual_scan->scan_results.count > 0)
|
|
scan_results_batch_update_playlists(&manual_scan->scan_results, manual_scan, manual_scan->task_config->target_is_single_determined_playlist);
|
|
/* If no results, still write an empty playlist, if it is specified. */
|
|
else if (manual_scan->task_config->target_is_single_determined_playlist)
|
|
playlist_write_file(manual_scan->playlist);
|
|
|
|
/* Update progress display */
|
|
#ifdef HAVE_LIBRETRODB
|
|
if (dbstate && dbstate->list && dbstate->list->size == 0 &&
|
|
(manual_scan->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_LOOSE ||
|
|
manual_scan->task_config->db_usage == MANUAL_CONTENT_SCAN_USE_DB_STRICT))
|
|
{
|
|
msg = msg_hash_to_str(MSG_SCANNING_NO_DATABASE);
|
|
task_set_error(task, strdup(msg));
|
|
}
|
|
else
|
|
{
|
|
if (manual_scan->flags & DB_HANDLE_FLAG_IS_DIRECTORY)
|
|
msg = msg_hash_to_str(MSG_SCANNING_OF_DIRECTORY_FINISHED);
|
|
else
|
|
msg = msg_hash_to_str(MSG_SCANNING_OF_FILE_FINISHED);
|
|
}
|
|
#else
|
|
msg = msg_hash_to_str(MSG_SCANNING_OF_DIRECTORY_FINISHED);
|
|
#endif
|
|
task_free_title(task);
|
|
task_set_title(task, strdup(msg));
|
|
task_set_progress(task, 100);
|
|
ui_companion_driver_notify_refresh();
|
|
RARCH_LOG("[Scanner] %s\n", msg);
|
|
if (retroarch_override_setting_is_set(RARCH_OVERRIDE_SETTING_DATABASE_SCAN, NULL))
|
|
printf("%s\n", msg);
|
|
|
|
RARCH_DBG("[Scanner] Scan settings were:\n");
|
|
RARCH_DBG("[Scanner] Content dir: \"%s\"\n",manual_scan->task_config->content_dir);
|
|
RARCH_DBG("[Scanner] Database name: \"%s\"\n",manual_scan->task_config->database_name);
|
|
RARCH_DBG("[Scanner] DAT / fallback db: \"%s\"\n",manual_scan->task_config->dat_file_path);
|
|
RARCH_DBG("[Scanner] Target playlist: \"%s\"\n",manual_scan->task_config->playlist_file);
|
|
RARCH_DBG("[Scanner] Core name: \"%s\"\n",manual_scan->task_config->core_name);
|
|
RARCH_DBG("[Scanner] File ext %s \"%s\"\n",
|
|
manual_scan->task_config->file_exts_custom_set ? "(custom):" : "(auto): ",
|
|
manual_scan->task_config->file_exts);
|
|
RARCH_DBG("[Scanner] DB usage, DB selection: %d / %d\n",manual_scan->task_config->db_usage,manual_scan->task_config->db_selection);
|
|
RARCH_DBG("[Scanner] Recursive, archives, single target: %s / %s / %s\n",
|
|
manual_scan->task_config->search_recursively ? "yes" : "no",
|
|
manual_scan->task_config->search_archives ? "yes" : "no",
|
|
manual_scan->task_config->target_is_single_determined_playlist ? "yes" : "no");
|
|
RARCH_DBG("[Scanner] Overwrite, validate, omit DB reference: %s / %s / %s\n",
|
|
manual_scan->task_config->overwrite_playlist ? "yes" : "no",
|
|
manual_scan->task_config->validate_entries ? "yes" : "no",
|
|
manual_scan->task_config->omit_db_reference ? "yes" : "no");
|
|
}
|
|
/* fall-through */
|
|
default:
|
|
task_set_progress(task, 100);
|
|
goto task_finished;
|
|
}
|
|
|
|
return;
|
|
|
|
task_finished:
|
|
#ifdef DEBUG
|
|
RARCH_DBG("[Scanner] Task finished\n");
|
|
#endif
|
|
if (task)
|
|
task_set_flags(task, RETRO_TASK_FLG_FINISHED, true);
|
|
}
|
|
|
|
static bool task_manual_content_scan_finder(retro_task_t *task, void *user_data)
|
|
{
|
|
manual_scan_handle_t *manual_scan = NULL;
|
|
|
|
if (!task || !user_data)
|
|
return false;
|
|
if (task->handler != task_manual_content_scan_handler)
|
|
return false;
|
|
if (!(manual_scan = (manual_scan_handle_t*)task->state))
|
|
return false;
|
|
return string_is_equal(
|
|
(const char*)user_data, manual_scan->playlist_config.path);
|
|
}
|
|
|
|
bool task_push_manual_content_scan(
|
|
bool do_menu_refresh)
|
|
{
|
|
size_t _len;
|
|
task_finder_data_t find_data;
|
|
char task_title[128];
|
|
retro_task_t *task = NULL;
|
|
manual_scan_handle_t *manual_scan = NULL;
|
|
settings_t *settings = config_get_ptr();
|
|
const char *playlist_dir = settings->paths.directory_playlist;
|
|
|
|
/* Sanity check */
|
|
if (!playlist_dir || !*playlist_dir)
|
|
return false;
|
|
|
|
if (!(manual_scan = (manual_scan_handle_t*)
|
|
calloc(1, sizeof(manual_scan_handle_t))))
|
|
return false;
|
|
|
|
/* Configure handle */
|
|
manual_scan->task_config = NULL;
|
|
manual_scan->playlist = NULL;
|
|
manual_scan->file_exts_list = NULL;
|
|
manual_scan->content_list = NULL;
|
|
manual_scan->dat_file = NULL;
|
|
manual_scan->playlist_size = 0;
|
|
manual_scan->playlist_index = 0;
|
|
manual_scan->content_list_index = 0;
|
|
manual_scan->status = MANUAL_SCAN_BEGIN;
|
|
manual_scan->m3u_index = 0;
|
|
manual_scan->m3u_list = string_list_new();
|
|
|
|
if (!manual_scan->m3u_list)
|
|
goto error;
|
|
|
|
manual_scan->playlist_config.capacity = COLLECTION_SIZE;
|
|
manual_scan->playlist_config.old_format = settings->bools.playlist_use_old_format;
|
|
manual_scan->playlist_config.compress = settings->bools.playlist_compression;
|
|
manual_scan->playlist_config.fuzzy_archive_match = settings->bools.playlist_fuzzy_archive_match;
|
|
playlist_config_set_base_content_directory(&manual_scan->playlist_config, settings->bools.playlist_portable_paths ? settings->paths.directory_menu_content : NULL);
|
|
|
|
#ifdef HAVE_LIBRETRODB
|
|
|
|
if (settings->bools.scan_without_core_match)
|
|
manual_scan->flags |= DB_HANDLE_FLAG_SCAN_WITHOUT_CORE_MATCH;
|
|
|
|
if (settings->bools.show_hidden_files)
|
|
manual_scan->flags |= DB_HANDLE_FLAG_SHOW_HIDDEN_FILES;
|
|
|
|
if (do_menu_refresh)
|
|
manual_scan->flags |= DB_HANDLE_FLAG_DO_MENU_REFRESH;
|
|
|
|
manual_scan->content_database_path = strdup(settings->paths.path_content_database);
|
|
#endif
|
|
manual_scan->playlist_directory = strdup(playlist_dir);
|
|
|
|
/* > Get current manual content scan configuration */
|
|
if (!(manual_scan->task_config = (manual_content_scan_task_config_t*)
|
|
calloc(1, sizeof(manual_content_scan_task_config_t))))
|
|
goto error;
|
|
|
|
if (!manual_content_scan_get_task_config(
|
|
manual_scan->task_config, playlist_dir))
|
|
{
|
|
const char *_msg = msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_INVALID_CONFIG);
|
|
RARCH_ERR("[Scanner] Invalid scan config\n");
|
|
runloop_msg_queue_push(_msg, strlen(_msg), 1, 100, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
goto error;
|
|
}
|
|
|
|
#ifdef HAVE_LIBRETRODB
|
|
if (manual_scan->task_config->db_selection == MANUAL_CONTENT_SCAN_SELECT_DB_AUTO_FIRST_MATCH)
|
|
manual_scan->flags |= DB_HANDLE_FLAG_USE_FIRST_MATCH_ONLY;
|
|
|
|
if (path_is_directory(manual_scan->task_config->content_dir))
|
|
manual_scan->flags |= DB_HANDLE_FLAG_IS_DIRECTORY;
|
|
#endif
|
|
|
|
playlist_config_set_path(
|
|
&manual_scan->playlist_config,
|
|
manual_scan->task_config->playlist_file);
|
|
|
|
/* Concurrent scanning of content to the same
|
|
* playlist is not allowed */
|
|
find_data.func = task_manual_content_scan_finder;
|
|
find_data.userdata = (void*)manual_scan->playlist_config.path;
|
|
|
|
if (task_queue_find(&find_data))
|
|
goto error;
|
|
|
|
/* Create task */
|
|
if (!(task = task_init()))
|
|
goto error;
|
|
|
|
/* > Get task title */
|
|
_len = strlcpy(
|
|
task_title, msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_START),
|
|
sizeof(task_title));
|
|
strlcpy(task_title + _len,
|
|
manual_scan->task_config->system_name,
|
|
sizeof(task_title) - _len);
|
|
|
|
/* > Configure task */
|
|
task->handler = task_manual_content_scan_handler;
|
|
task->state = manual_scan;
|
|
task->title = strdup(task_title);
|
|
task->progress = 0;
|
|
#ifdef HAVE_LIBRETRODB
|
|
task->progress_cb = task_window_progress_cb;
|
|
#else
|
|
task->progress_cb = NULL;
|
|
#endif
|
|
|
|
task->callback = cb_task_manual_content_scan;
|
|
task->cleanup = task_manual_content_scan_free;
|
|
task->flags |= RETRO_TASK_FLG_ALTERNATIVE_LOOK;
|
|
|
|
/* > Push task */
|
|
task_queue_push(task);
|
|
|
|
return true;
|
|
|
|
error:
|
|
/* Clean up handle */
|
|
free_manual_content_scan_handle(manual_scan);
|
|
manual_scan = NULL;
|
|
RARCH_ERR("[Scanner] Task creation failed\n");
|
|
|
|
return false;
|
|
}
|