mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-07 08:36:02 -10:00
Add strlcpy_append, a bound-checked replacement for the unsafe
idiom that's widespread in this codebase:
_len += strlcpy(s + _len, src, len - _len);
That looks like a self-bounding chain but isn't. strlcpy
returns strlen(source) regardless of how much was actually
written, so on truncation _len overshoots len and the next
(len - _len) underflows to a huge size_t, driving the next
strlcpy into an OOB-write or memcpy of attacker-influenced
size. This is the same primitive that drove the shader-
wildcard fix (commit bac6894).
strlcpy_append takes (s, len, *pos, src), checks the
truncation condition correctly, and on overflow:
- leaves s NUL-terminated at len - 1 (strlcpy's contract)
- clamps *pos to len - 1 so subsequent calls in a chain
short-circuit (also returning -1)
- returns -1 to the caller
The clamp is what makes chained use safe with a single check
at the end of the chain.
Apply to database_info_build_query_enum (database_info.c).
That function builds DB query strings of the shape
{'KEY':"PATH"} (or numeric variants without the quotes,
or {'developer':glob('*PATH*')})
via 22 switch cases, each of which was a hand-unrolled chain
of s[++_len] = '...' character writes followed by a path
strlcpy and another short s[++_len] = '...' chain to close
the brace. None of those writes was bounded against len.
A caller passing a buffer smaller than the prefix length
got OOB-writes from the prefix; a caller whose buffer was
just barely enough for prefix + path got the trailing
s[_len]='"';s[++_len]='}' writes off the end (up to 3 bytes
OOB). All callers in tree pass query[4096] from the menu
displaylist, so practical buffers are large -- but the
pattern is wrong and the path component is attacker-
influenced when scanning content metadata.
Side fix in DATABASE_QUERY_ENTRY_BBFC_RATING: pre-rewrite
the case wrote s[++_len] = '"' after the path strlcpy
instead of s[_len] = '"', leaving the strlcpy's NUL
terminator embedded in the query. BBFC rating searches
have been silently returning nothing since this code was
written. Fixed while rewriting.
Adds libretro-common/samples/string/strlcpy_append/
strlcpy_append_test as a regression test, registered in
.github/workflows/Linux-libretro-common-samples.yml's
RUN_TARGETS allowlist (which builds with ASan + UBSan
default since the cdfs/chd commit). Eight cases covering
the contract: basic append, chained append, exact-fit
boundary, wide-margin truncation, chain short-circuiting
after truncation, empty source, NULL/zero-length args, and
*pos already at or past len.
Test follows the existing libretro-common/samples pattern
(heap-allocated destinations sized to exactly the legal
capacity so any reintroduction of the unbounded chain is
flagged by ASan). Verified to fire under ASan when the
helper is replaced with the unsafe naive idiom: heap-
buffer-overflow WRITE of size 2 at offset 26 of an 8-byte
allocation in test_chain_short_circuits_after_truncation.
With the safe helper all 8 cases pass clean.
Verified database_info_build_query_enum produces identical
output for all 21 enum values at len=4096 (the production
caller's buffer size); also verified that at len=8 the
function returns -1 with no OOB write, which is the
behaviour the unsafe naive replacement would have failed at.
1168 lines
37 KiB
C
1168 lines
37 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/>.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdint.h>
|
|
|
|
#include <compat/strl.h>
|
|
#include <retro_endianness.h>
|
|
#include <file/file_path.h>
|
|
#include <lists/string_list.h>
|
|
#include <lists/dir_list.h>
|
|
#include <string/stdstring.h>
|
|
|
|
#include "libretro-db/libretrodb.h"
|
|
|
|
#include "core_info.h"
|
|
#include "database_info.h"
|
|
#include "manual_content_scan.h"
|
|
|
|
int database_info_build_query_enum(char *s, size_t len,
|
|
enum database_query_type type,
|
|
const char *path)
|
|
{
|
|
/* Build queries of the shape {'KEY':"PATH"} (or {'KEY':PATH}
|
|
* for numeric/rating fields, or the DEVELOPER glob form).
|
|
*
|
|
* Pre-this-rewrite each case unrolled the per-character
|
|
* writes with s[++_len]= and then a path strlcpy followed
|
|
* by another short s[++_len]= chain. None of those writes
|
|
* was bounded against len; if the caller's buffer was
|
|
* smaller than the prefix's length the writes ran off the
|
|
* end of s, and after the path strlcpy the trailing
|
|
* s[_len]='"';s[++_len]='}' sequence could OOB-write up to
|
|
* 3 bytes when path filled s. Naively replacing the chain
|
|
* with _len += strlcpy(s+_len, lit, len-_len) does not
|
|
* help: strlcpy returns strlen(source) regardless of
|
|
* truncation, so on overflow _len passes len and the next
|
|
* (len - _len) underflows size_t, producing an
|
|
* unbounded-size strlcpy and the same OOB.
|
|
*
|
|
* Use strlcpy_append (libretro-common/string/stdstring.c),
|
|
* which is bound-checked and clamps *pos to len - 1 on
|
|
* truncation, so subsequent calls in the chain short-
|
|
* circuit safely. The caller checks the final return
|
|
* value to detect any truncation in the chain. */
|
|
size_t _len = 0;
|
|
|
|
if (!s || len == 0)
|
|
return -1;
|
|
|
|
switch (type)
|
|
{
|
|
case DATABASE_QUERY_ENTRY:
|
|
strlcpy_append(s, len, &_len, "{'name':\"");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "\"}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_PUBLISHER:
|
|
strlcpy_append(s, len, &_len, "{'publisher':\"");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "\"}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_DEVELOPER:
|
|
strlcpy_append(s, len, &_len, "{'developer':glob('*");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "*')}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_ORIGIN:
|
|
strlcpy_append(s, len, &_len, "{'origin':\"");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "\"}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_FRANCHISE:
|
|
strlcpy_append(s, len, &_len, "{'franchise':\"");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "\"}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_RATING:
|
|
strlcpy_append(s, len, &_len, "{'esrb_rating':\"");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "\"}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_BBFC_RATING:
|
|
/* Pre-rewrite this case wrote s[++_len] = '"' after
|
|
* the path strlcpy instead of s[_len] = '"', leaving
|
|
* the strlcpy's NUL terminator embedded in the query
|
|
* and silently breaking BBFC searches. Fix while
|
|
* rewriting. */
|
|
strlcpy_append(s, len, &_len, "{'bbfc_rating':\"");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "\"}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_ELSPA_RATING:
|
|
strlcpy_append(s, len, &_len, "{'elspa_rating':\"");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "\"}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_ESRB_RATING:
|
|
strlcpy_append(s, len, &_len, "{'esrb_rating':\"");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "\"}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_PEGI_RATING:
|
|
strlcpy_append(s, len, &_len, "{'pegi_rating':\"");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "\"}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_CERO_RATING:
|
|
strlcpy_append(s, len, &_len, "{'cero_rating':\"");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "\"}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_ENHANCEMENT_HW:
|
|
strlcpy_append(s, len, &_len, "{'enhancement_hw':\"");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "\"}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_EDGE_MAGAZINE_RATING:
|
|
strlcpy_append(s, len, &_len, "{'edge_rating':");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_EDGE_MAGAZINE_ISSUE:
|
|
strlcpy_append(s, len, &_len, "{'edge_issue':");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_FAMITSU_MAGAZINE_RATING:
|
|
strlcpy_append(s, len, &_len, "{'famitsu_rating':");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_RELEASEDATE_MONTH:
|
|
strlcpy_append(s, len, &_len, "{'releasemonth':");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_RELEASEDATE_YEAR:
|
|
strlcpy_append(s, len, &_len, "{'releaseyear':");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_MAX_USERS:
|
|
strlcpy_append(s, len, &_len, "{'users':");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_GENRE:
|
|
strlcpy_append(s, len, &_len, "{'genre':\"");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "\"}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_ENTRY_REGION:
|
|
strlcpy_append(s, len, &_len, "{'region':\"");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "\"}"))
|
|
return -1;
|
|
break;
|
|
case DATABASE_QUERY_NONE:
|
|
strlcpy_append(s, len, &_len, "{'':':\"");
|
|
strlcpy_append(s, len, &_len, path);
|
|
if (strlcpy_append(s, len, &_len, "\"}"))
|
|
return -1;
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* 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)
|
|
{
|
|
size_t i;
|
|
char *ret = (char*)malloc(len * 2 + 1);
|
|
|
|
if (len && !ret)
|
|
return NULL;
|
|
|
|
ret[0] = '\0';
|
|
for (i = 0; i < len; i++)
|
|
snprintf(ret+i * 2, 3, "%02X", data[i]);
|
|
return ret;
|
|
}
|
|
|
|
/* Field selection flags for database_cursor_iterate_filtered.
|
|
* 0 = extract all fields (backward compatible). */
|
|
#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)
|
|
|
|
/* Combination used by the scanner */
|
|
#define DB_EXTRACT_SCAN_FIELDS \
|
|
(DB_EXTRACT_NAME | DB_EXTRACT_CRC | DB_EXTRACT_SERIAL | DB_EXTRACT_SIZE)
|
|
|
|
static int database_cursor_iterate(libretrodb_cursor_t *cur,
|
|
database_info_t *db_info)
|
|
{
|
|
size_t i;
|
|
struct rmsgpack_dom_value item;
|
|
|
|
if (libretrodb_cursor_read_item(cur, &item) != 0)
|
|
return -1;
|
|
|
|
if (item.type != RDT_MAP)
|
|
{
|
|
rmsgpack_dom_value_free(&item);
|
|
return 1;
|
|
}
|
|
|
|
db_info->analog_supported = -1;
|
|
db_info->rumble_supported = -1;
|
|
db_info->coop_supported = -1;
|
|
|
|
for (i = 0; i < item.val.map.len; i++)
|
|
{
|
|
struct rmsgpack_dom_value *key = &item.val.map.items[i].key;
|
|
struct rmsgpack_dom_value *val = &item.val.map.items[i].value;
|
|
const char *str;
|
|
const char *val_string;
|
|
size_t str_len;
|
|
|
|
if (!key || !val)
|
|
continue;
|
|
|
|
if (key->type != RDT_STRING)
|
|
continue;
|
|
|
|
str = key->val.string.buff;
|
|
str_len = strlen(str);
|
|
|
|
val_string = val->val.string.buff;
|
|
|
|
switch (str_len)
|
|
{
|
|
case 3:
|
|
if (memcmp(str, "crc", 3) == 0)
|
|
{
|
|
if (val->type == RDT_BINARY)
|
|
{
|
|
switch (val->val.binary.len)
|
|
{
|
|
case 1:
|
|
db_info->crc32 = *(uint8_t*)val->val.binary.buff;
|
|
break;
|
|
case 2:
|
|
db_info->crc32 = swap_if_little16(
|
|
*(uint16_t*)val->val.binary.buff);
|
|
break;
|
|
case 4:
|
|
db_info->crc32 = swap_if_little32(
|
|
*(uint32_t*)val->val.binary.buff);
|
|
break;
|
|
default:
|
|
db_info->crc32 = 0;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if (memcmp(str, "md5", 3) == 0)
|
|
{
|
|
if (val->type == RDT_BINARY)
|
|
db_info->md5 = bin_to_hex_alloc(
|
|
(uint8_t*)val->val.binary.buff,
|
|
val->val.binary.len);
|
|
}
|
|
break;
|
|
|
|
case 4:
|
|
if (memcmp(str, "name", 4) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->name = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "size", 4) == 0)
|
|
db_info->size = (uint64_t)val->val.uint_;
|
|
else if (memcmp(str, "coop", 4) == 0)
|
|
db_info->coop_supported = (int)val->val.uint_;
|
|
else if (memcmp(str, "sha1", 4) == 0)
|
|
{
|
|
if (val->type == RDT_BINARY)
|
|
db_info->sha1 = bin_to_hex_alloc(
|
|
(uint8_t*)val->val.binary.buff,
|
|
val->val.binary.len);
|
|
}
|
|
break;
|
|
|
|
case 5:
|
|
if (memcmp(str, "genre", 5) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->genre = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "score", 5) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->score = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "media", 5) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->media = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "users", 5) == 0)
|
|
db_info->max_users = (unsigned)val->val.uint_;
|
|
break;
|
|
|
|
case 6:
|
|
if (memcmp(str, "serial", 6) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->serial = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "region", 6) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->region = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "pacing", 6) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->pacing = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "visual", 6) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->visual = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "origin", 6) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->origin = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "rumble", 6) == 0)
|
|
db_info->rumble_supported = (int)val->val.uint_;
|
|
else if (memcmp(str, "analog", 6) == 0)
|
|
db_info->analog_supported = (int)val->val.uint_;
|
|
break;
|
|
|
|
case 7:
|
|
if (memcmp(str, "setting", 7) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->setting = strdup(val_string);
|
|
}
|
|
break;
|
|
|
|
case 8:
|
|
if (memcmp(str, "category", 8) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->category = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "language", 8) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->language = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "controls", 8) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->controls = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "artstyle", 8) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->artstyle = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "gameplay", 8) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->gameplay = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "rom_name", 8) == 0)
|
|
{
|
|
/* rom_name is not used anywhere in codebase,
|
|
* but is frequently added to DB */
|
|
}
|
|
break;
|
|
|
|
case 9:
|
|
if (memcmp(str, "publisher", 9) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->publisher = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "developer", 9) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->developer = string_split(val_string, "|");
|
|
}
|
|
else if (memcmp(str, "narrative", 9) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->narrative = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "vehicular", 9) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->vehicular = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "franchise", 9) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->franchise = strdup(val_string);
|
|
}
|
|
break;
|
|
|
|
case 10:
|
|
if (memcmp(str, "edge_issue", 10) == 0)
|
|
db_info->edge_magazine_issue = (unsigned)val->val.uint_;
|
|
break;
|
|
|
|
case 11:
|
|
if (memcmp(str, "description", 11) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->description = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "perspective", 11) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->perspective = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "bbfc_rating", 11) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->bbfc_rating = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "esrb_rating", 11) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->esrb_rating = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "cero_rating", 11) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->cero_rating = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "pegi_rating", 11) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->pegi_rating = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "edge_rating", 11) == 0)
|
|
db_info->edge_magazine_rating = (unsigned)val->val.uint_;
|
|
else if (memcmp(str, "tgdb_rating", 11) == 0)
|
|
db_info->tgdb_rating = (unsigned)val->val.uint_;
|
|
else if (memcmp(str, "edge_review", 11) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->edge_magazine_review = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "releaseyear", 11) == 0)
|
|
db_info->releaseyear = (unsigned)val->val.uint_;
|
|
break;
|
|
|
|
case 12:
|
|
if (memcmp(str, "elspa_rating", 12) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->elspa_rating = strdup(val_string);
|
|
}
|
|
else if (memcmp(str, "releasemonth", 12) == 0)
|
|
db_info->releasemonth = (unsigned)val->val.uint_;
|
|
else if (memcmp(str, "achievements", 12) == 0)
|
|
db_info->achievements = (int)val->val.uint_;
|
|
break;
|
|
|
|
case 14:
|
|
if (memcmp(str, "famitsu_rating", 14) == 0)
|
|
db_info->famitsu_magazine_rating = (unsigned)val->val.uint_;
|
|
else if (memcmp(str, "enhancement_hw", 14) == 0)
|
|
{
|
|
if (val_string && *val_string)
|
|
db_info->enhancement_hw = strdup(val_string);
|
|
}
|
|
break;
|
|
|
|
case 17:
|
|
if (memcmp(str, "console_exclusive", 17) == 0)
|
|
db_info->console_exclusive = (int)val->val.uint_;
|
|
break;
|
|
|
|
case 18:
|
|
if (memcmp(str, "platform_exclusive", 18) == 0)
|
|
db_info->platform_exclusive = (int)val->val.uint_;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
rmsgpack_dom_value_free(&item);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* database_cursor_iterate_filtered:
|
|
*
|
|
* Like database_cursor_iterate, but only extracts fields specified
|
|
* by the @fields bitmask. Skips strdup/allocation for unrequested
|
|
* fields, eliminating ~30 unnecessary strdup+free per record when
|
|
* only a few fields are needed (e.g. scanner needs crc+name+serial+size).
|
|
*
|
|
* @fields: Bitmask of DB_EXTRACT_* flags. 0 = extract all.
|
|
*/
|
|
static int database_cursor_iterate_filtered(libretrodb_cursor_t *cur,
|
|
database_info_t *db_info, unsigned fields)
|
|
{
|
|
size_t i;
|
|
struct rmsgpack_dom_value item;
|
|
|
|
/* Fall back to full extraction if no mask specified */
|
|
if (fields == 0)
|
|
return database_cursor_iterate(cur, db_info);
|
|
|
|
if (libretrodb_cursor_read_item(cur, &item) != 0)
|
|
return -1;
|
|
|
|
if (item.type != RDT_MAP)
|
|
{
|
|
rmsgpack_dom_value_free(&item);
|
|
return 1;
|
|
}
|
|
|
|
db_info->analog_supported = -1;
|
|
db_info->rumble_supported = -1;
|
|
db_info->coop_supported = -1;
|
|
|
|
for (i = 0; i < item.val.map.len; i++)
|
|
{
|
|
struct rmsgpack_dom_value *key = &item.val.map.items[i].key;
|
|
struct rmsgpack_dom_value *val = &item.val.map.items[i].value;
|
|
const char *str;
|
|
size_t str_len;
|
|
|
|
if (!key || !val || key->type != RDT_STRING)
|
|
continue;
|
|
|
|
str = key->val.string.buff;
|
|
str_len = key->val.string.len;
|
|
|
|
switch (str_len)
|
|
{
|
|
case 3:
|
|
if ((fields & DB_EXTRACT_CRC) && memcmp(str, "crc", 3) == 0)
|
|
{
|
|
if (val->type == RDT_BINARY)
|
|
{
|
|
switch (val->val.binary.len)
|
|
{
|
|
case 1: db_info->crc32 = *(uint8_t*)val->val.binary.buff; break;
|
|
case 2: db_info->crc32 = swap_if_little16(*(uint16_t*)val->val.binary.buff); break;
|
|
case 4: db_info->crc32 = swap_if_little32(*(uint32_t*)val->val.binary.buff); break;
|
|
default: db_info->crc32 = 0; break;
|
|
}
|
|
}
|
|
}
|
|
else if ((fields & DB_EXTRACT_MD5) && memcmp(str, "md5", 3) == 0)
|
|
{
|
|
if (val->type == RDT_BINARY)
|
|
db_info->md5 = bin_to_hex_alloc(
|
|
(uint8_t*)val->val.binary.buff, val->val.binary.len);
|
|
}
|
|
break;
|
|
|
|
case 4:
|
|
if ((fields & DB_EXTRACT_NAME) && memcmp(str, "name", 4) == 0)
|
|
{
|
|
const char *vs = val->val.string.buff;
|
|
if (vs && *vs)
|
|
db_info->name = strdup(vs);
|
|
}
|
|
else if ((fields & DB_EXTRACT_SIZE) && memcmp(str, "size", 4) == 0)
|
|
db_info->size = (uint64_t)val->val.uint_;
|
|
else if ((fields & DB_EXTRACT_SHA1) && memcmp(str, "sha1", 4) == 0)
|
|
{
|
|
if (val->type == RDT_BINARY)
|
|
db_info->sha1 = bin_to_hex_alloc(
|
|
(uint8_t*)val->val.binary.buff, val->val.binary.len);
|
|
}
|
|
break;
|
|
|
|
case 6:
|
|
if ((fields & DB_EXTRACT_SERIAL) && memcmp(str, "serial", 6) == 0)
|
|
{
|
|
const char *vs = val->val.string.buff;
|
|
if (vs && *vs)
|
|
db_info->serial = strdup(vs);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
rmsgpack_dom_value_free(&item);
|
|
return 0;
|
|
}
|
|
|
|
static int database_cursor_open(libretrodb_t *db,
|
|
libretrodb_cursor_t *cur, const char *path, const char *query)
|
|
{
|
|
const char *err = NULL;
|
|
libretrodb_query_t *q = NULL;
|
|
|
|
if ((libretrodb_open(path, db, false)) != 0)
|
|
return -1;
|
|
|
|
if (query)
|
|
q = (libretrodb_query_t*)libretrodb_query_compile(db, query,
|
|
strlen(query), &err);
|
|
|
|
if (err || (libretrodb_cursor_open(db, cur, q)) != 0)
|
|
{
|
|
if (q)
|
|
libretrodb_query_free(q);
|
|
libretrodb_close(db);
|
|
return -1;
|
|
}
|
|
|
|
if (q)
|
|
libretrodb_query_free(q);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Types 'cue' and 'gdi' are prioritized */
|
|
static bool type_is_prioritized(const char *path)
|
|
{
|
|
const char *ext = path_get_extension(path);
|
|
if (ext)
|
|
{
|
|
char e0 = ext[0] | 0x20;
|
|
char e1 = ext[1] | 0x20;
|
|
char e2 = ext[2] | 0x20;
|
|
if (ext[3] == '\0')
|
|
return (e0 == 'c' && e1 == 'u' && e2 == 'e')
|
|
|| (e0 == 'g' && e1 == 'd' && e2 == 'i');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static int dir_entry_compare(const void *left, const void *right)
|
|
{
|
|
const struct string_list_elem *le = (const struct string_list_elem*)left;
|
|
const struct string_list_elem *re = (const struct string_list_elem*)right;
|
|
bool l = type_is_prioritized(le->data);
|
|
bool r = type_is_prioritized(re->data);
|
|
|
|
return (int) r - (int) l;
|
|
}
|
|
|
|
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)
|
|
{
|
|
core_info_list_t *core_info_list = NULL;
|
|
struct string_list *list = NULL;
|
|
database_info_handle_t *db = (database_info_handle_t*)
|
|
malloc(sizeof(*db));
|
|
|
|
if (!db)
|
|
return NULL;
|
|
|
|
/* File list will include all supported files,
|
|
* unless extension list is given */
|
|
if (!file_exts || !*file_exts)
|
|
core_info_get_list(&core_info_list);
|
|
|
|
if (!(list = dir_list_new(dir, core_info_list ? core_info_list->all_ext : file_exts,
|
|
false, show_hidden_files,
|
|
include_archive, recursive)))
|
|
{
|
|
free(db);
|
|
return NULL;
|
|
}
|
|
|
|
/* dir list prioritize */
|
|
qsort(list->elems, list->size, sizeof(*list->elems), dir_entry_compare);
|
|
|
|
db->status = DATABASE_STATUS_ITERATE;
|
|
db->type = type;
|
|
*content_list = list;
|
|
|
|
return db;
|
|
}
|
|
|
|
database_info_handle_t *database_info_file_init(const char *path,
|
|
enum database_type type, retro_task_t *task, struct string_list **content_list)
|
|
{
|
|
union string_list_elem_attr attr;
|
|
struct string_list *list = NULL;
|
|
database_info_handle_t *db = (database_info_handle_t*)
|
|
malloc(sizeof(*db));
|
|
|
|
if (!db)
|
|
return NULL;
|
|
|
|
if (!(list = string_list_new()))
|
|
{
|
|
free(db);
|
|
return NULL;
|
|
}
|
|
|
|
attr.i = 0;
|
|
string_list_append(list, path, attr);
|
|
|
|
db->status = DATABASE_STATUS_ITERATE;
|
|
db->type = type;
|
|
*content_list = list;
|
|
|
|
return db;
|
|
}
|
|
|
|
void database_info_free(database_info_handle_t *db)
|
|
{
|
|
/* if (db)
|
|
string_list_free(db->list);*/
|
|
}
|
|
|
|
/**
|
|
* database_info_list_new_names_only:
|
|
*
|
|
* Fast path for loading just game names from an .rdb file.
|
|
* Used by the Database Manager browse list which only needs names.
|
|
* Reads each record's map header, scans for the "name" key using
|
|
* field-level skip, extracts just the name string, skips everything
|
|
* else. ~10x less work per record than the full extraction path.
|
|
*/
|
|
static database_info_list_t *database_info_list_new_names_only(
|
|
const char *rdb_path)
|
|
{
|
|
libretrodb_t *db = libretrodb_new();
|
|
libretrodb_cursor_t *cur = libretrodb_cursor_new();
|
|
database_info_list_t *list = NULL;
|
|
database_info_t *items = NULL;
|
|
size_t count = 0;
|
|
size_t capacity = 0;
|
|
|
|
if (!db || !cur)
|
|
goto end;
|
|
|
|
if (database_cursor_open(db, cur, rdb_path, NULL) != 0)
|
|
goto end;
|
|
|
|
list = (database_info_list_t*)malloc(sizeof(*list));
|
|
if (!list)
|
|
goto end;
|
|
|
|
list->count = 0;
|
|
list->list = NULL;
|
|
|
|
/* Initial capacity — avoids realloc churn for small databases
|
|
* and reduces it for large ones */
|
|
capacity = 256;
|
|
items = (database_info_t*)calloc(capacity, sizeof(*items));
|
|
if (!items)
|
|
{
|
|
free(list);
|
|
list = NULL;
|
|
goto end;
|
|
}
|
|
|
|
for (;;)
|
|
{
|
|
struct rmsgpack_dom_value item;
|
|
|
|
if (libretrodb_cursor_read_item(cur, &item) != 0)
|
|
break;
|
|
|
|
if (item.type == RDT_MAP)
|
|
{
|
|
unsigned i;
|
|
char *found_name = NULL;
|
|
|
|
/* Scan the DOM for the "name" field only */
|
|
for (i = 0; i < item.val.map.len; i++)
|
|
{
|
|
struct rmsgpack_dom_value *k = &item.val.map.items[i].key;
|
|
struct rmsgpack_dom_value *v = &item.val.map.items[i].value;
|
|
|
|
if ( k->type == RDT_STRING
|
|
&& k->val.string.len == 4
|
|
&& memcmp(k->val.string.buff, "name", 4) == 0
|
|
&& v->type == RDT_STRING
|
|
&& v->val.string.buff
|
|
&& *v->val.string.buff)
|
|
{
|
|
found_name = strdup(v->val.string.buff);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (found_name)
|
|
{
|
|
/* Grow array geometrically */
|
|
if (count >= capacity)
|
|
{
|
|
database_info_t *new_items;
|
|
capacity *= 2;
|
|
new_items = (database_info_t*)realloc(
|
|
items, capacity * sizeof(*items));
|
|
if (!new_items)
|
|
{
|
|
free(found_name);
|
|
rmsgpack_dom_value_free(&item);
|
|
break;
|
|
}
|
|
items = new_items;
|
|
/* Zero the new portion so free() on unset
|
|
* fields is safe */
|
|
memset(&items[count], 0,
|
|
(capacity - count) * sizeof(*items));
|
|
}
|
|
|
|
memset(&items[count], 0, sizeof(items[count]));
|
|
items[count].name = found_name;
|
|
count++;
|
|
}
|
|
}
|
|
|
|
rmsgpack_dom_value_free(&item);
|
|
}
|
|
|
|
list->list = items;
|
|
list->count = count;
|
|
|
|
end:
|
|
if (db)
|
|
{
|
|
libretrodb_cursor_close(cur);
|
|
libretrodb_close(db);
|
|
libretrodb_free(db);
|
|
}
|
|
if (cur)
|
|
libretrodb_cursor_free(cur);
|
|
|
|
return list;
|
|
}
|
|
|
|
database_info_list_t *database_info_list_new(
|
|
const char *rdb_path, const char *query)
|
|
{
|
|
return database_info_list_new_filtered(rdb_path, query, 0);
|
|
}
|
|
|
|
database_info_list_t *database_info_list_new_filtered(
|
|
const char *rdb_path, const char *query, unsigned fields)
|
|
{
|
|
int ret = 0;
|
|
unsigned k = 0;
|
|
unsigned capacity = 0;
|
|
database_info_t *database_info = NULL;
|
|
database_info_list_t *database_info_list = NULL;
|
|
libretrodb_t *db = libretrodb_new();
|
|
libretrodb_cursor_t *cur = libretrodb_cursor_new();
|
|
|
|
if (!db || !cur)
|
|
goto end;
|
|
|
|
/* Fast path: name-only extraction for Database Manager browse
|
|
* (NULL query = unfiltered scan, only name is used by caller) */
|
|
if (!query)
|
|
{
|
|
/* Free the db/cur we just allocated — the fast path
|
|
* creates its own */
|
|
libretrodb_free(db);
|
|
libretrodb_cursor_free(cur);
|
|
return database_info_list_new_names_only(rdb_path);
|
|
}
|
|
|
|
if ((database_cursor_open(db, cur, rdb_path, query) != 0))
|
|
goto end;
|
|
|
|
database_info_list = (database_info_list_t*)
|
|
malloc(sizeof(*database_info_list));
|
|
|
|
if (!database_info_list)
|
|
goto end;
|
|
|
|
database_info_list->count = 0;
|
|
database_info_list->list = NULL;
|
|
|
|
/* Pre-allocate with geometric growth instead of realloc-by-one */
|
|
capacity = 64;
|
|
database_info = (database_info_t*)calloc(capacity, sizeof(*database_info));
|
|
if (!database_info)
|
|
{
|
|
free(database_info_list);
|
|
database_info_list = NULL;
|
|
goto end;
|
|
}
|
|
|
|
while (ret != -1)
|
|
{
|
|
database_info_t db_info = {0};
|
|
ret = database_cursor_iterate_filtered(cur, &db_info, fields);
|
|
|
|
if (ret == 0)
|
|
{
|
|
/* Grow geometrically when full */
|
|
if (k >= capacity)
|
|
{
|
|
database_info_t *new_ptr;
|
|
capacity *= 2;
|
|
new_ptr = (database_info_t*)realloc(
|
|
database_info, capacity * sizeof(*database_info));
|
|
|
|
if (!new_ptr)
|
|
{
|
|
if (db_info.bbfc_rating)
|
|
free(db_info.bbfc_rating);
|
|
if (db_info.cero_rating)
|
|
free(db_info.cero_rating);
|
|
if (db_info.description)
|
|
free(db_info.description);
|
|
if (db_info.edge_magazine_review)
|
|
free(db_info.edge_magazine_review);
|
|
if (db_info.elspa_rating)
|
|
free(db_info.elspa_rating);
|
|
if (db_info.enhancement_hw)
|
|
free(db_info.enhancement_hw);
|
|
if (db_info.esrb_rating)
|
|
free(db_info.esrb_rating);
|
|
if (db_info.franchise)
|
|
free(db_info.franchise);
|
|
if (db_info.genre)
|
|
free(db_info.genre);
|
|
if (db_info.category)
|
|
free(db_info.category);
|
|
if (db_info.language)
|
|
free(db_info.language);
|
|
if (db_info.region)
|
|
free(db_info.region);
|
|
if (db_info.score)
|
|
free(db_info.score);
|
|
if (db_info.media)
|
|
free(db_info.media);
|
|
if (db_info.controls)
|
|
free(db_info.controls);
|
|
if (db_info.artstyle)
|
|
free(db_info.artstyle);
|
|
if (db_info.gameplay)
|
|
free(db_info.gameplay);
|
|
if (db_info.narrative)
|
|
free(db_info.narrative);
|
|
if (db_info.pacing)
|
|
free(db_info.pacing);
|
|
if (db_info.perspective)
|
|
free(db_info.perspective);
|
|
if (db_info.setting)
|
|
free(db_info.setting);
|
|
if (db_info.visual)
|
|
free(db_info.visual);
|
|
if (db_info.vehicular)
|
|
free(db_info.vehicular);
|
|
if (db_info.name)
|
|
free(db_info.name);
|
|
if (db_info.origin)
|
|
free(db_info.origin);
|
|
if (db_info.pegi_rating)
|
|
free(db_info.pegi_rating);
|
|
if (db_info.publisher)
|
|
free(db_info.publisher);
|
|
if (db_info.rom_name)
|
|
free(db_info.rom_name);
|
|
if (db_info.serial)
|
|
free(db_info.serial);
|
|
if (db_info.md5)
|
|
free(db_info.md5);
|
|
if (db_info.sha1)
|
|
free(db_info.sha1);
|
|
|
|
db_info.name = NULL;
|
|
db_info.rom_name = NULL;
|
|
db_info.serial = NULL;
|
|
db_info.genre = NULL;
|
|
db_info.description = NULL;
|
|
db_info.publisher = NULL;
|
|
db_info.developer = NULL;
|
|
db_info.origin = NULL;
|
|
db_info.franchise = NULL;
|
|
db_info.edge_magazine_review = NULL;
|
|
db_info.cero_rating = NULL;
|
|
db_info.pegi_rating = NULL;
|
|
db_info.enhancement_hw = NULL;
|
|
db_info.elspa_rating = NULL;
|
|
db_info.esrb_rating = NULL;
|
|
db_info.bbfc_rating = NULL;
|
|
db_info.sha1 = NULL;
|
|
db_info.md5 = NULL;
|
|
|
|
database_info_list_free(database_info_list);
|
|
free(database_info);
|
|
free(database_info_list);
|
|
database_info_list = NULL;
|
|
goto end;
|
|
}
|
|
|
|
database_info = new_ptr;
|
|
}
|
|
|
|
memcpy(&database_info[k], &db_info, sizeof(db_info));
|
|
|
|
k++;
|
|
}
|
|
}
|
|
|
|
database_info_list->list = database_info;
|
|
database_info_list->count = k;
|
|
|
|
end:
|
|
if (db)
|
|
{
|
|
libretrodb_cursor_close(cur);
|
|
libretrodb_close(db);
|
|
libretrodb_free(db);
|
|
}
|
|
if (cur)
|
|
libretrodb_cursor_free(cur);
|
|
|
|
return database_info_list;
|
|
}
|
|
|
|
void database_info_list_free(database_info_list_t *database_info_list)
|
|
{
|
|
size_t i;
|
|
|
|
if (!database_info_list)
|
|
return;
|
|
|
|
for (i = 0; i < database_info_list->count; i++)
|
|
{
|
|
database_info_t *info = &database_info_list->list[i];
|
|
|
|
if (info->name)
|
|
free(info->name);
|
|
if (info->rom_name)
|
|
free(info->rom_name);
|
|
if (info->serial)
|
|
free(info->serial);
|
|
if (info->genre)
|
|
free(info->genre);
|
|
if (info->category)
|
|
free(info->category);
|
|
if (info->language)
|
|
free(info->language);
|
|
if (info->region)
|
|
free(info->region);
|
|
if (info->score)
|
|
free(info->score);
|
|
if (info->media)
|
|
free(info->media);
|
|
if (info->controls)
|
|
free(info->controls);
|
|
if (info->artstyle)
|
|
free(info->artstyle);
|
|
if (info->gameplay)
|
|
free(info->gameplay);
|
|
if (info->narrative)
|
|
free(info->narrative);
|
|
if (info->pacing)
|
|
free(info->pacing);
|
|
if (info->perspective)
|
|
free(info->perspective);
|
|
if (info->setting)
|
|
free(info->setting);
|
|
if (info->visual)
|
|
free(info->visual);
|
|
if (info->vehicular)
|
|
free(info->vehicular);
|
|
if (info->description)
|
|
free(info->description);
|
|
if (info->publisher)
|
|
free(info->publisher);
|
|
if (info->developer)
|
|
string_list_free(info->developer);
|
|
if (info->origin)
|
|
free(info->origin);
|
|
if (info->franchise)
|
|
free(info->franchise);
|
|
if (info->edge_magazine_review)
|
|
free(info->edge_magazine_review);
|
|
|
|
if (info->cero_rating)
|
|
free(info->cero_rating);
|
|
if (info->pegi_rating)
|
|
free(info->pegi_rating);
|
|
if (info->enhancement_hw)
|
|
free(info->enhancement_hw);
|
|
if (info->elspa_rating)
|
|
free(info->elspa_rating);
|
|
if (info->esrb_rating)
|
|
free(info->esrb_rating);
|
|
if (info->bbfc_rating)
|
|
free(info->bbfc_rating);
|
|
if (info->sha1)
|
|
free(info->sha1);
|
|
if (info->md5)
|
|
free(info->md5);
|
|
|
|
info->name = NULL;
|
|
info->rom_name = NULL;
|
|
info->serial = NULL;
|
|
info->genre = NULL;
|
|
info->description = NULL;
|
|
info->publisher = NULL;
|
|
info->developer = NULL;
|
|
info->origin = NULL;
|
|
info->franchise = NULL;
|
|
info->edge_magazine_review = NULL;
|
|
info->cero_rating = NULL;
|
|
info->pegi_rating = NULL;
|
|
info->enhancement_hw = NULL;
|
|
info->elspa_rating = NULL;
|
|
info->esrb_rating = NULL;
|
|
info->bbfc_rating = NULL;
|
|
info->sha1 = NULL;
|
|
info->md5 = NULL;
|
|
}
|
|
|
|
free(database_info_list->list);
|
|
}
|