Files
RetroArch/database_info.h
T
Davey HughesandLibretroAdmin ad2e9e05d8 scan: parallel readers, 3DS product codes, CRCs and index builds over steps (#19920)
* samples/database: include compat/strl.h in the scan test

The test calls strlcpy(), which mingw-w64 does not declare, so it did
not build for Windows.

* PS1 scan: PSX.EXE means no serial only as the BOOT target

A root directory can list a PSX.EXE ahead of the executable SYSTEM.CNF
boots: Tomb Raider (USA) lists one before SLUS_001.52. Stopping at the
first PSX.EXE in the probe window sent such discs to a whole-track CRC,
eight seconds for a CHD. Look for the BOOT target first, matched without
regard to case so cdrom:\psx.exe counts. A disc that boots PSX.EXE, or
has no SYSTEM.CNF so the BIOS boots PSX.EXE, has no serial and still
falls back to its CRC (#15241); any other disc is searched for its
serial with PSX.EXE directory records ignored.

* 3DS scan: identify .3ds, .cci and .cxi by the NCCH product code

These images took the default whole-file CRC, 0.5-4 GB a game. The
product code in the NCCH header (partition 0 of a cart image, or the
start of a bare NCCH) is the serial the 3DS database is keyed on, and
the header is plaintext in encrypted dumps too. A code held by one
record matches it outright. Revisions and regions that share a code fall
back to the file's CRC to choose, and to the first record when no CRC
matches; with scan_serial_and_crc on every match is confirmed by CRC, as
for PSP and Dreamcast, and a file none of whose records carry its CRC
goes on to the plain CRC lookup with the CRC already computed. A code no
record carries, or a header without valid magic such as a CIA, keeps the
CRC path, in first-match mode too. Like the CRC lookup, the serial pass
visits only databases an installed core claims the extension for, unless
Scan Without Core Match is on.

* scan: run ITERATE steps while the shared I/O window lasts

A file takes about twenty ITERATE steps, and the handler ran one per
call. On the regular task queue that is one step per frame: 1000 NES
zips took 335 s of pacing, 400 arcade sets 1639 s. The ITERATE states
now run in a loop under the same window the BEGIN states use, which
times the steps rather than counting them and grants every call at least
one. The scan test's unclaimed-database lane spends the window before
each check, so that it still counts steps.

* scan: hash file CRCs a quantum at a time across steps

A step that needed a CRC computed all of it in one handler call: a whole
CHD track with LZMA decode, or a multi-gigabyte 3DS image. On the
regular queue that froze a frame, and on the threaded queue it held the
worker for seconds. Each CRC site now opens its span and returns, and
later steps hash 256 KiB quanta while the shared I/O window lasts, at
least one per call. The lookup that wanted the CRC runs when it is in,
at the same entry. The one-shot CRC helpers, which nothing else called,
are gone.

* scan: walk each database once for its crc and serial indexes

The first file to reach the serial lookup built a crc index and then a
serial index for every database, a full walk each: 47 MB of Mobile -
J2ME twice in one handler call, the second for an index that came out
empty. The crc walk now reads serials as well. It notes whether a
database has any, so the serial lookup passes over one with none, and
for the serial lookup it builds the serial index in the same pass. Index
contents, matches and the split of the index budget are unchanged.

* scan: let a database's indexes be built a run of records at a time

A database's crc and serial indexes were built by one walk and a qsort
in a single call: 50-70 ms for Mobile - J2ME. libretrodb_scan_begin(),
_step() and _end() walk a run of records per call, and
database_info_index_job_*() builds both indexes over as many calls as it
takes, each walking a run of records or merging a run of entries in a
resumable merge sort. libretrodb_scan_field2() and
database_info_index_new() are built on them and answer as before.

* scan: build each database's indexes over steps

The first file to reach a database built its indexes in one handler
call: 52-68 ms on the regular queue for Mobile - J2ME, 13-17 frames'
worth of the 4 ms window. The build now runs over the steps that follow
the lookup that began it, a run of records walked or entries merged at a
time while the window lasts, and the lookup takes the indexes when they
are done. A database counts as size-checked only then. Cancelling frees
a build in flight. The one-shot builders, database_info_crc_index_new(),
database_info_serial_index_new(), database_info_index_new() and
libretrodb_scan_field() and _field2(), lose their last caller here and
go; the tests build through the job and the stepped scan.

* task_queue samples: build on macOS, run on GCD in a macOS job

No sample ran the GCD runner and nothing in CI built HAVE_GCD. Apple's
ld has no --wrap, so on Darwin the switch and init-failure tests
redirect the wrapped calls from task_queue.c by macro instead, and
libdispatch comes from libSystem there. The switch test gets a GCD=1
build, and a macOS job runs every task_queue sample plain, under
ASan+UBSan and TSan, threaded and, where it has a GCD=1 build, on GCD.

* task_queue: a switch waits out a wake from another thread

task_queue_wake() is documented as safe from any thread, but a switch
between the threaded and the unthreaded queue frees the threaded
runner's lock and condition variable while a waker on another thread can
still be inside it. Toggling Threaded Video makes that switch at the
next frame. Two seq_cst counters now keep them apart without a lock: a
switch that finds a waker inside is put off to the next
task_queue_check(), and a waker that finds a switch under way leaves its
task to it. A wake on the thread that drives the queue cannot meet a
switch and skips the counters.

A switch now ends every park, and the new runner takes wakes again
before it runs any task, so a parked task loses nothing to a wake
dropped during the switch. A wake dropped while a switch is put off and
then taken back, or for a task whose park a switch keeps because it
leaves a stuck handler behind, waits for that park's horizon.

The switch test holds a waker inside the queue across a switch, wakes
from four threads through 1000 switches, and holds the main thread where
the new runner first runs a task while that task's first pass wakes it.

GCD builds leave out the threaded runner's own code, which they never
used and which warned there. The switch test also checks that its
realloc() wrapper ran, since on macOS that wrap is a macro rather than a
link option.

* scan: a scan of one file only adds to its playlist

A Content Location that is a file is scanned as one file, which the Scan
button, --scan=FILE and iOS "open in" already rely on. Such a scan
cleared the playlist when Overwrite was on, and recorded the file, and
its core, as what the playlist refreshes with, so Refresh Playlist later
rescanned that file alone. Overwrite is now ignored for a file, and the
playlist's scan record and default core are left as they were. Validate
Existing Entries is skipped too: a file scan's extensions can differ
from its folder's, so validating with them could drop entries the folder
scan added. A later folder scan or Refresh still prunes. With no
database and an automatic target playlist, a file is filed under its
folder's name, as a scan of that folder files it, rather than in a
playlist named after the file. A Content Location that is missing, as on
a drive that is offline, counts as a file, so Overwrite no longer
empties its playlist while the drive is away.

* menu: Content Location takes a file or a folder

Content Scan's Content Location now opens one browser that lists folders
and files in every scan method: OK on a file picks it, <Use This
Directory> picks the folder. In Automatic it lists every file, as an
Automatic scan reads every file; Custom keeps listing the scan's
extensions. The Scan Single File toggle, which switched the browser to
files and was shown only in Custom with Search Recursively off, goes
with its setting and strings, and a scan of a file no longer turns
Search Recursively off behind the user's back. Import Content drops Scan
Directory and Scan File, as the Scan Directory list already had, leaving
Content Scan as the one way in; Scan File no longer started a scan. On
Apple, File > Open and the iOS document picker opened from Content
Location set the location as OK on a file does.

* task_queue: hold pushes from other threads for the main thread

task_queue_push() from another thread could meet a switch between the
threaded and the unthreaded queue, which frees the lock and condition
variable the push takes. On the unthreaded queue, with no switch at all,
it put the task on a list the main thread walks without a lock. Windows
pushes from the video thread under threaded video, and HID builds from
libusb's thread. Such a push now goes through the switch's gate: when
the gate is shut, or the unthreaded queue is up, the task goes onto a
lock-free list that the next gather pushes on the main thread, and the
push returns true. A push is also held while earlier ones still wait, so
a thread's pushes start in the order it made them. A BLOCKING task
refused there is cancelled, and its callback still runs. The switch test
pushes from four threads on the unthreaded queue and through 1000
switches, checks a held BLOCKING push, and holds the main thread inside
the gather that reopens the queue to show that a thread's next push does
not pass its held one.

* task_queue: cancel by ident and find safely from other threads

task_queue_cancel_task() and task_queue_find() from another thread met
the same switch and the same unlocked lists as a push did.
task_queue_cancel_ident() cancels by ident from any thread: a locked
walk when the threaded queue is up with nothing held, otherwise an ident
left for the next gather, which applies it after its pushes, to a held
push as well. A held push cancelled so retires cancelled without
running, and its callback runs. A find from another thread goes through
the switch's gate and reports false when it cannot walk the lists. The
queue's own workers and GCD threads skip the gate, and a find on the
main thread also sees held pushes. Each worker keeps its thread id in a
slot of its own and clears it before it reports that it left, so a
thread that reuses a joined worker's id is not taken for one. The switch
test cancels and finds from other threads on both queues and across a
put-off switch, and holds the main thread in the gather to show that a
thread's cancel of its own held push is not lost. Once extra workers
start, finds from handlers on the one worker and on an extra still skip
the gate, and a find with a joined extra worker's id from inside deinit
is refused.

* autoconfig: make connects reported on other threads on the main thread

libusb's hotplug thread and the Wii's HID poll thread report pads from
their own threads, where task_queue_find() can no longer answer exactly:
at a switch, or on the unthreaded queue, it reports false. Off the main
thread, input_autoconfigure_connect_ex() and
input_autoconfigure_disconnect() now copy the request into a main-thread
task and return true. Its callback makes the call on the main thread, in
the order the requests were made, so the duplicate check, the port's
sequence number and the settings it reads are the main thread's. A new
sample drives task_autodetect.c on the task queue with the rest stubbed,
reports pads from another thread, and checks that every find runs on the
main thread, that a pad reported twice queues one connect, that a
disconnect handed off is made and that a cancelled request is not.

* task_queue: wait, reset and deinit see pushes held for the main thread

On the unthreaded queue every push from another thread is held for the
next gather. A wait with nothing running returned without running it,
and a reset, the exit drain's included, left it to run normally
afterwards. Cancels held for the gather were never freed at deinit.

The unthreaded wait now drains held pushes first, and a reset drains
them into the runner before it cancels, so a held task meets a queued
one's fate. Deinit applies and frees held cancels; held pushes stay held
for the next runner's first gather, so no callback runs inside deinit.

* task_queue: make the Threaded Tasks flag atomic

Task handlers on the worker read it through task_queue_is_threaded(),
and the main thread writes it when the setting changes, so changing the
setting while a task ran was a data race.  ThreadSanitizer shows it on a
database scan whose queue is switched under it.

* task_queue: a parallel task names the extra workers it needs

retro_task_t gains parallel_workers, which task_init() clears. A
parallel push starts the extra workers it finds missing, up to the
larger of task_queue_set_parallel_workers() and the task's number, at
most 8. They serve every parallel task and stay until deinit, as before,
so the pool only grows; 0 keeps the frontend's number. The started count
is atomic, so a push that finds enough takes no lock, and one that needs
more claims the next entries under running_lock.

The parallel test has a task asking for three, a later one asking for
fewer, one asking for more than eight, a plain task whose number is
ignored, the frontend's number holding for a task asking for fewer, and
a task that leaves the number alone.

* task_queue: let deinit leave extra workers behind in detachable handlers

A parallel task may now also be detachable, and then runs on the extra
workers like any other parallel task. Deinit gives every worker the one
bound it gave the one worker, the extra workers first. One still inside
a detachable handler past it is left behind to finish alone, as the one
worker was, and one inside any other handler is still waited for. The
primitives of an init are held by deinit and by each worker it leaves
behind, and the last of them to let go frees them, so the one worker and
several extras can be left behind together.

A worker left behind, the one or an extra, now runs the task's cleanup
once the handler returns, then frees the task, so a detachable task
releases what it holds however it ends. Its callback still never runs;
its cleanup, like its handler, may touch only what the task owns.

The parallel test runs a detachable parallel task on an extra worker,
leaves one stuck past the bound, waits for a slow one that is not
detachable, and leaves the one worker and two extras behind together
with a new queue running beside them. Each task left behind is cleaned
up once, by its own worker, after its handler returns, and one not left
behind at its retirement as before. The switch test switches to the
unthreaded queue and back with extra workers live.

* task_queue: on Emscripten, sleep after a pass on the one worker only

On Emscripten every worker sleeps 1 ms after each handler call, a
pthreads workaround from when the one worker ran every task.  The extra
workers run parallel tasks, which work in many short passes, and the
sleep cost each of those passes 1 ms, more than the pass itself. The
extra workers no longer sleep; the one worker still does.

* emscripten: ask for at most 4 extra workers, and start 5 threads

In the browser a pthread the pool has not started is made on demand by
the main thread, and with the JS file system, where every thread's file
calls go through the main thread, 8 extra workers made each of those
calls about 40% dearer than 4 did.  The frontend asked for cores - 3
extra workers, up to 8; on Emscripten it now asks for at most 4, and the
build starts 5 pthreads up front, enough for the one worker and those 4.

* scan: read small plain files on parallel reader tasks, in list order

A database scan on the threaded queue pushes min(4, cores) reader tasks,
none below 2, as parallel tasks that ask for that many extra workers, in
HAVE_THREADS builds with lock-free atomics, except on Wii U, whose C
library and FAT layer take no locks, and on Vita, where Vita3K's sceIo
tables take none.  With Threaded Tasks off none starts and the scan
reads every file itself, stepped as before.

The scan hands the plain files and archives it would CRC whole to the
readers, up to 64 per reader ahead of it, each with its own copy of the
path.  A reader claims them in list order, opens and sizes each, leaves
one over 128 MiB to the scan unread and CRCs the rest a quantum per
call.  The scan takes each result at its own place in the content list,
so the databases see files in the same order as before, parks while the
next one is still being read, and reads itself whatever a reader could
not.

The scan never waits for its readers.  At its end, on cancel and once
the queue is unthreaded, each reader leaves at its next call and hands
back the file in hand.  Each reader drops its reference to what they
share in its cleanup, and the last reference frees it.  A queue deinit
leaves a reader stuck in a read behind after its 1 s bound and never
calls it again; the worker left with it runs its cleanup once the read
returns, which hands back the file in hand and drops the reference.

The scan test covers list order, files over the cutoff, failed pushes,
cancels and quits with a reader stuck in a read, and queue switches
under a scan; CI runs it under TSan, without threads and with no
readers.

* scan: run the command-line scan with no menu on the threaded queue

A command-line scan with no menu waits for the task queue to run dry,
and only a scan on the threaded queue gets readers.  In threaded builds
that run now uses the threaded queue whatever Threaded Tasks says; the
setting is not changed.

* menu: Import Content offers Scan Directory and Scan File

Both entries sit beside Content Scan when the database is built in.
OK on a file in Scan File's browser scans that file, as the Scan
action does.  The load_content_filter harness checks that the
entries are listed and that OK on a file starts its scan.

* manual scan: Content Location holds a full file path

The location can now be a file, so it is kept in a PATH_MAX_LENGTH
buffer, as is the scan task's copy.  A file whose path does not fit
is refused in the browser instead of being joined short.  The
load_content_filter harness sets a location deeper than a folder
buffer and checks that it is kept whole.

---------

Co-authored-by: LibretroAdmin <[email protected]>
2026-10-26 13:28:42 +01:00

283 lines
9.9 KiB
C

/* RetroArch - A frontend for libretro.
* Copyright (C) 2010-2014 - Hans-Kristian Arntzen
* Copyright (C) 2011-2017 - Daniel De Matteis
* 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/>.
*/
#ifndef DATABASE_INFO_H_
#define DATABASE_INFO_H_
#include <stdint.h>
#include <stddef.h>
#include <file/archive_file.h>
#include <retro_common_api.h>
#include <queues/task_queue.h>
RETRO_BEGIN_DECLS
enum database_status
{
DATABASE_STATUS_NONE = 0,
DATABASE_STATUS_ITERATE,
DATABASE_STATUS_ITERATE_BEGIN,
DATABASE_STATUS_ITERATE_START,
DATABASE_STATUS_ITERATE_NEXT,
DATABASE_STATUS_FREE
};
enum database_type
{
DATABASE_TYPE_NONE = 0,
DATABASE_TYPE_ITERATE,
DATABASE_TYPE_ITERATE_ARCHIVE,
DATABASE_TYPE_ITERATE_LUTRO,
DATABASE_TYPE_SERIAL_LOOKUP,
DATABASE_TYPE_SERIAL_LOOKUP_SIZEHINT,
DATABASE_TYPE_CRC_LOOKUP
};
enum database_query_type
{
DATABASE_QUERY_NONE = 0,
DATABASE_QUERY_ENTRY,
DATABASE_QUERY_ENTRY_PUBLISHER,
DATABASE_QUERY_ENTRY_DEVELOPER,
DATABASE_QUERY_ENTRY_ORIGIN,
DATABASE_QUERY_ENTRY_FRANCHISE,
DATABASE_QUERY_ENTRY_RATING,
DATABASE_QUERY_ENTRY_BBFC_RATING,
DATABASE_QUERY_ENTRY_ELSPA_RATING,
DATABASE_QUERY_ENTRY_ESRB_RATING,
DATABASE_QUERY_ENTRY_PEGI_RATING,
DATABASE_QUERY_ENTRY_CERO_RATING,
DATABASE_QUERY_ENTRY_ENHANCEMENT_HW,
DATABASE_QUERY_ENTRY_EDGE_MAGAZINE_RATING,
DATABASE_QUERY_ENTRY_EDGE_MAGAZINE_ISSUE,
DATABASE_QUERY_ENTRY_FAMITSU_MAGAZINE_RATING,
DATABASE_QUERY_ENTRY_RELEASEDATE_MONTH,
DATABASE_QUERY_ENTRY_RELEASEDATE_YEAR,
DATABASE_QUERY_ENTRY_MAX_USERS,
DATABASE_QUERY_ENTRY_GENRE,
DATABASE_QUERY_ENTRY_REGION
};
typedef struct
{
enum database_status status;
enum database_type type;
} database_info_handle_t;
typedef struct
{
char *name;
char *rom_name;
char *serial;
char *description;
char *genre;
char *category;
char *language;
char *region;
char *score;
char *media;
char *controls;
char *artstyle;
char *gameplay;
char *narrative;
char *pacing;
char *perspective;
char *setting;
char *visual;
char *vehicular;
char *publisher;
struct string_list *developer;
char *origin;
char *franchise;
char *edge_magazine_review;
char *bbfc_rating;
char *elspa_rating;
char *esrb_rating;
char *pegi_rating;
char *cero_rating;
char *enhancement_hw;
char *sha1;
char *md5;
void *userdata;
int achievements;
int console_exclusive;
int platform_exclusive;
int analog_supported;
int rumble_supported;
int coop_supported;
uint32_t crc32;
uint64_t size;
unsigned famitsu_magazine_rating;
unsigned edge_magazine_rating;
unsigned edge_magazine_issue;
unsigned max_users;
unsigned releasemonth;
unsigned releaseyear;
unsigned tgdb_rating;
} database_info_t;
typedef struct
{
database_info_t *list;
size_t count;
} database_info_list_t;
/* Field selection flags for database_info_list_new_filtered.
* Controls which fields are extracted from each record.
* 0 = extract all fields. */
#define DB_EXTRACT_NAME (1 << 0)
#define DB_EXTRACT_CRC (1 << 1)
#define DB_EXTRACT_SERIAL (1 << 2)
#define DB_EXTRACT_SIZE (1 << 3)
#define DB_EXTRACT_MD5 (1 << 4)
#define DB_EXTRACT_SHA1 (1 << 5)
/* Preset used by the ROM scanner */
#define DB_EXTRACT_SCAN_FIELDS \
(DB_EXTRACT_NAME | DB_EXTRACT_CRC | DB_EXTRACT_SERIAL | DB_EXTRACT_SIZE)
database_info_list_t *database_info_list_new(const char *rdb_path,
const char *query);
/* One-slot cache of the most recent async database scan result
* (see tasks/task_database_info.c). The cache retains ownership
* of the returned list - callers must not free it. */
database_info_list_t *menu_dbinfo_cache_get(const char *path,
const char *query);
bool menu_dbinfo_cache_has(const char *path, const char *query);
/* A crc -> record-offset index over one database, built in a single
* pass. The scanner otherwise walks a whole database per content
* file; with an index it walks once and binary-searches thereafter.
* Costs 8 bytes per indexed record - a 32-bit offset beside the key -
* and lives for as long as the caller keeps it. */
typedef struct database_info_crc_index database_info_crc_index_t;
/* Bytes the index actually holds, for a caller tracking a budget
* across several databases. */
size_t database_info_crc_index_bytes(const database_info_crc_index_t *idx);
void database_info_crc_index_free(database_info_crc_index_t *idx);
size_t database_info_crc_index_count(const database_info_crc_index_t *idx);
/* Size range of the records the index covers, collected during the
* same walk that built it. False when no record carried a size. */
bool database_info_crc_index_size_range(
const database_info_crc_index_t *idx, int64_t *min, int64_t *max);
/* Records whose crc is @crc or @archive_crc, in file order, with
* @fields extracted - the same list database_info_list_new_filtered()
* returns for "{crc:or(...)}". NULL if the lookup could not be
* served, so the caller falls back to the query path. */
/* @rdb_path is the database the caller believes @idx describes; the
* lookup refuses if it does not match, so an index paired with the
* wrong database degrades to the query path instead of answering with
* another system's records. Pass NULL to skip the check. */
database_info_list_t *database_info_list_new_crc(
database_info_crc_index_t *idx, const char *rdb_path,
uint32_t crc, uint32_t archive_crc, unsigned fields);
/* The same treatment for the serial lookup disc content uses. A
* serial is variable-length, so the index keeps a hash and the
* candidate records are read back and compared exactly: a collision
* costs an extra read, never a wrong match. */
typedef struct database_info_serial_index database_info_serial_index_t;
size_t database_info_serial_index_bytes(
const database_info_serial_index_t *idx);
void database_info_serial_index_free(database_info_serial_index_t *idx);
size_t database_info_serial_index_count(
const database_info_serial_index_t *idx);
database_info_list_t *database_info_list_new_serial(
database_info_serial_index_t *idx, const char *rdb_path,
const char *serial, unsigned fields);
/* What database_info_index_job_new() builds. */
enum database_info_index_want
{
DATABASE_INFO_INDEX_CRC = (1 << 0),
DATABASE_INFO_INDEX_SERIAL = (1 << 1),
/* Whether any record carries a serial, without the serial index. */
DATABASE_INFO_INDEX_HAS_SERIAL = (1 << 2)
};
/* Builds the crc index of a database and its serial index in one
* walk, spread over as many calls as it takes, so that no call walks
* or sorts a whole database. */
typedef struct database_info_index_job database_info_index_job_t;
/* NULL when @rdb_path cannot be opened or @want names no index.
* @max_bytes caps the crc index's entry table and the serial index
* gets what the crc index leaves; an index that cannot be built whole
* comes back NULL, because a partial index would miss matches without
* saying so. Zero means no limit. */
database_info_index_job_t *database_info_index_job_new(
const char *rdb_path, size_t max_bytes, unsigned want);
/* Walks up to @records records or merges up to @entries index
* entries; both must be non-zero. Unlike finish and free, it takes no
* NULL job. True once the job is done, built or failed. */
bool database_info_index_job_step(database_info_index_job_t *job,
unsigned records, size_t entries);
/* Hands over what a done job built and frees the job. Only what the
* job was asked for comes back; the other pointers may be NULL.
* @has_serial says whether any record carries a serial the serial
* index keys on; it is complete whenever an index comes back. */
void database_info_index_job_finish(database_info_index_job_t *job,
database_info_crc_index_t **crc,
database_info_serial_index_t **serial, bool *has_serial);
/* Frees a job, done or not. */
void database_info_index_job_free(database_info_index_job_t *job);
database_info_list_t *database_info_list_new_filtered(const char *rdb_path,
const char *query, unsigned fields);
void database_info_list_free(database_info_list_t *list);
database_info_handle_t *database_info_dir_init(const char *dir,
enum database_type type, char* file_exts,
bool show_hidden_files, bool recursive, bool include_archive,
struct string_list **content_list);
/* As database_info_dir_init(), but over a content list the caller has
* already built (e.g. incrementally via dir_list_iter_step() so the
* walk could be spread across task gathers). Applies the same
* cue/gdi-prioritising sort the directory variant applies and borrows
* @list without taking ownership. Returns NULL only on allocation
* failure. */
database_info_handle_t *database_info_dir_init_from_list(
enum database_type type, struct string_list *list);
database_info_handle_t *database_info_file_init(const char *path,
enum database_type type, retro_task_t *task, struct string_list **content_list);
void database_info_free(database_info_handle_t *handle);
int database_info_build_query_enum(
char *query, size_t len, enum database_query_type type, const char *path);
/* NOTE: Allocates memory, it is the caller's responsibility to free the
* memory after it is no longer required. */
char *bin_to_hex_alloc(const uint8_t *data, size_t len);
RETRO_END_DECLS
#endif /* CORE_INFO_H_ */