Files
RetroArch/menu/menu_explore.c
T
LibretroAdmin b9be38fd55 strl: copy string literals without scanning them at runtime
strlcpy() begins with strlen() of its source. It lives in another
translation unit, so with no LTO that scan happens at runtime even when
the source is a literal whose length the compiler already measured, and
the call itself cannot be folded away either.

strlcpy_lit() passes the length over as sizeof(). When the destination
size is also a constant -- sizeof(buf), the usual case -- nothing is
left but the stores memcpy() compiles to:

  strlcpy      "core_options"  sizeof(buf)   7.01 ns
  strlcpy_lit  "core_options"  sizeof(buf)   0.42 ns
  strlcpy      41-char literal sizeof(buf)   7.74 ns
  strlcpy_lit  41-char literal sizeof(buf)   0.71 ns
  strlcpy      "core_options"  runtime len   7.11 ns
  strlcpy_lit  "core_options"  runtime len   0.70 ns

The destination bound is kept. When the literal does not fit it defers
to strlcpy(), so the copy truncates as before rather than overrunning;
a form that took only the literal would have turned every one of these
sites into an overflow waiting for a small buffer. The literal is
concatenated with "" so a non-literal source is a compile error rather
than a sizeof() that silently measures a pointer.

818 sites across 104 files were converted by parsing arguments rather
than by pattern-matching text: only a three-argument call whose second
argument is a bare string literal is rewritten, so a macro that happens
to expand to a literal is left alone. Line endings are preserved, so
the CRLF files carry only their own changed lines.

test/compat/test_strl.c compares the two against each other over every
destination size from 0 past the literal's length, for literals that
are empty, single-character, path-shaped, adjacently concatenated and
escape-bearing, checking the return, the terminator and the whole
buffer so an overrun past the terminator is caught. ASan+UBSan+LSan and
TSan both 4/4. The full menu_displaylist_build_list walk over every
displaylist type is byte-identical to before the change
(md5 a2a081bcac09cdc8d72d67efd14ac40e).
2026-08-30 02:10:20 +02:00

1957 lines
68 KiB
C

