Files
RetroArch/database_info.h
T
Davey Hughesandlibretroadmin 770391e8d5 scan: read an archive whole only when a database may hold its size (#19944)
* scan: read an archive's directory once for all its passes

The scan fetched an archive's first member's CRC, size and extension
through a helper that stops at the first entry and reports how many
the header counts, and its readers did the same.  One walk now reads
the whole directory, in the readers as in the scan: every member's
stored CRC, size and name, kept for each of the archive's passes.
Its transfer state is on the heap, out of the readers' frames.  The
walk replaces the helper in the archive passes; the helper stays for
its other users.

The walk decodes the first member for a CRC where the archive stores
none, a zstd stream or a RAR 5 member hashed with BLAKE2, so it is
looked up as before.  Later members are not decoded and keep a CRC of
0.  A .bz2 is decoded to be listed at all.

Folders are not members: an archive of a folder and one file has one
member, that file, and is looked up by its CRC first.

* zip: report a TorrentZip end comment

A zip whose end-of-central-directory comment is TORRENTZIPPED- and
eight hex digits was written by TorrentZip, as arcade sets are.  The
zip reader already reads that record to find the directory, so it now
says so in the transfer state, for a caller that wants the hint.  The
CRC in the comment is not checked: nothing depends on it.

* database_info, libretro-db: archive record sizes beside the crc index

The crc index build walks each database once, off the main thread.
In the same walk it now also collects the size of every record whose
rom_name ends in an archive extension (zip, 7z, apk, rar, zst), kept
in one sorted array keyed by size and type, so a caller can ask
whether a database may hold an archive of a given type and size.  A
record of that type with no size, or an index built without the
sizes, answers yes.  The sizes count against the index budget.

The stepped database scan reports one string field for this:
libretrodb_scan_begin() and its callback gain a name field.  A
record's rom_name can also be extracted on request.

* scan: look every archive member up from the directory

An archive whose first member missed was read whole and, when that
missed too, expanded: each member went through the databases again,
by the stored CRC the directory already held.  An archive of several
members was read whole before any lookup, even where no database
could hold it, so disc zips and console sets were read whole again.

One member: its stored CRC is looked up first, as before.  A hit in a
database claimed only for the member's type is held, and every
member's CRC is looked up next.  On a miss the archive is read whole
only when a database whose cores open its type may hold it by its
exact size; its own CRC is then looked up there, and where that is
not asked or misses, every member's.

Several members: the same size question comes first.  Each database
whose cores open the type is then asked for the first member's CRC,
and for the archive's own CRC only when the size let it be read
whole, so an arcade record still wins and no database claimed only
for a member's type is walked before it.  Where neither matches,
every member's stored CRC is looked up.

A database may hold the archive when it has a record whose rom_name
is of the archive's type and size, one of that type with no size, or
no sizes at all.  Every member is looked up with the gates and size
checks the expansion uses.  A hit names the archive itself only when
it is the one record hit, that record is named for this archive and
its database's cores open the type; other hits are written as the
expansion wrote them, archive#member.  The expansion stays for scans
that keep unmatched files, for archives whose directory cannot be
read, and where nothing hit and a member stores no CRC, which only
the expansion unpacks for one.

Readers read an archive whole ahead only after a whole match, twice
as many after each such match up to the cap; any other verdict takes
the rest back.  A TorrentZip set of several members is still read
whole by a reader with no grant while some database opens zip.  An
archive over the readers' cutoff is handed back before a grant is
spent on it.

This narrows "several members are read whole first" to "read whole
when a database may hold it by its size".  A record whose crc is an
archive's own CRC but whose rom_name is not of the archive's type, or
whose size is not the file's, is no longer matched; each of FBNeo's
596 sets carries its zip's size.  A zip of a folder and one ROM takes
the one-member order.

* database_scan_test: rar archives through every pass

A RAR 4 archive of stored entries, written by hand as the zips are,
takes the passes zip and 7z take: a first member's hit where a core
opens rar names the archive, a later member's elsewhere is held as
archive#member, and the archive is read whole only while a database
whose cores open rar holds a rar record of its size.

* database_scan_test: define the fixture byte writers once

* archive_file: clear the TorrentZip flag for every archive

The flag was cleared only by the zip reader, so a transfer reused for
a 7z, a rar or an archive with no backend kept what the last zip said.
It is now cleared before any backend opens the archive.

* scan: readers read TorrentZip sets whole while the scan asks for that

Readers read every TorrentZip set of several members whole while some
database opens zip, but where no database may hold a set by its size
the scan drops that CRC, so a folder of sets no database holds was
read whole for nothing.  Each TorrentZip set the scan rules on now
says whether its own CRC was asked, and the readers read the next ones
whole only while it was.

---------

Co-authored-by: libretroadmin <[email protected]>
2026-10-30 04:08:09 +01:00

297 lines
11 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)
#define DB_EXTRACT_ROM_NAME (1 << 6)
/* 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);
/* False only when every record whose rom_name ends in ".@ext" carries
* a size and none is @size. True when the index was built without
* those sizes, so a caller reads such an archive as it would without
* an index; also for a type it keeps no sizes for, an unknown @size of
* 0 and a @size of 2^61 or more. */
bool database_info_crc_index_may_hold_archive(
const database_info_crc_index_t *idx, const char *ext, uint64_t size);
/* 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),
/* With CRC: the sizes of the records named for an archive, for
* database_info_crc_index_may_hold_archive(). */
DATABASE_INFO_INDEX_ARCHIVE_SIZES = (1 << 3)
};
/* 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; its archive sizes, then
* the serial index, get what that leaves. An index that cannot be
* built whole comes back NULL, because a partial index would miss
* matches without saying so; sizes that do not fit are dropped, and
* the archives they would have gated are read whole. 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_ */