/* RetroArch - A frontend for libretro.
* Copyright (C) 2011-2020 - Daniel De Matteis
* Copyright (C) 2020 - Psyraven
*
* 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 <stddef.h>
#include <compat/strcasestr.h>
#include <compat/strl.h>
#include <array/rbuf.h>
#include <array/rhmap.h>
#include <formats/rjson.h>
#include <formats/rjson_stream.h>
#include <formats/rjson_helpers.h>
#include <retro_endianness.h>
#include <streams/file_stream.h>
#include <string/stdstring.h>
#include "menu_driver.h"
#include "menu_cbs.h"
#include "../retroarch.h"
#include "../configuration.h"
#include "../file_path_special.h"
#include "../msg_hash_lbl_str.h"
#include "../playlist.h"
#include "../verbosity.h"
#include "../libretro-db/libretrodb.h"
#include "../tasks/tasks_internal.h"
/* Explore */
enum
{
EXPLORE_BY_DEVELOPER = 0,
EXPLORE_BY_PUBLISHER,
EXPLORE_BY_RELEASEYEAR,
EXPLORE_BY_PLAYERCOUNT,
EXPLORE_BY_GENRE,
EXPLORE_BY_ACHIEVEMENTS,
EXPLORE_BY_CATEGORY,
EXPLORE_BY_LANGUAGE,
EXPLORE_BY_CONSOLE_EXCLUSIVE,
EXPLORE_BY_PLATFORM_EXCLUSIVE,
EXPLORE_BY_RUMBLE,
EXPLORE_BY_SCORE,
EXPLORE_BY_MEDIA,
EXPLORE_BY_CONTROLS,
EXPLORE_BY_ARTSTYLE,
EXPLORE_BY_GAMEPLAY,
EXPLORE_BY_NARRATIVE,
EXPLORE_BY_PACING,
EXPLORE_BY_PERSPECTIVE,
EXPLORE_BY_SETTING,
EXPLORE_BY_VISUAL,
EXPLORE_BY_VEHICULAR,
EXPLORE_BY_ORIGIN,
EXPLORE_BY_REGION,
EXPLORE_BY_FRANCHISE,
EXPLORE_BY_TAGS,
EXPLORE_BY_SYSTEM,
EXPLORE_CAT_COUNT,
EXPLORE_OP_EQUAL = 0,
EXPLORE_OP_MIN,
EXPLORE_OP_MAX,
EXPLORE_OP_RANGE,
EXPLORE_ICONS_OFF = 0,
EXPLORE_ICONS_CONTENT = 1,
EXPLORE_ICONS_SYSTEM_CATEGORY = 2,
EXPLORE_TYPE_ADDITIONALFILTER = FILE_TYPE_RDB, /* database icon */
EXPLORE_TYPE_VIEW = FILE_TYPE_PLAIN, /* file icon */
EXPLORE_TYPE_FILTERNULL = MENU_SETTINGS_LAST,
EXPLORE_TYPE_SEARCH,
EXPLORE_TYPE_SHOWALL,
EXPLORE_TYPE_FIRSTCATEGORY,
EXPLORE_TYPE_FIRSTITEM = EXPLORE_TYPE_FIRSTCATEGORY + EXPLORE_CAT_COUNT
};
/* Arena allocator */
typedef struct ex_arena
{
char *ptr;
char *end;
char **blocks;
} ex_arena;
typedef struct
{
uint32_t idx;
char str[1];
} explore_string_t;
typedef struct
{
const struct playlist_entry* playlist_entry;
explore_string_t *by[EXPLORE_CAT_COUNT];
explore_string_t **split;
#ifdef EXPLORE_SHOW_ORIGINAL_TITLE
char* original_title;
#endif
} explore_entry_t;
struct explore_state
{
ex_arena arena;
explore_string_t **by[EXPLORE_CAT_COUNT];
explore_entry_t *entries;
playlist_t **playlists;
uintptr_t *icons;
const char *label_explore_item_str;
char title[1024];
bool has_unknown[EXPLORE_CAT_COUNT];
unsigned show_icons;
unsigned view_levels;
char view_search[1024];
uint8_t view_op[EXPLORE_CAT_COUNT];
bool view_use_split[EXPLORE_CAT_COUNT];
unsigned view_cats[EXPLORE_CAT_COUNT];
explore_string_t* view_match[EXPLORE_CAT_COUNT];
uint32_t view_idx_min[EXPLORE_CAT_COUNT];
uint32_t view_idx_max[EXPLORE_CAT_COUNT];
};
static const struct
{
const char* rdbkey;
enum msg_hash_enums name_enum, by_enum;
bool use_split, is_company, is_numeric, is_boolean;
}
explore_by_info[EXPLORE_CAT_COUNT] =
{
{ "developer", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_DEVELOPER, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_DEVELOPER, true, true, false, false },
{ "publisher", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_PUBLISHER, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_PUBLISHER, true, true, false, false },
{ "releaseyear", MENU_ENUM_LABEL_VALUE_EXPLORE_CATEGORY_RELEASE_YEAR, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_RELEASE_YEAR, false, false, true, false },
{ "users", MENU_ENUM_LABEL_VALUE_EXPLORE_CATEGORY_PLAYER_COUNT, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_PLAYER_COUNT, false, false, true, false },
{ "genre", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_GENRE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_GENRE, true, false, false, false },
{ "achievements", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_ACHIEVEMENTS, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_ACHIEVEMENTS, false, false, false, true },
{ "category", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_CATEGORY, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_CATEGORY, true, false, false, false },
{ "language", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_LANGUAGE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_LANGUAGE, true, false, false, false },
{ "console_exclusive", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_CONSOLE_EXCLUSIVE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_CONSOLE_EXCLUSIVE, false, false, false, true },
{ "platform_exclusive", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_PLATFORM_EXCLUSIVE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_PLATFORM_EXCLUSIVE, false, false, false, true },
{ "rumble", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_RUMBLE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_RUMBLE, false, false, false, true },
{ "score", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_SCORE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_SCORE, true, false, false, false },
{ "media", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_MEDIA, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_MEDIA, true, false, false, false },
{ "controls", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_CONTROLS, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_CONTROLS, true, false, false, false },
{ "artstyle", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_ARTSTYLE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_ARTSTYLE, true, false, false, false },
{ "gameplay", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_GAMEPLAY, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_GAMEPLAY, true, false, false, false },
{ "narrative", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_NARRATIVE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_NARRATIVE, true, false, false, false },
{ "pacing", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_PACING, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_PACING, true, false, false, false },
{ "perspective", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_PERSPECTIVE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_PERSPECTIVE, true, false, false, false },
{ "setting", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_SETTING, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_SETTING, true, false, false, false },
{ "visual", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_VISUAL, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_VISUAL, true, false, false, false },
{ "vehicular", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_VEHICULAR, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_VEHICULAR, true, false, false, false },
{ "origin", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_ORIGIN, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_ORIGIN, false, false, false, false },
{ "region", MENU_ENUM_LABEL_VALUE_EXPLORE_CATEGORY_REGION, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_REGION, false, false, false, false },
{ "franchise", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_FRANCHISE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_FRANCHISE, false, false, false, false },
{ "tags", MENU_ENUM_LABEL_VALUE_EXPLORE_CATEGORY_TAG, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_TAG, true, false, false, false },
{ "system", MENU_ENUM_LABEL_VALUE_CORE_INFO_SYSTEM_NAME, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_SYSTEM_NAME, false, false, false, false },
};
/* TODO/FIXME - static global */
static explore_state_t* explore_state;
#if defined(_MSC_VER)
#define EX_ALIGNOF(type) ((int)__alignof(type))
#else
#define EX_ALIGNOF(type) ((int)__alignof__(type))
#endif
#define EX_ARENA_ALIGNMENT 8
#define EX_ARENA_BLOCK_SIZE (64 * 1024)
#define EX_ARENA_ALIGN_UP(n, a) (((n) + (a) - 1) & ~((a) - 1))
static void ex_arena_grow(ex_arena *arena, size_t min_size)
{
size_t _len = EX_ARENA_ALIGN_UP(
MAX(min_size, EX_ARENA_BLOCK_SIZE), EX_ARENA_ALIGNMENT);
char *new_block = (char *)malloc(_len);
/* NULL-check: on OOM leave arena->ptr and arena->end
* pointing at the current (exhausted) block if there is one,
* or both NULL on first grow. ex_arena_alloc returns the
* current arena->ptr as the caller's 'ptr' and the caller
* dereferences it, so we need ex_arena_alloc itself to
* signal the failure - that's handled by the 'end - ptr'
* pointer-subtraction which is defined to be 0 when both
* are NULL. The second check in ex_arena_alloc below
* catches the remaining OOM path. */
if (!new_block)
return;
arena->ptr = new_block;
arena->end = arena->ptr + _len;
RBUF_PUSH(arena->blocks, arena->ptr);
}
static void *ex_arena_alloc(ex_arena *arena, size_t len)
{
void *ptr = NULL;
if (len > (size_t)(arena->end - arena->ptr))
ex_arena_grow(arena, len);
/* Re-check after grow: on OOM the grow function leaves
* arena->ptr and arena->end unchanged, so the capacity
* check still fails. Return NULL so callers can bail
* rather than dereference stale or NULL storage. */
if (len > (size_t)(arena->end - arena->ptr))
return NULL;
ptr = arena->ptr;
arena->ptr = (char *)
EX_ARENA_ALIGN_UP((uintptr_t)(arena->ptr + len), EX_ARENA_ALIGNMENT);
return ptr;
}
static void ex_arena_free(ex_arena *arena)
{
char **it;
for (it = arena->blocks; it != RBUF_END(arena->blocks); it++)
free(*it);
RBUF_FREE(arena->blocks);
arena->ptr = NULL;
arena->end = NULL;
arena->blocks = NULL;
}
/* Hash function */
static uint32_t ex_hash32_nocase_filtered(
const unsigned char *s, size_t len,
unsigned char f_first, unsigned char f_last)
{
const unsigned char *end = NULL;
uint32_t hash = (uint32_t)0x811c9dc5;
for (end = s + len; s != end;)
{
unsigned char c = *(s++);
if (c >= f_first && c <= f_last)
hash = ((hash * (uint32_t)0x01000193)
^ (uint32_t)((c >= 'A' && c <= 'Z')
? (c | 0x20) : c));
}
if (hash)
return hash;
return 1;
}
static int explore_qsort_func_nums(const void *a_, const void *b_)
{
const char *a = (*(const explore_string_t**)a_)->str;
const char *b = (*(const explore_string_t**)b_)->str;
return atoi(a) - atoi(b);
}
static int explore_qsort_func_strings(const void *a_, const void *b_)
{
const char *a = (*(const explore_string_t**)a_)->str;
const char *b = (*(const explore_string_t**)b_)->str;
int a0 = TOLOWER(a[0]), b0 = TOLOWER(b[0]);
return (a0 != b0 ? (a0 - b0) : strcasecmp(a, b));
}
static int explore_qsort_func_entries(const void *a_, const void *b_)
{
const char *a = ((const explore_entry_t*)a_)->playlist_entry->label;
const char *b = ((const explore_entry_t*)b_)->playlist_entry->label;
int a0 = TOLOWER(a[0]), b0 = TOLOWER(b[0]);
if (a0 != b0)
return a0 - b0;
return strcasecmp(a, b);
}
static int explore_qsort_func_menulist(const void *a_, const void *b_)
{
const struct item_file *a = (const struct item_file*)a_;
const struct item_file *b = (const struct item_file*)b_;
if (a->path[0] != b->path[0])
return (unsigned char)a->path[0] - (unsigned char)b->path[0];
return strcasecmp(a->path, b->path);
}
static int explore_check_company_suffix(const char* p, bool search_reverse)
{
int p0, p0_lc, p1, p1_lc, p2, p2_lc;
if (search_reverse)
{
p -= (p[-1] == '.' ? 4 : 3);
if (p[-1] != ' ')
return 0;
}
if (p[0] == '\0' || p[1] == '\0' || p[2] == '\0')
return 0;
p0 = p[0];
p1 = p[1];
p2 = p[2];
p0_lc = TOLOWER(p0);
p1_lc = TOLOWER(p1);
p2_lc = TOLOWER(p2);
if ( (p0_lc == 'i' && p1_lc == 'n' && p2_lc == 'c') /*, Inc */
|| (p0_lc == 'l' && p1_lc == 't' && p2_lc == 'd') /*, Ltd */
|| (p0_lc == 't' && p1_lc == 'h' && p2_lc == 'e') /*, The */
)
return (p[3] == '.' ? 4 : 3);
return 0;
}
static void explore_add_unique_string(
explore_state_t *state,
explore_string_t** maps[EXPLORE_CAT_COUNT], explore_entry_t *e,
unsigned cat, const char *str,
explore_string_t ***split_buf)
{
bool is_company;
const char *p;
const char *p_next;
if (!str || !*str)
{
state->has_unknown[cat] = true;
return;
}
if (!explore_by_info[cat].use_split)
split_buf = NULL;
is_company = explore_by_info[cat].is_company;
for (p = str + 1;; p++)
{
size_t _len = 0;
uint32_t hash = 0;
explore_string_t* entry = NULL;
if (*p != '/' && *p != ',' && *p != '|' && *p != '\0')
continue;
if (!split_buf && *p != '\0')
continue;
p_next = p;
while (*str == ' ')
str++;
while (p[-1] == ' ')
p--;
if (p == str)
{
if (*p == '\0')
return;
continue;
}
if (is_company && p - str > 5)
{
p -= explore_check_company_suffix(p, true);
while (p[-1] == ' ')
p--;
}
_len = p - str;
hash = ex_hash32_nocase_filtered(
(unsigned char*)str, _len, '0', 255);
entry = RHMAP_GET(maps[cat], hash);
if (!entry)
{
entry = (explore_string_t*)
ex_arena_alloc(&state->arena,
sizeof(explore_string_t) + _len);
/* NULL-check: ex_arena_alloc returns NULL on OOM now.
* On failure skip this entry - the surrounding loop
* iterates over chars in the input string splitting on
* separators, so one missed entry just means one
* category value doesn't get indexed this pass. Picked
* up on the next scan once memory is available. */
if (!entry)
continue;
memcpy(entry->str, str, _len);
entry->str[_len] = '\0';
RBUF_PUSH(state->by[cat], entry);
RHMAP_SET(maps[cat], hash, entry);
}
if (!e->by[cat])
e->by[cat] = entry;
else
RBUF_PUSH(*split_buf, entry);
if (*p_next == '\0')
return;
if (is_company && *p_next == ',')
{
p = p_next + 1;
while (*p == ' ')
p++;
p += explore_check_company_suffix(p, false);
while (*p == ' ')
p++;
if (*p == '\0')
return;
if (*p == '/' || *p == ',' || *p == '|')
p_next = p;
}
p = p_next;
str = p + 1;
}
}
static void explore_unload_icons(explore_state_t *state)
{
unsigned i;
if (!state)
return;
for (i = 0; i != RBUF_LEN(state->icons); i++)
if (state->icons[i])
video_driver_texture_unload(&state->icons[i]);
}
/* File-static generation counter for async icon loads */
static uint64_t explore_icon_load_gen = 0;
static void explore_load_icons(explore_state_t *state)
{
char path[PATH_MAX_LENGTH];
size_t i, _len, system_count;
bool supports_rgba = (video_driver_get_disp_flags() & VIDEO_FLAG_USE_RGBA);
if (!state)
return;
if ((system_count = RBUF_LEN(state->by[EXPLORE_BY_SYSTEM])) <= 0)
return;
/* unload any icons that could exist from a previous call to this */
explore_unload_icons(state);
/* Invalidate any in-flight async icon loads */
explore_icon_load_gen++;
/* RBUF_RESIZE leaves memory uninitialised,
have to zero it 'manually' */
RBUF_RESIZE(state->icons, system_count);
memset(state->icons, 0, RBUF_SIZEOF(state->icons));
fill_pathname_application_special(path, sizeof(path),
APPLICATION_SPECIAL_DIRECTORY_ASSETS_SYSICONS);
if (!*path)
return;
_len = fill_pathname_slash(path, sizeof(path));
for (i = 0; i != system_count; i++)
{
size_t __len = _len;
__len += strlcpy(path + _len,
state->by[EXPLORE_BY_SYSTEM][i]->str,
sizeof(path) - _len);
strlcpy_lit(path + __len, ".png", sizeof(path) - __len);
if (!path_is_valid(path))
continue;
gfx_display_load_icon(path, supports_rgba,
&state->icons[i], explore_icon_load_gen,
&explore_icon_load_gen);
}
}
explore_state_t *menu_explore_build_list(const char *directory_playlist,
const char *directory_database)
{
unsigned i;
char tmp[PATH_MAX_LENGTH];
struct explore_source
{
const struct playlist_entry *source;
uint32_t entry_index, meta_count;
};
struct explore_rdb
{
libretrodb_t *handle;
struct explore_source *playlist_crcs;
struct explore_source *playlist_names;
size_t count;
char systemname[NAME_MAX_LENGTH];
}
*rdbs = NULL;
int *rdb_indices = NULL;
explore_string_t **cat_maps[EXPLORE_CAT_COUNT] = {NULL};
explore_string_t **split_buf = NULL;
libretro_vfs_implementation_dir *dir = NULL;
explore_state_t *state = (explore_state_t*)calloc(1, sizeof(*state));
if (!state)
return NULL;
state->label_explore_item_str = MENU_ENUM_LABEL_EXPLORE_ITEM_STR;
/* Index all playlists */
for (dir = retro_vfs_opendir_impl(directory_playlist, false); dir;)
{
playlist_config_t playlist_config;
size_t j, used_entries = 0;
playlist_t *playlist = NULL;
const char *fext = NULL;
const char *fname = NULL;
uint32_t fhash = 0;
playlist_config.path[0] = '\0';
playlist_config.base_content_directory[0] = '\0';
playlist_config.capacity = 0;
playlist_config.old_format = false;
playlist_config.compress = false;
playlist_config.fuzzy_archive_match = false;
playlist_config.autofix_paths = false;
if (!retro_vfs_readdir_impl(dir))
{
retro_vfs_closedir_impl(dir);
break;
}
fname = retro_vfs_dirent_get_name_impl(dir);
if (fname)
fext = strrchr(fname, '.');
if (!fext || strcasecmp(fext, ".lpl"))
continue;
fill_pathname_join_special(playlist_config.path,
directory_playlist, fname, sizeof(playlist_config.path));
playlist_config.capacity = COLLECTION_SIZE;
playlist = playlist_init(&playlist_config);
fhash = ex_hash32_nocase_filtered(
(unsigned char*)fname, fext - fname, '0', 255);
for (j = 0; j < playlist_size(playlist); j++)
{
int rdb_num;
uint32_t entry_crc32;
struct explore_source src = { NULL, (uint32_t)-1, 0 };
struct explore_rdb* rdb = NULL;
const struct playlist_entry *entry = NULL;
const char *db_name = fname;
const char *db_ext = fext;
uint32_t rdb_hash = fhash;
playlist_get_index(playlist, j, &entry);
/* We also could build label from file name, for now it's required */
if (!entry->label || !*entry->label)
continue;
/* For auto scanned playlists the entry db_name matches the
* lpl file name and we can just use that */
if (entry->db_name && *entry->db_name
&& strcasecmp(entry->db_name, fname))
{
db_name = entry->db_name;
db_ext = strrchr(db_name, '.');
if (!db_ext)
db_ext = db_name + strlen(db_name);
rdb_hash = ex_hash32_nocase_filtered(
(unsigned char*)db_name, db_ext - db_name, '0', 255);
}
rdb_num = RHMAP_GET(rdb_indices, rdb_hash);
if (!rdb_num)
{
size_t _len;
struct explore_rdb newrdb;
char *ext_path = NULL;
newrdb.handle = libretrodb_new();
newrdb.count = 0;
newrdb.playlist_crcs = NULL;
newrdb.playlist_names = NULL;
_len = db_ext - db_name;
if (_len >= sizeof(newrdb.systemname))
_len = sizeof(newrdb.systemname)-1;
memcpy(newrdb.systemname, db_name, _len);
newrdb.systemname[_len] = '\0';
fill_pathname_join_special(
tmp, directory_database, db_name, sizeof(tmp));
/* Replace the extension - change 'lpl' to 'rdb' */
if (( ext_path = path_get_extension_mutable(tmp))
&& ext_path[0] == '.'
&& ext_path[1] == 'l'
&& ext_path[2] == 'p'
&& ext_path[3] == 'l')
{
ext_path[1] = 'r';
ext_path[2] = 'd';
ext_path[3] = 'b';
}
if (libretrodb_open(tmp, newrdb.handle, false) != 0)
{
/* Invalid RDB file */
libretrodb_free(newrdb.handle);
RHMAP_SET(rdb_indices, rdb_hash, -1);
continue;
}
RBUF_PUSH(rdbs, newrdb);
rdb_num = (int)RBUF_LEN(rdbs);
RHMAP_SET(rdb_indices, rdb_hash, rdb_num);
}
if ((uintptr_t)rdb_num == (uintptr_t)-1)
continue;
rdb = &rdbs[rdb_num - 1];
if (rdb)
{
rdb->count++;
entry_crc32 = (uint32_t)strtoul(
(entry->crc32 ? entry->crc32 : ""), NULL, 16);
src.source = entry;
if (entry_crc32)
{
RHMAP_SET(rdb->playlist_crcs, entry_crc32, src);
}
else
{
RHMAP_SET_STR(rdb->playlist_names, entry->label, src);
}
}
used_entries++;
}
if (used_entries)
RBUF_PUSH(state->playlists, playlist);
else
playlist_free(playlist);
}
/* Loop through all RDBs referenced in the playlists
* and load meta data strings */
for (i = 0; i != RBUF_LEN(rdbs); i++)
{
struct rmsgpack_dom_value item;
struct explore_rdb* rdb = &rdbs[i];
libretrodb_cursor_t *cur = libretrodb_cursor_new();
bool more =
(
libretrodb_cursor_open(rdb->handle, cur, NULL) == 0
&& libretrodb_cursor_read_item(cur, &item) == 0);
for (; more; more = (rmsgpack_dom_value_free(&item),
libretrodb_cursor_read_item(cur, &item) == 0))
{
unsigned k, l, cat;
explore_entry_t* e;
const char *fields[EXPLORE_CAT_COUNT];
char numeric_buf[EXPLORE_CAT_COUNT][16];
uint32_t crc32 = 0;
uint32_t meta_count = 0;
char *name = NULL;
#ifdef EXPLORE_SHOW_ORIGINAL_TITLE
char *original_title = NULL;
#endif
struct explore_source* src = NULL;
if (item.type != RDT_MAP)
continue;
for (k = 0; k < EXPLORE_CAT_COUNT; k++)
fields[k] = NULL;
for (k = 0; k < item.val.map.len; k++)
{
const char *key_str = NULL;
struct rmsgpack_dom_value *key = &item.val.map.items[k].key;
struct rmsgpack_dom_value *val = &item.val.map.items[k].value;
if (!key || !val || key->type != RDT_STRING)
continue;
key_str = key->val.string.buff;
if (!strcmp(key_str, "crc"))
{
switch (val->val.binary.len)
{
case 1:
crc32 = *(uint8_t*)val->val.binary.buff;
break;
case 2:
crc32 = swap_if_little16(*(uint16_t*)val->val.binary.buff);
break;
case 4:
crc32 = swap_if_little32(*(uint32_t*)val->val.binary.buff);
break;
default:
crc32 = 0;
break;
}
continue;
}
else if (!strcmp(key_str, "name"))
{
name = val->val.string.buff;
continue;
}
#ifdef EXPLORE_SHOW_ORIGINAL_TITLE
else if (!strcmp(key_str, "original_title"))
{
original_title = val->val.string.buff;
continue;
}
#endif
for (cat = 0; cat != EXPLORE_CAT_COUNT; cat++)
{
if (strcmp(key_str, explore_by_info[cat].rdbkey) != 0)
continue;
meta_count++;
if (explore_by_info[cat].is_numeric)
{
if (val->type >= RDT_STRING)
break;
snprintf(numeric_buf[cat],
sizeof(numeric_buf[cat]),
"%d", (int)val->val.int_);
fields[cat] = numeric_buf[cat];
break;
}
if (explore_by_info[cat].is_boolean)
{
if (val->type >= RDT_STRING)
break;
fields[cat] = msg_hash_to_str(val->val.int_ ?
MENU_ENUM_LABEL_VALUE_YES : MENU_ENUM_LABEL_VALUE_NO);
break;
}
if (val->type != RDT_STRING)
break;
fields[cat] = val->val.string.buff;
break;
}
}
if (crc32)
{
ptrdiff_t idx = RHMAP_IDX(rdb->playlist_crcs, crc32);
src = (idx != -1 ? &rdb->playlist_crcs[idx] : NULL);
}
if (!src && name)
{
ptrdiff_t idx = RHMAP_IDX_STR(rdb->playlist_names, name);
src = (idx != -1 ? &rdb->playlist_names[idx] : NULL);
}
if (!src)
continue;
if (src->entry_index != (uint32_t)-1 && src->meta_count >= meta_count)
continue;
if (src->entry_index == (uint32_t)-1)
{
src->entry_index = (uint32_t)RBUF_LEN(state->entries);
RBUF_RESIZE(state->entries, src->entry_index + 1);
}
e = &state->entries[src->entry_index];
src->meta_count = meta_count;
e->playlist_entry = src->source;
for (l = 0; l < EXPLORE_CAT_COUNT; l++)
e->by[l] = NULL;
e->split = NULL;
#ifdef EXPLORE_SHOW_ORIGINAL_TITLE
e->original_title = NULL;
#endif
fields[EXPLORE_BY_SYSTEM] = rdb->systemname;
for (cat = 0; cat != EXPLORE_CAT_COUNT; cat++)
{
explore_add_unique_string(state,
cat_maps, e, cat,
fields[cat], &split_buf);
}
#ifdef EXPLORE_SHOW_ORIGINAL_TITLE
if (original_title && *original_title)
{
size_t _len = strlen(original_title) + 1;
e->original_title = (char*)
ex_arena_alloc(&state->arena, _len);
/* NULL-check: arena alloc returns NULL on OOM. Skip
* the memcpy; e->original_title stays NULL (matches
* the 'e->original_title = NULL' initialisation a
* few lines above). Callers under
* EXPLORE_SHOW_ORIGINAL_TITLE are expected to gate
* reads of this field. */
if (e->original_title)
memcpy(e->original_title, original_title, _len);
}
#endif
if (RBUF_LEN(split_buf))
{
size_t _len;
RBUF_PUSH(split_buf, NULL); /* terminator */
_len = RBUF_SIZEOF(split_buf);
e->split = (explore_string_t **)
ex_arena_alloc(&state->arena, _len);
/* NULL-check: arena alloc returns NULL on OOM. Skip
* the memcpy; e->split stays NULL. Downstream
* iteration in the Explore menu checks 'e->split'
* before walking the pointer array. */
if (e->split)
memcpy(e->split, split_buf, _len);
RBUF_CLEAR(split_buf);
}
/* Do not leave early, even if all items have been found - merge all hits */
}
libretrodb_cursor_close(cur);
libretrodb_cursor_free(cur);
libretrodb_close(rdb->handle);
libretrodb_free(rdb->handle);
RHMAP_FREE(rdb->playlist_crcs);
RHMAP_FREE(rdb->playlist_names);
}
RBUF_FREE(split_buf);
RHMAP_FREE(rdb_indices);
RBUF_FREE(rdbs);
for (i = 0; i != EXPLORE_CAT_COUNT; i++)
{
uint32_t idx;
size_t _len = RBUF_LEN(state->by[i]);
if (state->by[i])
qsort(state->by[i], _len, sizeof(*state->by[i]),
(explore_by_info[i].is_numeric ?
explore_qsort_func_nums : explore_qsort_func_strings));
for (idx = 0; idx != _len; idx++)
state->by[i][idx]->idx = idx;
RHMAP_FREE(cat_maps[i]);
}
/* NULL is not a valid value as a first argument for qsort */
if (state->entries)
qsort(state->entries,
RBUF_LEN(state->entries),
sizeof(*state->entries), explore_qsort_func_entries);
return state;
}
static int explore_action_get_title(
const char *path, const char *label,
unsigned menu_type, char *s, size_t len)
{
strlcpy(s, (explore_state ? explore_state->title :
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_EXPLORE_TAB)), len);
return 0;
}
static void explore_append_title(char *s, size_t len,
const char* fmt, ...)
{
va_list ap;
size_t _len = strlen(s);
va_start(ap, fmt);
vsnprintf(s + _len, len - _len, fmt, ap);
va_end(ap);
}
static int explore_action_sublabel_spacer(
file_list_t *list, unsigned type, unsigned i,
const char *label, const char *path, char *s, size_t len)
{
#ifdef HAVE_OZONE
const char *menu_driver = menu_driver_ident();
/* Only add a blank 'spacer' sublabel when
* using Ozone
* > In XMB/GLUI it upsets the vertical layout
* > In RGUI it does nothing other than
* unnecessarily blank out the fallback
* core title text in the sublabel area */
if (!strcmp(menu_driver, "ozone"))
{
s[0] = ' ';
s[1] = '\0';
}
#endif
return 1; /* 1 means it'll never change and can be cached */
}
static int explore_action_ok(const char *path, const char *label,
unsigned type, size_t idx, size_t entry_idx)
{
const char *explore_tab = MENU_ENUM_LABEL_EXPLORE_TAB_STR;
if (type >= EXPLORE_TYPE_FIRSTITEM || type == EXPLORE_TYPE_FILTERNULL)
{
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *menu_list = menu_st->entries.list;
file_list_t *menu_stack = MENU_LIST_GET(menu_list, 0);
unsigned prev_type = menu_stack->list[menu_stack->size - 1].type;
unsigned cat = (prev_type - EXPLORE_TYPE_FIRSTCATEGORY);
if (cat < EXPLORE_CAT_COUNT)
{
explore_state_t *state = explore_state;
unsigned lvl, lvl_max = state->view_levels;
for (lvl = 0; lvl != lvl_max; lvl++)
if (state->view_cats[lvl] == cat)
break;
if (lvl == lvl_max)
{
/* new matching filter */
state->view_op[lvl] = EXPLORE_OP_EQUAL;
state->view_use_split[lvl] = explore_by_info[cat].use_split;
state->view_cats[lvl] = cat;
state->view_match[lvl] = (type == EXPLORE_TYPE_FILTERNULL ?
NULL : state->by[cat][type-EXPLORE_TYPE_FIRSTITEM]);
state->view_levels++;
}
else
{
/* switch from match to filter range */
explore_string_t* r1 = state->view_match[lvl];
explore_string_t* r2 = state->by[cat][type-EXPLORE_TYPE_FIRSTITEM];
state->view_op[lvl] = EXPLORE_OP_RANGE;
state->view_idx_min[lvl] = (r1->idx < r2->idx ? r1->idx : r2->idx);
state->view_idx_max[lvl] = (r1->idx < r2->idx ? r2->idx : r1->idx);
}
}
}
filebrowser_clear_type();
return generic_action_ok_displaylist_push(explore_tab,
NULL, explore_tab, type, idx, entry_idx, ACTION_OK_DL_PUSH_DEFAULT);
}
static int explore_cancel(const char *path,
const char *label, unsigned type, size_t idx)
{
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *menu_list = menu_st->entries.list;
file_list_t *menu_stack = MENU_LIST_GET(menu_list, 0);
unsigned closed_type = menu_stack->list[menu_stack->size - 1].type;
if (closed_type >= EXPLORE_TYPE_FIRSTITEM ||
closed_type == EXPLORE_TYPE_FILTERNULL)
{
/* popping one filter, check if popping filter range or match */
unsigned prev_type = menu_stack->list[menu_stack->size - 2].type;
unsigned cat = (prev_type - EXPLORE_TYPE_FIRSTCATEGORY);
unsigned lvl, lvl_max = explore_state->view_levels;
for (lvl = 0; lvl != lvl_max; lvl++)
{
if (explore_state->view_cats[lvl] != cat) continue;
if (explore_state->view_op[lvl] == EXPLORE_OP_EQUAL)
explore_state->view_levels--;
else
{
explore_state->view_op[lvl] = EXPLORE_OP_EQUAL;
if (!explore_state->view_match[lvl])
explore_state->view_match[lvl] =
explore_state->by[cat][explore_state->view_idx_min[lvl]];
}
break;
}
}
else if (closed_type == EXPLORE_TYPE_SEARCH)
explore_state->view_search[0] = '\0';
return action_cancel_pop_default(path, label, type, idx);
}
void explore_menu_entry(file_list_t *list, explore_state_t *state,
const char *path, unsigned type, int (*action_ok)(const char *path,
const char *label, unsigned type, size_t idx, size_t entry_idx))
{
menu_file_list_cbs_t *cbs;
if (!menu_entries_append(list, path, state->label_explore_item_str,
MENU_ENUM_LABEL_EXPLORE_ITEM, type, 0, 0, NULL))
return;
cbs = ((menu_file_list_cbs_t*)list->list[list->size-1].actiondata);
if (!cbs)
return;
cbs->action_ok = action_ok;
cbs->action_cancel = explore_cancel;
}
static void explore_action_find_complete(void *userdata, const char *line)
{
menu_input_dialog_end();
if (line && *line)
{
strlcpy(explore_state->view_search, line,
sizeof(explore_state->view_search));
explore_action_ok(NULL, NULL, EXPLORE_TYPE_SEARCH, 0, 0);
}
}
static int explore_action_ok_find(const char *path, const char *label,
unsigned type, size_t idx, size_t entry_idx)
{
menu_input_ctx_line_t line;
line.label = msg_hash_to_str(MENU_ENUM_LABEL_VALUE_SEARCH);
line.label_setting = NULL;
line.type = 0;
line.idx = 0;
line.cb = explore_action_find_complete;
menu_input_dialog_start(&line);
return 0;
}
static const char* explore_get_view_path(struct menu_state *menu_st,
menu_list_t *menu_list, file_list_t *menu_stack)
{
struct item_file *cur = (struct item_file *)&menu_stack->list[menu_stack->size - 1];
/* check if we are opening a saved view from the horizontal/tabs menu */
if (cur->type == MENU_SETTING_HORIZONTAL_MENU)
{
const menu_ctx_driver_t *driver_ctx = menu_st->driver_ctx;
if (driver_ctx->list_get_entry)
{
size_t selection = driver_ctx->list_get_selection
? driver_ctx->list_get_selection(menu_st->userdata) : 0;
size_t _len = driver_ctx->list_get_size
? driver_ctx->list_get_size(menu_st->userdata, MENU_LIST_TABS) : 0;
if (selection > 0 && _len > 0)
{
struct item_file *item = NULL;
/* Label contains the path and path contains the label */
if ((item = (struct item_file*)driver_ctx->list_get_entry(
menu_st->userdata, MENU_LIST_HORIZONTAL,
(unsigned)(selection - (_len +1)))))
return item->label;
}
}
}
/* check if we are opening a saved view via Content > Playlists */
if ( (cur->type == MENU_EXPLORE_TAB)
&& cur->path
&& memcmp(cur->path,
MENU_ENUM_LABEL_GOTO_EXPLORE_STR,
STRLEN_CONST(MENU_ENUM_LABEL_GOTO_EXPLORE_STR) + 1) != 0
)
return cur->path;
return NULL;
}
static void explore_on_edit_views(enum msg_hash_enums msg)
{
const char *_msg = msg_hash_to_str(msg);
struct menu_state *menu_st = menu_state_get_ptr();
if (menu_st->driver_ctx->environ_cb)
menu_st->driver_ctx->environ_cb(MENU_ENVIRON_RESET_HORIZONTAL_LIST,
NULL, menu_st->userdata);
runloop_msg_queue_push(_msg, strlen(_msg), 1, 180, true, NULL,
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
}
static int explore_action_ok_deleteview(const char *path, const char *label,
unsigned type, size_t idx, size_t entry_idx)
{
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *menu_list = menu_st->entries.list;
file_list_t *menu_stack = MENU_LIST_GET(menu_list, 0);
filestream_delete(explore_get_view_path(menu_st, menu_list, menu_stack));
explore_on_edit_views(MENU_ENUM_LABEL_EXPLORE_VIEW_DELETED);
/* if we're at the top of the menu we can't cancel so just refresh
what becomes selected after MENU_ENVIRON_RESET_HORIZONTAL_LIST. */
if (menu_stack->size == 1)
menu_st->flags |= MENU_ST_FLAG_ENTRIES_NEED_REFRESH;
return explore_cancel(path, label, type, idx);
}
static void explore_action_saveview_complete(void *userdata, const char *name)
{
size_t _len;
int count = 0, op;
char lvwpath[PATH_MAX_LENGTH];
intfstream_t *file = NULL;
rjsonwriter_t* w = NULL;
explore_state_t *state = explore_state;
settings_t *settings = NULL;
menu_input_dialog_end();
if (!name || !*name)
return;
settings = config_get_ptr();
_len = fill_pathname_join_special(lvwpath,
settings->paths.directory_playlist, name, sizeof(lvwpath));
strlcpy_lit(lvwpath + _len, ".lvw", sizeof(lvwpath) - _len);
if (filestream_exists(lvwpath))
{
const char *_msg = msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_VIEW_EXISTS);
runloop_msg_queue_push(_msg, strlen(_msg), 1, 360, true, NULL,
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR);
return;
}
if (!(file = intfstream_open_file(lvwpath,
RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE)))
{
RARCH_ERR("[Explore] Failed to write json file %s.\n", lvwpath);
return;
}
if (!(w = rjsonwriter_open_intfstream(file)))
{
/* Every rjsonwriter_* call below dereferences this without a
* check of its own. */
RARCH_ERR("[Explore] Failed to create json writer for %s.\n", lvwpath);
intfstream_close(file);
free(file);
filestream_delete(lvwpath);
return;
}
rjsonwriter_add_start_object(w);
if (state->view_search[0])
{
rjsonwriter_add_newline(w);
rjsonwriter_add_tabs(w, 1);
rjsonwriter_add_string(w, "filter_name");
rjsonwriter_add_colon(w);
rjsonwriter_add_space(w);
rjsonwriter_add_string(w, state->view_search);
count++;
}
for (op = EXPLORE_OP_EQUAL; op <= EXPLORE_OP_MAX; op++)
{
unsigned i, n;
for (i = n = 0; i != state->view_levels; i++)
{
uint8_t vop = state->view_op[i];
unsigned vcat = state->view_cats[i];
explore_string_t **by = state->by[vcat];
if (vop != op && (vop != EXPLORE_OP_RANGE || op == EXPLORE_OP_EQUAL))
continue;
if (n++ == 0)
{
if (count++)
rjsonwriter_add_comma(w);
rjsonwriter_add_newline(w);
rjsonwriter_add_tabs(w, 1);
rjsonwriter_add_string(w,
(op == EXPLORE_OP_EQUAL ? "filter_equal" :
(op == EXPLORE_OP_MIN ? "filter_min" :
(op == EXPLORE_OP_MAX ? "filter_max" : ""))));
rjsonwriter_add_colon(w);
rjsonwriter_add_space(w);
rjsonwriter_add_start_object(w);
}
if (n > 1)
rjsonwriter_add_comma(w);
rjsonwriter_add_newline(w);
rjsonwriter_add_tabs(w, 2);
rjsonwriter_add_string(w, explore_by_info[vcat].rdbkey);
rjsonwriter_add_colon(w);
rjsonwriter_add_space(w);
rjsonwriter_add_string(w,
((op == EXPLORE_OP_EQUAL && !state->view_match[i]) ? "" :
(op == EXPLORE_OP_EQUAL ? state->view_match[i]->str :
(op == EXPLORE_OP_MIN ? by[state->view_idx_min[i]]->str :
(op == EXPLORE_OP_MAX ? by[state->view_idx_max[i]]->str :
"")))));
}
if (n)
{
rjsonwriter_add_newline(w);
rjsonwriter_add_tabs(w, 1);
rjsonwriter_add_end_object(w);
}
}
rjsonwriter_add_newline(w);
rjsonwriter_add_end_object(w);
rjsonwriter_add_newline(w);
/* rjsonwriter_free() performs the final flush, so its result is what
* says whether the view was written completely. A short write used
* to be announced as "view saved" and left a truncated .lvw behind -
* which then fails to parse when the view is opened, and, because
* saving refuses to overwrite an existing file, also blocks saving
* the same view again under that name. Remove the partial file so
* the name stays free. */
if (!rjsonwriter_free(w))
{
RARCH_ERR("[Explore] Failed to write json file %s.\n", lvwpath);
intfstream_close(file);
free(file);
filestream_delete(lvwpath);
return;
}
intfstream_close(file);
free(file);
explore_on_edit_views(MENU_ENUM_LABEL_EXPLORE_VIEW_SAVED);
}
static int explore_action_ok_saveview(const char *path, const char *label,
unsigned type, size_t idx, size_t entry_idx)
{
menu_input_ctx_line_t line;
line.label = msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_NEW_VIEW);
line.label_setting = NULL;
line.type = 0;
line.idx = 0;
line.cb = explore_action_saveview_complete;
menu_input_dialog_start(&line);
return 0;
}
static void explore_load_view(explore_state_t *state, const char* path)
{
unsigned cat;
rjson_t* json;
intfstream_t *file;
enum rjson_type type;
uint8_t op = ((uint8_t)-1);
state->view_levels = 0;
state->view_search[0] = '\0';
if (!(file = intfstream_open_file(path,
RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE)))
return;
json = rjson_open_intfstream(file);
/* Configure parser */
rjson_set_options(json,
RJSON_OPTION_ALLOW_UTF8BOM|RJSON_OPTION_ALLOW_COMMENTS);
while ((type = rjson_next(json)) != RJSON_DONE && type != RJSON_ERROR)
{
unsigned int depth = rjson_get_context_depth(json);
if (depth == 1 && type == RJSON_STRING)
{
const char* key = rjson_get_string(json, NULL);
if ( !strcmp(key, "filter_name")
&& rjson_next(json) == RJSON_STRING)
strlcpy(state->view_search,
rjson_get_string(json, NULL),
sizeof(state->view_search));
else if ( !strcmp(key, "filter_equal")
&& rjson_next(json) == RJSON_OBJECT)
op = EXPLORE_OP_EQUAL;
else if ( !strcmp(key, "filter_min")
&& rjson_next(json) == RJSON_OBJECT)
op = EXPLORE_OP_MIN;
else if ( !strcmp(key, "filter_max")
&& rjson_next(json) == RJSON_OBJECT)
op = EXPLORE_OP_MAX;
}
else if ( (depth == 2)
&& (type == RJSON_STRING)
&& (op != ((uint8_t)-1)))
{
const char* key = rjson_get_string(json, NULL);
for (cat = 0; cat != EXPLORE_CAT_COUNT; cat++)
if (memcmp(key, explore_by_info[cat].rdbkey,
strlen(explore_by_info[cat].rdbkey) + 1) == 0)
break;
if (cat == EXPLORE_CAT_COUNT)
rjson_next(json); /* skip value */
else
{
explore_string_t **entries = state->by[cat];
const char* value = NULL;
unsigned lvl, lvl_max = state->view_levels;
uint8_t valid_op = ((uint8_t)-1);
type = rjson_next(json);
if ( type == RJSON_STRING
|| type == RJSON_NUMBER)
value = rjson_get_string(json, NULL);
if (value && !*value)
value = NULL;
for (lvl = 0; lvl != lvl_max; lvl++)
if (state->view_cats[lvl] == cat)
break;
if (!value && state->has_unknown[cat] && op == EXPLORE_OP_EQUAL)
{
state->view_match[lvl] = NULL;
valid_op = EXPLORE_OP_EQUAL;
}
else if (value && entries)
{
int cmp;
/* use existing qsort function for binary search */
explore_string_t *evalue = (explore_string_t *)
(value - offsetof(explore_string_t, str));
uint32_t i, ifrom, ito, imax = (uint32_t)RBUF_LEN(entries);
int (*compare_func)(const void *, const void *) =
(explore_by_info[cat].is_numeric
? explore_qsort_func_nums
: explore_qsort_func_strings);
/* binary search index where entry <= value */
for (i = 0, ifrom = 0, ito = imax, cmp = 1; ito && cmp;)
{
size_t remain = ito % 2;
ito = ito / 2;
i = ifrom + ito;
cmp = compare_func(&evalue, &entries[i]);
if (cmp >= 0)
ifrom = (uint32_t)(i + remain);
}
if (op == EXPLORE_OP_EQUAL && !cmp)
{
state->view_match[lvl] = entries[i];
valid_op = EXPLORE_OP_EQUAL;
}
else if (op == EXPLORE_OP_MIN)
{
state->view_idx_min[lvl] = (cmp ? i + 1 : i);
valid_op =
((lvl != lvl_max && state->view_op[lvl] == EXPLORE_OP_MAX)
? EXPLORE_OP_RANGE : EXPLORE_OP_MIN);
}
else if (op == EXPLORE_OP_MAX)
{
state->view_idx_max[lvl] = i;
valid_op =
((lvl != lvl_max && state->view_op[lvl] == EXPLORE_OP_MIN)
? EXPLORE_OP_RANGE : EXPLORE_OP_MAX);
}
}
if (valid_op != ((uint8_t)-1))
{
state->view_op [lvl] = valid_op;
state->view_cats [lvl] = cat;
state->view_use_split[lvl] = explore_by_info[cat].use_split;
if (lvl == lvl_max)
state->view_levels++;
}
}
}
else if (depth == 1 && type == RJSON_OBJECT_END)
op = ((uint8_t)-1);
}
rjson_free(json);
intfstream_close(file);
free(file);
}
unsigned menu_displaylist_explore(file_list_t *list, settings_t *settings)
{
unsigned i;
char tmp[1024];
struct explore_state *state = explore_state;
struct menu_state *menu_st = menu_state_get_ptr();
menu_handle_t *menu = menu_st->driver_data;
menu_list_t *menu_list = menu_st->entries.list;
file_list_t *menu_stack = MENU_LIST_GET(menu_list, 0);
struct item_file *stack_top = menu_stack->list;
size_t depth = menu_stack->size;
unsigned current_type = (depth > 0 ? stack_top[depth - 1].type : 0);
unsigned previous_type = (depth > 1 ? stack_top[depth - 2].type : 0);
unsigned current_cat = current_type - EXPLORE_TYPE_FIRSTCATEGORY;
/* overwrite the menu title function with our custom one */
/* depth 1 is never popped so we can only do this on sub menus */
if (depth > 1)
((menu_file_list_cbs_t*)stack_top[depth - 1].actiondata)
->action_get_title = explore_action_get_title;
if (!state)
{
if (!menu_explore_init_in_progress(NULL))
task_push_menu_explore_init(
settings->paths.directory_playlist,
settings->paths.path_content_database);
menu_entries_append(list,
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_EXPLORE_INITIALISING_LIST),
MENU_ENUM_LABEL_EXPLORE_INITIALISING_LIST_STR,
MENU_ENUM_LABEL_EXPLORE_INITIALISING_LIST,
FILE_TYPE_NONE, 0, 0, NULL);
return (unsigned)list->size;
}
/* check if we are opening a saved view */
if (current_type == MENU_SETTING_HORIZONTAL_MENU || current_type == MENU_EXPLORE_TAB)
{
const char* view_path = explore_get_view_path(menu_st, menu_list, menu_stack);
if (view_path)
{
explore_load_view(state, view_path);
current_type = EXPLORE_TYPE_VIEW;
}
}
/* clear any filter remaining from showing a view on the horizontal menu */
if (current_type == MENU_EXPLORE_TAB)
{
state->view_levels = 0;
state->view_search[0] = '\0';
}
/* clear title string */
state->title[0] = '\0';
/* append filtered categories to title */
for (i = 0; i != state->view_levels; i++)
{
unsigned cat = state->view_cats[i];
explore_string_t **entries = state->by[cat];
explore_string_t *match = state->view_match[i];
explore_append_title(state->title, sizeof(state->title), "%s%s: ", (i ? " / " : ""),
msg_hash_to_str(explore_by_info[cat].name_enum));
switch (state->view_op[i])
{
case EXPLORE_OP_EQUAL:
explore_append_title(state->title, sizeof(state->title), "%s", (match
? match->str
: msg_hash_to_str(MENU_ENUM_LABEL_VALUE_UNKNOWN)));
break;
case EXPLORE_OP_MIN:
explore_append_title(state->title, sizeof(state->title), "%s - %s",
entries[state->view_idx_min[i]]->str,
entries[RBUF_LEN(entries)-1]->str);
break;
case EXPLORE_OP_MAX:
explore_append_title(state->title, sizeof(state->title), "%s - %s",
entries[0]->str,
entries[state->view_idx_max[i]]->str);
break;
case EXPLORE_OP_RANGE:
explore_append_title(state->title, sizeof(state->title), "%s - %s",
entries[state->view_idx_min[i]]->str,
entries[state->view_idx_max[i]]->str);
break;
}
}
/* append string search to title */
if (*state->view_search)
explore_append_title(state->title, sizeof(state->title), "%s%s: '%s'",
(state->view_levels ? " / " : ""),
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_RDB_ENTRY_NAME),
state->view_search);
state->show_icons = (current_cat == EXPLORE_BY_SYSTEM)
? EXPLORE_ICONS_SYSTEM_CATEGORY
: (current_type >= EXPLORE_TYPE_FIRSTITEM)
? EXPLORE_ICONS_CONTENT
: EXPLORE_ICONS_OFF;
if ( current_type == MENU_EXPLORE_TAB
|| current_type == EXPLORE_TYPE_ADDITIONALFILTER)
{
/* Explore top or selecting an additional filter category */
unsigned cat;
bool is_top = (current_type == MENU_EXPLORE_TAB);
if (is_top)
strlcpy(state->title,
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_EXPLORE_TAB),
sizeof(state->title));
else
explore_append_title(state->title, sizeof(state->title), " / %s",
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_EXPLORE_ADDITIONAL_FILTER));
if (!*state->view_search)
{
/* Start navigation from first item */
if (menu_st->selection_ptr < 1)
menu_st->selection_ptr = 1;
explore_menu_entry(
list, state,
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_EXPLORE_SEARCH_NAME),
EXPLORE_TYPE_SEARCH, explore_action_ok_find);
if (list->size)
((menu_file_list_cbs_t*)list->list[list->size-1].actiondata)->action_sublabel =
explore_action_sublabel_spacer;
}
for (cat = 0; cat < EXPLORE_CAT_COUNT; cat++)
{
explore_string_t **entries = state->by[cat];
if (!RBUF_LEN(entries))
continue;
/* don't list already filtered categories unless it can be filtered by range */
for (i = 0; i != state->view_levels; i++)
if (state->view_cats[i] == cat)
break;
if (i == state->view_levels || (i == state->view_levels - 1
&& state->view_op[i] == EXPLORE_OP_EQUAL
&& !explore_by_info[cat].is_boolean
&& RBUF_LEN(state->by[cat]) > 1))
{
size_t _len = 0;
strlcpy_append(tmp, sizeof(tmp), &_len,
msg_hash_to_str(explore_by_info[cat].by_enum));
if (is_top)
{
if (explore_by_info[cat].is_numeric)
snprintf(tmp + _len,
sizeof(tmp) - _len,
" (%s - %s)",
entries[0]->str,
entries[RBUF_LEN(entries) - 1]->str);
else if (!explore_by_info[cat].is_boolean)
{
strlcpy_append(tmp, sizeof(tmp), &_len, " (");
_len += snprintf(tmp + _len, sizeof(tmp) - _len,
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_EXPLORE_ITEMS_COUNT),
(unsigned)RBUF_LEN(entries));
if (_len >= sizeof(tmp))
_len = sizeof(tmp) - 1;
strlcpy_append(tmp, sizeof(tmp), &_len, ")");
}
}
else if (i != state->view_levels)
{
strlcpy_append(tmp, sizeof(tmp), &_len, " (");
strlcpy_append(tmp, sizeof(tmp), &_len,
msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_RANGE_FILTER));
strlcpy_append(tmp, sizeof(tmp), &_len, ")");
}
explore_menu_entry(list, state,
tmp, cat + EXPLORE_TYPE_FIRSTCATEGORY, explore_action_ok);
}
}
if (is_top)
{
if (list->size)
((menu_file_list_cbs_t*)list->list[list->size-1].actiondata)->action_sublabel =
explore_action_sublabel_spacer;
explore_menu_entry(list, state,
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_EXPLORE_SHOW_ALL),
EXPLORE_TYPE_SHOWALL, explore_action_ok);
}
}
else if ((!state->view_levels && !*state->view_search
&& current_cat < EXPLORE_CAT_COUNT))
{
/* Unfiltered list of all items in a selected explore by category */
explore_string_t **entries = state->by[current_cat];
size_t i_last = RBUF_LEN(entries) - 1;
for (i = 0; i <= i_last; i++)
explore_menu_entry(list, state,
entries[i]->str, EXPLORE_TYPE_FIRSTITEM + i, explore_action_ok);
if (state->has_unknown[current_cat])
{
if (list->size)
((menu_file_list_cbs_t*)list->list[list->size-1].actiondata)->action_sublabel =
explore_action_sublabel_spacer;
explore_menu_entry(list, state,
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_UNKNOWN),
EXPLORE_TYPE_FILTERNULL, explore_action_ok);
}
explore_append_title(state->title, sizeof(state->title), "%s",
msg_hash_to_str(explore_by_info[current_cat].by_enum));
}
else if (current_type < EXPLORE_TYPE_FIRSTITEM
|| (previous_type >= EXPLORE_TYPE_FIRSTCATEGORY
&& previous_type < EXPLORE_TYPE_FIRSTITEM))
{
size_t first_list_entry;
unsigned view_levels = state->view_levels;
char* view_search = state->view_search;
uint8_t* view_op = state->view_op;
bool* view_use_split = state->view_use_split;
unsigned* view_cats = state->view_cats;
explore_string_t** view_match = state->view_match;
uint32_t* view_idx_min = state->view_idx_min;
uint32_t* view_idx_max = state->view_idx_max;
explore_entry_t* entries = state->entries, *e, *eend;
bool* map_filtered_category = NULL;
bool has_search = !!*view_search;
bool is_show_all = (!view_levels && !has_search);
bool is_filtered_category = (current_cat < EXPLORE_CAT_COUNT);
bool filtered_category_have_unknown = false;
if (is_show_all)
explore_append_title(state->title, sizeof(state->title),
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_EXPLORE_ALL));
if (is_filtered_category)
{
/* List filtered items in a selected explore by category */
if (!view_levels || view_cats[view_levels - 1] != current_cat)
explore_append_title(state->title, sizeof(state->title), " / %s",
msg_hash_to_str(explore_by_info[current_cat].by_enum));
else
{
/* List all items again when setting a range filter */
explore_append_title(state->title, sizeof(state->title), " (%s)",
msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_RANGE_FILTER));
view_levels--;
}
}
else if (current_type == EXPLORE_TYPE_VIEW)
{
/* Start navigation from first item */
if (menu_st->selection_ptr < 1)
menu_st->selection_ptr = 1;
/* Show a saved view */
state->show_icons = EXPLORE_ICONS_CONTENT;
explore_menu_entry(list, state,
msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_DELETE_VIEW),
EXPLORE_TYPE_VIEW, explore_action_ok_deleteview);
if (list->size)
((menu_file_list_cbs_t*)list->list[list->size-1].actiondata)->action_sublabel =
explore_action_sublabel_spacer;
}
else
{
/* Start navigation from first item */
if (menu_st->selection_ptr < 2)
menu_st->selection_ptr = 2;
/* Game list */
state->show_icons = EXPLORE_ICONS_CONTENT;
explore_menu_entry(list, state,
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_EXPLORE_ADD_ADDITIONAL_FILTER),
EXPLORE_TYPE_ADDITIONALFILTER, explore_action_ok);
explore_menu_entry(list, state,
msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_SAVE_VIEW),
EXPLORE_TYPE_VIEW, explore_action_ok_saveview);
if (list->size)
((menu_file_list_cbs_t*)list->list[list->size-1].actiondata)->action_sublabel =
explore_action_sublabel_spacer;
}
first_list_entry = list->size;
for (e = entries, eend = RBUF_END(entries); e != eend; e++)
{
for (i = 0; i != view_levels; i++)
{
explore_string_t* eby = e->by[view_cats[i]];
switch (view_op[i])
{
case EXPLORE_OP_EQUAL:
if (view_match[i] == eby)
continue;
if (view_use_split[i] && e->split)
{
explore_string_t** split = e->split;
do
{
if (*split == view_match[i])
break;
} while (*(++split));
if (*split)
continue;
}
goto SKIP_ENTRY;
case EXPLORE_OP_MIN:
if (eby && eby->idx >= view_idx_min[i])
continue;
goto SKIP_ENTRY;
case EXPLORE_OP_MAX:
if (eby && eby->idx <= view_idx_max[i])
continue;
goto SKIP_ENTRY;
case EXPLORE_OP_RANGE:
if (eby && eby->idx >= view_idx_min[i]
&& eby->idx <= view_idx_max[i])
continue;
goto SKIP_ENTRY;
}
}
if (has_search && !compat_strcasestr(e->playlist_entry->label, view_search))
goto SKIP_ENTRY;
if (is_filtered_category)
{
explore_string_t* str = e->by[current_cat];
if (!str)
{
filtered_category_have_unknown = true;
continue;
}
if (RHMAP_HAS(map_filtered_category, str->idx + 1))
continue;
RHMAP_SET(map_filtered_category, str->idx + 1, true);
explore_menu_entry(list, state, str->str,
EXPLORE_TYPE_FIRSTITEM + str->idx, explore_action_ok);
}
#ifdef EXPLORE_SHOW_ORIGINAL_TITLE
else if (e->original_title)
explore_menu_entry(list, state, e->original_title,
EXPLORE_TYPE_FIRSTITEM + (e - entries), explore_action_ok);
#endif
else
explore_menu_entry(list, state, e->playlist_entry->label,
(unsigned)(EXPLORE_TYPE_FIRSTITEM + (e - entries)), explore_action_ok);
SKIP_ENTRY:;
}
if (is_filtered_category)
qsort(list->list, list->size, sizeof(*list->list),
explore_qsort_func_menulist);
explore_append_title(state->title, sizeof(state->title),
" (%u)", (unsigned)(list->size - first_list_entry));
if (is_filtered_category && filtered_category_have_unknown)
{
if (list->size)
((menu_file_list_cbs_t*)list->list[list->size-1].actiondata)->action_sublabel =
explore_action_sublabel_spacer;
explore_menu_entry(list, state,
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_UNKNOWN),
EXPLORE_TYPE_FILTERNULL, explore_action_ok);
}
RHMAP_FREE(map_filtered_category);
}
else
{
/* Content page of selected game */
int pl_idx;
const struct playlist_entry *pl_entry =
state->entries[current_type - EXPLORE_TYPE_FIRSTITEM].playlist_entry;
strlcpy(state->title, pl_entry->label, sizeof(state->title));
for (pl_idx = 0; pl_idx != (int)RBUF_LEN(state->playlists); pl_idx++)
{
menu_displaylist_info_t info;
const struct playlist_entry *pl_first = NULL;
playlist_t *pl =
state->playlists[pl_idx];
playlist_get_index(pl, 0, &pl_first);
if ( (pl_entry < pl_first)
|| (pl_entry >= pl_first + playlist_size(pl)))
continue;
/* Fake all the state so the content screen
* and information screen think we're viewing via playlist */
menu_displaylist_info_init(&info);
playlist_set_cached_external(pl);
menu->rpl_entry_selection_ptr = (unsigned)(pl_entry - pl_first);
strlcpy(menu->deferred_path,
pl_entry->path, sizeof(menu->deferred_path));
info.list = list;
menu_displaylist_ctl(DISPLAYLIST_HORIZONTAL_CONTENT_ACTIONS, &info,
settings);
break;
}
}
return (unsigned)list->size;
}
uintptr_t menu_explore_get_entry_icon(unsigned type)
{
unsigned i;
if ( !explore_state
|| !explore_state->show_icons
|| (type < EXPLORE_TYPE_FIRSTITEM))
return 0;
i = (type - EXPLORE_TYPE_FIRSTITEM);
if (explore_state->show_icons == EXPLORE_ICONS_CONTENT)
{
explore_entry_t* e = &explore_state->entries[i];
if (e < RBUF_END(explore_state->entries))
return explore_state->icons[e->by[EXPLORE_BY_SYSTEM]->idx];
}
else if (explore_state->show_icons == EXPLORE_ICONS_SYSTEM_CATEGORY)
{
if (i < RBUF_LEN(explore_state->icons))
return explore_state->icons[i];
}
return 0;
}
ssize_t menu_explore_get_entry_playlist_index(unsigned type,
playlist_t **playlist, const struct playlist_entry **playlist_entry,
file_list_t *list, size_t *list_pos, size_t *list_size)
{
int pl_idx;
explore_entry_t* entry;
if ( !explore_state
|| (type < EXPLORE_TYPE_FIRSTITEM)
|| (explore_state->show_icons != EXPLORE_ICONS_CONTENT))
return -1;
entry = &explore_state->entries[type - EXPLORE_TYPE_FIRSTITEM];
if ( (entry >= RBUF_END(explore_state->entries))
|| !entry->playlist_entry)
return -1;
for (pl_idx = 0; pl_idx != (int)RBUF_LEN(explore_state->playlists); pl_idx++)
{
const struct playlist_entry* pl_first = NULL;
playlist_t *pl = explore_state->playlists[pl_idx];
playlist_get_index(pl, 0, &pl_first);
if ( (entry->playlist_entry < pl_first)
|| (entry->playlist_entry >= pl_first + playlist_size(pl)))
continue;
if (playlist)
*playlist = pl;
if (playlist_entry)
*playlist_entry = entry->playlist_entry;
/* correct numbers of list pos and list size */
if (list && list_pos && list_size)
while (*list_size && list->list[list->size-*list_size].type < EXPLORE_TYPE_FIRSTITEM)
{ (*list_size)--; (*list_pos)--; }
/* Playlist needs to get cached for on-demand thumbnails */
playlist_set_cached_external(pl);
return (ssize_t)(entry->playlist_entry - pl_first);
}
return -1;
}
ssize_t menu_explore_set_playlist_thumbnail(unsigned type,
gfx_thumbnail_path_data_t *thumbnail_path_data)
{
const char *db_name;
explore_entry_t* entry;
ssize_t playlist_index = -1;
playlist_t *playlist = NULL;
if ( !explore_state
|| (type < EXPLORE_TYPE_FIRSTITEM)
|| (explore_state->show_icons != EXPLORE_ICONS_CONTENT))
return -1;
entry = &explore_state->entries[type - EXPLORE_TYPE_FIRSTITEM];
if (entry >= RBUF_END(explore_state->entries))
return -1;
db_name = entry->by[EXPLORE_BY_SYSTEM]->str;
if (db_name && *db_name)
playlist_index = menu_explore_get_entry_playlist_index(
type, &playlist, NULL, NULL, NULL, NULL);
if (playlist_index >= 0 && playlist)
{
gfx_thumbnail_set_system(thumbnail_path_data, db_name, playlist);
gfx_thumbnail_set_content_playlist(thumbnail_path_data,
playlist, playlist_index);
return playlist_index;
}
gfx_thumbnail_set_content_playlist(thumbnail_path_data, NULL, 0);
return -1;
}
bool menu_explore_is_content_list(void)
{
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *menu_list = menu_st->entries.list;
file_list_t *menu_stack = MENU_LIST_GET(menu_list, 0);
if (explore_state)
return (explore_state->show_icons == EXPLORE_ICONS_CONTENT);
return explore_get_view_path(menu_st, menu_list, menu_stack) != NULL;
}
void menu_explore_context_init(void)
{
if (explore_state)
explore_load_icons(explore_state);
}
void menu_explore_context_deinit(void)
{
if (explore_state)
{
/* Invalidate in-flight async icon loads before unloading */
explore_icon_load_gen++;
explore_unload_icons(explore_state);
}
}
void menu_explore_free_state(explore_state_t *state)
{
unsigned i;
if (!state)
return;
for (i = 0; i != EXPLORE_CAT_COUNT; i++)
RBUF_FREE(state->by[i]);
RBUF_FREE(state->entries);
for (i = 0; i != RBUF_LEN(state->playlists); i++)
playlist_free(state->playlists[i]);
RBUF_FREE(state->playlists);
/* Invalidate in-flight async icon loads before freeing */
explore_icon_load_gen++;
explore_unload_icons(state);
RBUF_FREE(state->icons);
ex_arena_free(&state->arena);
}
void menu_explore_free(void)
{
if (!explore_state)
return;
menu_explore_free_state(explore_state);
free(explore_state);
explore_state = NULL;
}
void menu_explore_set_state(explore_state_t *state)
{
if (!state)
return;
if (explore_state)
menu_explore_free();
/* needs to be done now on the main thread */
explore_load_icons(state);
explore_state = state;
}