mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-08 08:10:30 -10:00
config_file: audit and rework — zero-copy pooled parsing (−63% load, −48% write, −99.8% heap ops), injectable file I/O, streaming API, autoconfig profile index (437 opens → 2), 5 pre-existing bugs fixed, fuzzing + regression tests in CI (#19305)
This commit is contained in:
1 parent
6898587979
commit
1d1f27a025
32 files changed
+2932
-553
No files matched your search
@@ -0,0 +1,52 @@
|
||||
name: CI Linux autoconfig regression
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true
|
||||
|
||||
jobs:
|
||||
autoconf-tests:
|
||||
name: Build and run the autoconfig regression suite
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y build-essential zlib1g-dev libfreetype6-dev
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Configure (headless, test drivers)
|
||||
run: |
|
||||
set -eu
|
||||
# The suite drives the test input/joypad drivers with the
|
||||
# null video/audio/menu drivers - no X11/Vulkan/menu needed.
|
||||
# HAVE_TEST_DRIVERS is on by default; assert it survived
|
||||
# configure so a silent drop turns into a clear failure
|
||||
# rather than a skipped test.
|
||||
./configure --disable-menu --disable-ffmpeg --disable-x11 --disable-wayland
|
||||
grep -q '^HAVE_TEST_DRIVERS = 1' config.mk
|
||||
|
||||
- name: Build
|
||||
run: make -j"$(nproc)"
|
||||
|
||||
- name: Run autoconfig regression suite
|
||||
shell: bash
|
||||
run: |
|
||||
set -eu
|
||||
test -x ./retroarch
|
||||
./tests-other/test_autoconf.sh ./retroarch
|
||||
@@ -89,12 +89,11 @@ jobs:
|
||||
)
|
||||
|
||||
# Per-binary run command (overrides ./<binary> if present).
|
||||
# config_file_test has pre-existing sample leaks unrelated to
|
||||
# any regression so we disable the ASan leak detector -- if a
|
||||
# real leak shows up elsewhere it will be flagged during build
|
||||
# of ASan-enabled configurations, not here.
|
||||
# config_file_test's leak exemption is gone: the harness now
|
||||
# frees everything it allocates and the parser's own include
|
||||
# leak is fixed, so LeakSanitizer runs at full strictness and
|
||||
# any surviving allocation is a real config_file regression.
|
||||
declare -A RUN_ENV=(
|
||||
[config_file_test]="ASAN_OPTIONS=detect_leaks=0"
|
||||
)
|
||||
|
||||
# Samples that are build-only (either they require command-line
|
||||
@@ -407,6 +406,62 @@ jobs:
|
||||
# Win11/ARM64 atomic ordering bug, found only on real silicon),
|
||||
# but qemu catches most categorical errors and is much cheaper than
|
||||
# provisioning ARM runners.
|
||||
config-file-fuzz-smoke:
|
||||
name: config_file differential fuzz (smoke)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y clang
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build fuzz target
|
||||
shell: bash
|
||||
working-directory: libretro-common/samples/file/config_file
|
||||
run: |
|
||||
set -eu
|
||||
LC=../../..
|
||||
clang -O1 -g -fsanitize=fuzzer,address,undefined \
|
||||
-fno-sanitize-recover=all \
|
||||
-I$LC/include -o config_file_fuzz config_file_fuzz.c \
|
||||
$LC/file/config_file.c \
|
||||
$LC/file/file_path.c \
|
||||
$LC/compat/compat_strl.c \
|
||||
$LC/compat/compat_strcasestr.c \
|
||||
$LC/compat/compat_posix_string.c \
|
||||
$LC/encodings/encoding_utf.c \
|
||||
$LC/lists/string_list.c \
|
||||
$LC/time/rtime.c
|
||||
|
||||
- name: Run fuzz smoke
|
||||
shell: bash
|
||||
working-directory: libretro-common/samples/file/config_file
|
||||
run: |
|
||||
set -eu
|
||||
# Seed corpus: representative config shapes plus the byte
|
||||
# patterns the target's invariants care about (embedded NUL,
|
||||
# comment-in-literal, directives, duplicate keys, CRLF).
|
||||
# Generated here rather than committed so no binary files
|
||||
# live in the tree.
|
||||
mkdir -p corpus
|
||||
printf 'video_driver = "vulkan"\nvideo_fullscreen = "true"\naudio_rate = "48000"\n' > corpus/basic
|
||||
printf '# comment\nkey = "has # inside"\ndup = "a"\ndup = "b"\n spaced = bare\r\nempty = ""\n' > corpus/edge
|
||||
printf '#include "sub.cfg"\n#reference "other.cfg"\nafter = "1"\n' > corpus/directives
|
||||
python3 -c "import sys; sys.stdout.buffer.write(b'a = \"1\"\nb\x00c = \"2\"\nd = \"3\"\n')" > corpus/embedded_nul
|
||||
# 60 seconds of coverage-guided differential fuzzing:
|
||||
# every input is parsed whole and again through the
|
||||
# streaming push API in input-derived packet splits, then
|
||||
# entry-list equality, hash-map coherence and tail
|
||||
# validity are enforced (see config_file_fuzz.c). Any
|
||||
# divergence or sanitizer report fails the job.
|
||||
./config_file_fuzz -max_len=512 -max_total_time=60 \
|
||||
-print_final_stats=1 corpus/
|
||||
|
||||
retro-atomic-cross:
|
||||
name: Cross-arch retro_atomic_test (${{ matrix.arch }})
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -359,6 +359,7 @@ OBJ += \
|
||||
core_backup.o \
|
||||
core_option_manager.o \
|
||||
$(LIBRETRO_COMM_DIR)/file/config_file.o \
|
||||
$(LIBRETRO_COMM_DIR)/file/config_file_io.o \
|
||||
$(LIBRETRO_COMM_DIR)/file/config_file_userdata.o \
|
||||
runtime_file.o \
|
||||
disk_index_file.o
|
||||
|
||||
@@ -38,6 +38,7 @@ OBJ := ctr/ctr_system.o \
|
||||
libretro-common/compat/compat_strl.o \
|
||||
libretro-common/compat/compat_strldup.o \
|
||||
libretro-common/file/config_file.o \
|
||||
libretro-common/file/config_file_io.o \
|
||||
libretro-common/streams/file_stream.o \
|
||||
libretro-common/vfs/vfs_implementation.o \
|
||||
libretro-common/hash/lrc_hash.o \
|
||||
|
||||
@@ -88,6 +88,7 @@ OBJ := frontend/frontend_salamander.o \
|
||||
libretro-common/compat/compat_strldup.o \
|
||||
libretro-common/compat/compat_strcasestr.o \
|
||||
libretro-common/file/config_file.o \
|
||||
libretro-common/file/config_file_io.o \
|
||||
libretro-common/streams/file_stream.o \
|
||||
libretro-common/vfs/vfs_implementation.o \
|
||||
libretro-common/hash/lrc_hash.o \
|
||||
|
||||
@@ -86,6 +86,7 @@ OBJS = frontend/frontend_salamander.o \
|
||||
libretro-common/compat/compat_strcasestr.o \
|
||||
libretro-common/compat/fopen_utf8.o \
|
||||
libretro-common/file/config_file.o \
|
||||
libretro-common/file/config_file_io.o \
|
||||
libretro-common/streams/file_stream.o \
|
||||
libretro-common/vfs/vfs_implementation.o \
|
||||
libretro-common/hash/lrc_hash.o \
|
||||
|
||||
@@ -56,6 +56,7 @@ EE_OBJS = frontend/frontend_salamander.o \
|
||||
libretro-common/compat/compat_strldup.o \
|
||||
libretro-common/compat/compat_strcasestr.o \
|
||||
libretro-common/file/config_file.o \
|
||||
libretro-common/file/config_file_io.o \
|
||||
libretro-common/streams/file_stream.o \
|
||||
libretro-common/vfs/vfs_implementation.o \
|
||||
libretro-common/hash/lrc_hash.o \
|
||||
|
||||
@@ -72,6 +72,7 @@ OBJ = frontend/frontend_salamander.o \
|
||||
libretro-common/streams/file_stream.o \
|
||||
libretro-common/vfs/vfs_implementation.o \
|
||||
libretro-common/file/config_file.o \
|
||||
libretro-common/file/config_file_io.o \
|
||||
libretro-common/time/rtime.o \
|
||||
verbosity.o
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ OBJS = frontend/frontend_salamander.o \
|
||||
libretro-common/compat/compat_strldup.o \
|
||||
libretro-common/compat/compat_strcasestr.o \
|
||||
libretro-common/file/config_file.o \
|
||||
libretro-common/file/config_file_io.o \
|
||||
libretro-common/streams/file_stream.o \
|
||||
libretro-common/vfs/vfs_implementation.o \
|
||||
libretro-common/hash/lrc_hash.o \
|
||||
|
||||
@@ -47,6 +47,7 @@ OBJS = frontend/frontend_salamander.o \
|
||||
libretro-common/compat/compat_strcasestr.o \
|
||||
libretro-common/compat/fopen_utf8.o \
|
||||
libretro-common/file/config_file.o \
|
||||
libretro-common/file/config_file_io.o \
|
||||
libretro-common/streams/file_stream.o \
|
||||
libretro-common/vfs/vfs_implementation.o \
|
||||
libretro-common/hash/lrc_hash.o \
|
||||
|
||||
@@ -82,6 +82,7 @@ OBJ = frontend/frontend_salamander.o \
|
||||
libretro-common/compat/compat_strcasestr.o \
|
||||
libretro-common/compat/fopen_utf8.o \
|
||||
libretro-common/file/config_file.o \
|
||||
libretro-common/file/config_file_io.o \
|
||||
libretro-common/time/rtime.o \
|
||||
verbosity.o \
|
||||
$(APP_BOOTER_DIR)/app_booter.binobj
|
||||
|
||||
@@ -68,6 +68,7 @@ ifeq ($(SALAMANDER_BUILD),1)
|
||||
OBJ += libretro-common/compat/compat_strl.o
|
||||
OBJ += libretro-common/compat/compat_strldup.o
|
||||
OBJ += libretro-common/file/config_file.o
|
||||
OBJ += libretro-common/file/config_file_io.o
|
||||
OBJ += libretro-common/streams/file_stream.o
|
||||
OBJ += libretro-common/vfs/vfs_implementation.o
|
||||
OBJ += libretro-common/hash/lrc_hash.o
|
||||
|
||||
+3
-4
@@ -26,7 +26,6 @@
|
||||
#include <compat/posix_string.h>
|
||||
#include <string/stdstring.h>
|
||||
#include <streams/file_stream.h>
|
||||
#include <array/rhmap.h>
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
@@ -7017,7 +7016,7 @@ static bool config_load_file(global_t *global,
|
||||
frontend_driver_set_sustained_performance_mode(settings->bools.sustained_performance_mode);
|
||||
recording_driver_update_streaming_url();
|
||||
|
||||
if (!(bool)RHMAP_HAS_STR(conf->entries_map, "user_language"))
|
||||
if (!config_get_entry(conf, "user_language"))
|
||||
msg_hash_set_uint(MSG_HASH_USER_LANGUAGE, frontend_driver_get_user_language());
|
||||
|
||||
/* If GameMode is enabled in the config but libgamemode is not
|
||||
@@ -7037,8 +7036,8 @@ static bool config_load_file(global_t *global,
|
||||
* history playlist size limit. (Have to do this, otherwise
|
||||
* users with large custom history size limits may lose
|
||||
* favourites entries when updating RetroArch...) */
|
||||
if ( (bool)RHMAP_HAS_STR(conf->entries_map, "content_history_size")
|
||||
&& !(bool)RHMAP_HAS_STR(conf->entries_map, "content_favorites_size"))
|
||||
if ( config_get_entry(conf, "content_history_size")
|
||||
&& !config_get_entry(conf, "content_favorites_size"))
|
||||
{
|
||||
if (settings->uints.content_history_size > 999)
|
||||
settings->ints.content_favorites_size = -1;
|
||||
|
||||
+36
-124
@@ -1651,23 +1651,11 @@ static void core_info_resolve_firmware(
|
||||
|
||||
strlcpy(key + _len2, "path", sizeof(key) - _len2);
|
||||
|
||||
entry = config_get_entry(conf, key);
|
||||
|
||||
if (entry && (entry->value && *entry->value))
|
||||
{
|
||||
firmware[i].path = entry->value;
|
||||
entry->value = NULL;
|
||||
}
|
||||
firmware[i].path = config_take_string(conf, key);
|
||||
|
||||
strlcpy(key + _len2, "desc", sizeof(key) - _len2);
|
||||
|
||||
entry = config_get_entry(conf, key);
|
||||
|
||||
if (entry && (entry->value && *entry->value))
|
||||
{
|
||||
firmware[i].desc = entry->value;
|
||||
entry->value = NULL;
|
||||
}
|
||||
firmware[i].desc = config_take_string(conf, key);
|
||||
}
|
||||
|
||||
info->firmware_count = firmware_count;
|
||||
@@ -1692,149 +1680,93 @@ static void core_info_parse_config_file(
|
||||
config_file_t *conf)
|
||||
{
|
||||
bool tmp_bool = false;
|
||||
struct config_entry_list *entry = config_get_entry(conf, "display_name");
|
||||
struct config_entry_list *entry = NULL;
|
||||
|
||||
if (entry && (entry->value && *entry->value))
|
||||
info->display_name = config_take_string(conf, "display_name");
|
||||
|
||||
info->display_version = config_take_string(conf, "display_version");
|
||||
|
||||
info->core_name = config_take_string(conf, "corename");
|
||||
|
||||
info->systemname = config_take_string(conf, "systemname");
|
||||
|
||||
info->system_id = config_take_string(conf, "systemid");
|
||||
|
||||
info->system_manufacturer = config_take_string(conf, "manufacturer");
|
||||
|
||||
info->supported_extensions = config_take_string(conf, "supported_extensions");
|
||||
|
||||
if (info->supported_extensions)
|
||||
{
|
||||
info->display_name = entry->value;
|
||||
entry->value = NULL;
|
||||
}
|
||||
|
||||
entry = config_get_entry(conf, "display_version");
|
||||
|
||||
if (entry && (entry->value && *entry->value))
|
||||
{
|
||||
info->display_version = entry->value;
|
||||
entry->value = NULL;
|
||||
}
|
||||
|
||||
entry = config_get_entry(conf, "corename");
|
||||
|
||||
if (entry && (entry->value && *entry->value))
|
||||
{
|
||||
info->core_name = entry->value;
|
||||
entry->value = NULL;
|
||||
}
|
||||
|
||||
entry = config_get_entry(conf, "systemname");
|
||||
|
||||
if (entry && (entry->value && *entry->value))
|
||||
{
|
||||
info->systemname = entry->value;
|
||||
entry->value = NULL;
|
||||
}
|
||||
|
||||
entry = config_get_entry(conf, "systemid");
|
||||
|
||||
if (entry && (entry->value && *entry->value))
|
||||
{
|
||||
info->system_id = entry->value;
|
||||
entry->value = NULL;
|
||||
}
|
||||
|
||||
entry = config_get_entry(conf, "manufacturer");
|
||||
|
||||
if (entry && (entry->value && *entry->value))
|
||||
{
|
||||
info->system_manufacturer = entry->value;
|
||||
entry->value = NULL;
|
||||
}
|
||||
|
||||
entry = config_get_entry(conf, "supported_extensions");
|
||||
|
||||
if (entry && (entry->value && *entry->value))
|
||||
{
|
||||
info->supported_extensions = entry->value;
|
||||
entry->value = NULL;
|
||||
|
||||
info->supported_extensions_list =
|
||||
string_split(info->supported_extensions, "|");
|
||||
}
|
||||
|
||||
entry = config_get_entry(conf, "authors");
|
||||
info->authors = config_take_string(conf, "authors");
|
||||
|
||||
if (entry && (entry->value && *entry->value))
|
||||
if (info->authors)
|
||||
{
|
||||
info->authors = entry->value;
|
||||
entry->value = NULL;
|
||||
|
||||
info->authors_list =
|
||||
string_split(info->authors, "|");
|
||||
}
|
||||
|
||||
entry = config_get_entry(conf, "permissions");
|
||||
info->permissions = config_take_string(conf, "permissions");
|
||||
|
||||
if (entry && (entry->value && *entry->value))
|
||||
if (info->permissions)
|
||||
{
|
||||
info->permissions = entry->value;
|
||||
entry->value = NULL;
|
||||
|
||||
info->permissions_list =
|
||||
string_split(info->permissions, "|");
|
||||
}
|
||||
|
||||
entry = config_get_entry(conf, "license");
|
||||
info->licenses = config_take_string(conf, "license");
|
||||
|
||||
if (entry && (entry->value && *entry->value))
|
||||
if (info->licenses)
|
||||
{
|
||||
info->licenses = entry->value;
|
||||
entry->value = NULL;
|
||||
|
||||
info->licenses_list =
|
||||
string_split(info->licenses, "|");
|
||||
}
|
||||
|
||||
entry = config_get_entry(conf, "categories");
|
||||
info->categories = config_take_string(conf, "categories");
|
||||
|
||||
if (entry && (entry->value && *entry->value))
|
||||
if (info->categories)
|
||||
{
|
||||
info->categories = entry->value;
|
||||
entry->value = NULL;
|
||||
|
||||
info->categories_list =
|
||||
string_split(info->categories, "|");
|
||||
}
|
||||
|
||||
entry = config_get_entry(conf, "database");
|
||||
info->databases = config_take_string(conf, "database");
|
||||
|
||||
if (entry && (entry->value && *entry->value))
|
||||
if (info->databases)
|
||||
{
|
||||
info->databases = entry->value;
|
||||
entry->value = NULL;
|
||||
|
||||
info->databases_list =
|
||||
string_split(info->databases, "|");
|
||||
}
|
||||
|
||||
entry = config_get_entry(conf, "notes");
|
||||
info->notes = config_take_string(conf, "notes");
|
||||
|
||||
if (entry && (entry->value && *entry->value))
|
||||
if (info->notes)
|
||||
{
|
||||
info->notes = entry->value;
|
||||
entry->value = NULL;
|
||||
|
||||
info->note_list =
|
||||
string_split(info->notes, "|");
|
||||
}
|
||||
|
||||
entry = config_get_entry(conf, "required_hw_api");
|
||||
info->required_hw_api = config_take_string(conf, "required_hw_api");
|
||||
|
||||
if (entry && (entry->value && *entry->value))
|
||||
if (info->required_hw_api)
|
||||
{
|
||||
info->required_hw_api = entry->value;
|
||||
entry->value = NULL;
|
||||
|
||||
info->required_hw_api_list =
|
||||
string_split(info->required_hw_api, "|");
|
||||
}
|
||||
|
||||
entry = config_get_entry(conf, "description");
|
||||
|
||||
if (entry && (entry->value && *entry->value))
|
||||
{
|
||||
info->description = entry->value;
|
||||
entry->value = NULL;
|
||||
}
|
||||
info->description = config_take_string(conf, "description");
|
||||
|
||||
if (config_get_bool(conf, "supports_no_game",
|
||||
&tmp_bool))
|
||||
@@ -2829,8 +2761,6 @@ size_t core_info_list_get_display_name(
|
||||
core_updater_info_t *core_info_get_core_updater_info(
|
||||
const char *info_path)
|
||||
{
|
||||
struct config_entry_list
|
||||
*entry = NULL;
|
||||
bool tmp_bool = false;
|
||||
core_updater_info_t *info = NULL;
|
||||
config_file_t *conf = NULL;
|
||||
@@ -2858,31 +2788,13 @@ core_updater_info_t *core_info_get_core_updater_info(
|
||||
info->is_experimental = tmp_bool;
|
||||
|
||||
/* > display_name */
|
||||
entry = config_get_entry(conf, "display_name");
|
||||
|
||||
if (entry && entry->value && *entry->value)
|
||||
{
|
||||
info->display_name = entry->value;
|
||||
entry->value = NULL;
|
||||
}
|
||||
info->display_name = config_take_string(conf, "display_name");
|
||||
|
||||
/* > description */
|
||||
entry = config_get_entry(conf, "description");
|
||||
|
||||
if (entry && entry->value && *entry->value)
|
||||
{
|
||||
info->description = entry->value;
|
||||
entry->value = NULL;
|
||||
}
|
||||
info->description = config_take_string(conf, "description");
|
||||
|
||||
/* > licenses */
|
||||
entry = config_get_entry(conf, "license");
|
||||
|
||||
if (entry && entry->value && *entry->value)
|
||||
{
|
||||
info->licenses = entry->value;
|
||||
entry->value = NULL;
|
||||
}
|
||||
info->licenses = config_take_string(conf, "license");
|
||||
|
||||
/* Clean up */
|
||||
config_file_free(conf);
|
||||
|
||||
@@ -196,6 +196,9 @@ int salamander_main(int argc, char *argv[])
|
||||
int main(int argc, char *argv[])
|
||||
#endif
|
||||
{
|
||||
/* File access for the config parser (see config_file.h). */
|
||||
config_file_set_io_default(config_file_io_filestream());
|
||||
|
||||
char libretro_path[PATH_MAX_LENGTH] = {0};
|
||||
void *args = NULL;
|
||||
struct rarch_main_wrap *wrap_args = NULL;
|
||||
|
||||
@@ -194,6 +194,7 @@ CONFIG FILE
|
||||
|
||||
#ifdef HAVE_CONFIGFILE
|
||||
#include "../libretro-common/file/config_file.c"
|
||||
#include "../libretro-common/file/config_file_io.c"
|
||||
#include "../libretro-common/file/config_file_userdata.c"
|
||||
#endif
|
||||
|
||||
|
||||
@@ -4,3 +4,6 @@ glsm/
|
||||
*.so
|
||||
*.dylib
|
||||
*.exe
|
||||
|
||||
# built sample binaries
|
||||
samples/**/config_file_test
|
||||
+909
-271
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,185 @@
|
||||
/* Copyright (C) 2010-2026 The RetroArch team
|
||||
*
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* The following license statement only applies to this file (config_file_io.c).
|
||||
* ---------------------------------------------------------------------------------------
|
||||
*
|
||||
* Permission is hereby granted, free of charge,
|
||||
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/* File access for config_file.
|
||||
*
|
||||
* The parser core (config_file.c) performs no file I/O: it consumes
|
||||
* buffers, the way the codecs under formats/ do, and reaches files
|
||||
* only through the config_file_io interface declared in
|
||||
* config_file.h. This translation unit is where the file system
|
||||
* actually enters the picture: the filestream/VFS-backed
|
||||
* implementation of that interface, the path-based constructors
|
||||
* built on it, and config_file_write - everything that opens a file
|
||||
* by name lives here, so a build that links only config_file.c gets
|
||||
* a pure in-memory parser with no streams/ dependency.
|
||||
*
|
||||
* Every constructor here self-registers the filestream
|
||||
* implementation as the io default (idempotently, never displacing
|
||||
* a host-installed interface), so any binary that loads at least
|
||||
* one config by path resolves '#include' directives exactly as
|
||||
* before this split. */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <compat/fopen_utf8.h>
|
||||
#include <file/config_file.h>
|
||||
#include <streams/file_stream.h>
|
||||
|
||||
static char *config_file_io_fs_read_file(const char *path,
|
||||
int64_t *len, void *ud)
|
||||
{
|
||||
void *buf = NULL;
|
||||
/* No path_is_valid() first: filestream_read_file() opens the file
|
||||
* itself and returns 0 when it cannot, so a preceding stat only
|
||||
* repeats the lookup the open already does - and no caller can see
|
||||
* its result, since a missing file and an unreadable one both land
|
||||
* on the same NULL return. Dropping it also closes the window
|
||||
* between the stat and the open in which the file could change. */
|
||||
if (!filestream_read_file(path, &buf, len))
|
||||
return NULL;
|
||||
/* filestream_read_file NUL-terminates past *len, which is the
|
||||
* buffer contract the parser relies on. */
|
||||
return (char*)buf;
|
||||
}
|
||||
|
||||
static void config_file_io_fs_free_file(char *buf, void *ud)
|
||||
{
|
||||
free(buf);
|
||||
}
|
||||
|
||||
const config_file_io_t *config_file_io_filestream(void)
|
||||
{
|
||||
static const config_file_io_t config_file_io_fs = {
|
||||
config_file_io_fs_read_file,
|
||||
config_file_io_fs_free_file,
|
||||
NULL
|
||||
};
|
||||
return &config_file_io_fs;
|
||||
}
|
||||
|
||||
/* Idempotent: never displaces an interface a host installed. */
|
||||
static void config_file_io_ensure_default(void)
|
||||
{
|
||||
if (!config_file_get_io_default())
|
||||
config_file_set_io_default(config_file_io_filestream());
|
||||
}
|
||||
|
||||
/**
|
||||
* config_file_new_with_callback:
|
||||
*
|
||||
* Loads a config file.
|
||||
* If @path is NULL, will create an empty config file.
|
||||
* Includes cb callbacks to run custom code during config file processing.
|
||||
*
|
||||
* @return Returns NULL if file doesn't exist.
|
||||
**/
|
||||
config_file_t *config_file_new_with_callback(
|
||||
const char *path, config_file_cb_t *cb)
|
||||
{
|
||||
int ret = 0;
|
||||
struct config_file *conf = config_file_new_alloc();
|
||||
if (!path || !*path)
|
||||
return conf;
|
||||
config_file_io_ensure_default();
|
||||
if ((ret = config_file_load_file(conf, path, cb)) == -1)
|
||||
{
|
||||
config_file_free(conf);
|
||||
return NULL;
|
||||
}
|
||||
else if (ret == 1)
|
||||
{
|
||||
free(conf);
|
||||
return NULL;
|
||||
}
|
||||
return conf;
|
||||
}
|
||||
|
||||
/**
|
||||
* config_file_new:
|
||||
*
|
||||
* Loads a config file.
|
||||
* If @path is NULL, will create an empty config file.
|
||||
*
|
||||
* @return Returns NULL if file doesn't exist.
|
||||
**/
|
||||
config_file_t *config_file_new(const char *path)
|
||||
{
|
||||
return config_file_new_with_callback(path, NULL);
|
||||
}
|
||||
|
||||
config_file_t *config_file_new_from_path_to_string(const char *path)
|
||||
{
|
||||
/* Historically this read the file and re-parsed it through
|
||||
* config_file_new_from_string; since the line loops were
|
||||
* unified the two path constructors have identical behaviour
|
||||
* (same parse, same include handling, path set before parsing,
|
||||
* no callback), so route through config_file_new - which parses
|
||||
* in borrow mode: the conf adopts the file buffer and entries
|
||||
* point straight into it, skipping the per-entry key/value
|
||||
* allocations and copies the from_string detour forced. */
|
||||
return config_file_new(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* config_append_file:
|
||||
*
|
||||
* Loads a new config, and appends its data to @conf.
|
||||
* The key-value pairs of the new config file takes priority over the old.
|
||||
**/
|
||||
bool config_append_file(config_file_t *conf, const char *path)
|
||||
{
|
||||
config_file_t *new_conf = config_file_new_from_path_to_string(path);
|
||||
if (!new_conf)
|
||||
return false;
|
||||
return config_file_append_conf(conf, new_conf);
|
||||
}
|
||||
|
||||
/**
|
||||
* config_file_write:
|
||||
*
|
||||
* Write the current config to a file.
|
||||
**/
|
||||
bool config_file_write(config_file_t *conf, const char *path, bool sort)
|
||||
{
|
||||
if (!conf)
|
||||
return false;
|
||||
if (conf->flags & CONF_FILE_FLG_MODIFIED)
|
||||
{
|
||||
if (!path || !*path)
|
||||
config_file_dump(conf, stdout, sort);
|
||||
else
|
||||
{
|
||||
char buf[0x4000];
|
||||
FILE *file = (FILE*)fopen_utf8(path, "wb");
|
||||
if (!file)
|
||||
return false;
|
||||
setvbuf(file, buf, _IOFBF, sizeof(buf));
|
||||
config_file_dump(conf, file, sort);
|
||||
if (file != stdout)
|
||||
fclose(file);
|
||||
conf->flags &= ~CONF_FILE_FLG_MODIFIED;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -253,6 +253,11 @@ static char *rhmap_strdup(const char *s)
|
||||
++count;
|
||||
++count;
|
||||
out = (char*)malloc(sizeof(char) * count);
|
||||
/* On allocation failure store no key string rather than crash:
|
||||
* the probe comparison already treats a NULL key_str as a
|
||||
* hash-only match, so lookups degrade instead of faulting. */
|
||||
if (!out)
|
||||
return NULL;
|
||||
out[--count] = 0;
|
||||
while (--count >= 0)
|
||||
out[count] = s[count];
|
||||
@@ -295,6 +300,20 @@ RHMAP__UNUSED static ptrdiff_t rhmap__idx(struct rhmap__hdr* hdr, uint32_t key,
|
||||
{
|
||||
if (add)
|
||||
{
|
||||
/* Never claim the table's last empty slot. Deletion
|
||||
* uses backward-shift (no tombstones), so a completely
|
||||
* full table has no terminator for this probe loop, for
|
||||
* absent-key lookups, or for the deletion fixup walk -
|
||||
* they all spin forever. Growth keeps the load factor
|
||||
* at or below 50%, so this bound is unreachable unless
|
||||
* RHMAP__GROW failed under OOM and the caller kept
|
||||
* inserting; in that case the insert is dropped (the
|
||||
* slot is returned unclaimed: its key stays 0, len is
|
||||
* unchanged, and a value written through it by the SET
|
||||
* macros is simply never found) rather than corrupting
|
||||
* or hanging. */
|
||||
if (hdr->len >= hdr->maxlen)
|
||||
return (ptrdiff_t)i;
|
||||
hdr->len++;
|
||||
hdr->keys[i] = key;
|
||||
if (str)
|
||||
|
||||
@@ -57,10 +57,42 @@ enum config_file_flags
|
||||
CONF_FILE_FLG_GUARANTEED_NO_DUPLICATES = (1 << 1)
|
||||
};
|
||||
|
||||
/* Per-entry ownership bits (struct config_entry_list::flags).
|
||||
* Entries parsed from a buffer the conf owns borrow their key and
|
||||
* value strings from it - the strings live exactly as long as the
|
||||
* conf, no per-entry allocation or copy is made, and the free paths
|
||||
* skip them. Entries created or overwritten through config_set_*
|
||||
* own heap strings as before. Treat key/value of entries with a
|
||||
* BORROWED bit as read-only storage. */
|
||||
#define CONF_ENTRY_FLG_KEY_BORROWED (1 << 0)
|
||||
#define CONF_ENTRY_FLG_VAL_BORROWED (1 << 1)
|
||||
/* Entry struct lives in a conf-owned pool block, not an individual
|
||||
* malloc: teardown releases the block, never the entry. */
|
||||
#define CONF_ENTRY_FLG_POOLED (1 << 2)
|
||||
|
||||
/* A text buffer owned by a config_file that entries borrow from.
|
||||
* 'io' is the interface the buffer came from (NULL: plain free). */
|
||||
struct config_file_owned_buf
|
||||
{
|
||||
struct config_file_owned_buf *next;
|
||||
char *data;
|
||||
const struct config_file_io *io;
|
||||
};
|
||||
|
||||
|
||||
struct config_file_entry_pool;
|
||||
|
||||
struct config_file
|
||||
{
|
||||
char *path;
|
||||
struct config_entry_list **entries_map;
|
||||
/* Chain of adopted text buffers entries borrow from (see
|
||||
* CONF_ENTRY_FLG_*); released at deinitialize, spliced to the
|
||||
* parent on include/append pilfering. */
|
||||
struct config_file_owned_buf *owned_bufs;
|
||||
/* Chain of entry-pool blocks (see CONF_ENTRY_FLG_POOLED);
|
||||
* lifetime and pilfer semantics match owned_bufs. */
|
||||
struct config_file_entry_pool *entry_pool;
|
||||
struct config_entry_list *entries;
|
||||
struct config_entry_list *tail;
|
||||
struct config_entry_list *last;
|
||||
@@ -132,8 +164,116 @@ config_file_t *config_file_new_with_callback(
|
||||
config_file_t *config_file_new_from_string(char *from_string,
|
||||
const char *path);
|
||||
|
||||
/**
|
||||
* config_file_new_take_string:
|
||||
*
|
||||
* Like config_file_new_from_string, but takes ownership of
|
||||
* @from_string instead of copying out of it: the conf adopts the
|
||||
* buffer and entries borrow their key/value strings straight from
|
||||
* it, the same zero-copy parse the path loaders use. The buffer
|
||||
* must come from the malloc family (it is released with free() at
|
||||
* conf teardown) and must not be touched or freed by the caller
|
||||
* afterwards - ownership transfers unconditionally, including on
|
||||
* failure. @s_len is the string length if the caller knows it, 0
|
||||
* otherwise (sizing hint only).
|
||||
**/
|
||||
config_file_t *config_file_new_take_string(char *from_string,
|
||||
size_t s_len, const char *path);
|
||||
|
||||
/* Streaming (push) parser: the codec-style ingest path.
|
||||
*
|
||||
* For consumers whose config bytes arrive in pieces - a network
|
||||
* fetch, an archive entry inflating, a data_transfer fill budgeted
|
||||
* across frames - instead of as one resident buffer. Open a
|
||||
* stream, push packets of any size in any split (line boundaries
|
||||
* need not align with packet boundaries), then finish:
|
||||
*
|
||||
* config_file_stream_t *st = config_file_stream_new(path);
|
||||
* while ((n = source_read(chunk, sizeof(chunk))) > 0)
|
||||
* config_file_stream_push(st, chunk, n);
|
||||
* conf = config_file_stream_finish(st);
|
||||
*
|
||||
* Memory held is one window of the unconsumed tail: complete lines
|
||||
* are parsed and discarded as each push arrives, so the window's
|
||||
* size is bounded by the longest line plus the largest packet, not
|
||||
* by the file. Parsed output is identical to handing the same
|
||||
* bytes to config_file_new_from_string in one piece.
|
||||
*
|
||||
* @path is recorded as the config's path (include resolution,
|
||||
* reference abbreviation); it may be NULL. '#include' directives
|
||||
* resolve through the registered io interface as usual.
|
||||
*
|
||||
* push returns false only on allocation failure; the stream then
|
||||
* swallows further pushes and finish returns NULL. finish frees
|
||||
* the stream and returns the parsed config (caller owns it).
|
||||
* config_file_stream_free abandons a stream part-way, freeing
|
||||
* everything including the partial config. */
|
||||
typedef struct config_file_stream config_file_stream_t;
|
||||
|
||||
config_file_stream_t *config_file_stream_new(const char *path);
|
||||
|
||||
bool config_file_stream_push(config_file_stream_t *stream,
|
||||
const void *data, size_t len);
|
||||
|
||||
config_file_t *config_file_stream_finish(config_file_stream_t *stream);
|
||||
|
||||
void config_file_stream_free(config_file_stream_t *stream);
|
||||
|
||||
config_file_t *config_file_new_from_path_to_string(const char *path);
|
||||
|
||||
/* File access used by the parser core.
|
||||
*
|
||||
* config_file.c itself performs no file I/O: every byte it parses is
|
||||
* either handed to it directly (config_file_new_from_string) or
|
||||
* fetched through this interface - the path-based constructors and
|
||||
* the '#include' directive both route through it. The interface is
|
||||
* how a host decides where config bytes come from: the plain-VFS
|
||||
* implementation in config_file_io.c, an archive, a network fetch,
|
||||
* or a test harness.
|
||||
*
|
||||
* read_file returns the entire file as a heap buffer with a NUL
|
||||
* appended after *len content bytes (the parser consumes the buffer
|
||||
* in place and relies on the terminator), or NULL on any failure.
|
||||
* free_file releases a buffer read_file returned.
|
||||
*
|
||||
* The default is registered once at startup, before any threads
|
||||
* that touch config files exist, and is only read afterwards.
|
||||
* The path-based constructors in config_file_io.c self-register the
|
||||
* filestream implementation, so any binary that links that file and
|
||||
* loads at least one config by path is covered; a host whose first
|
||||
* config comes from a string and may contain '#include' directives
|
||||
* must register explicitly first. With no interface registered,
|
||||
* path loads fail as file-not-found and include directives are
|
||||
* recorded in the include list but not loaded. */
|
||||
struct config_file_io
|
||||
{
|
||||
char *(*read_file)(const char *path, int64_t *len, void *ud);
|
||||
void (*free_file)(char *buf, void *ud);
|
||||
void *ud;
|
||||
};
|
||||
|
||||
typedef struct config_file_io config_file_io_t;
|
||||
|
||||
void config_file_set_io_default(const config_file_io_t *io);
|
||||
|
||||
const config_file_io_t *config_file_get_io_default(void);
|
||||
|
||||
/* The filestream/VFS-backed implementation (config_file_io.c). */
|
||||
const config_file_io_t *config_file_io_filestream(void);
|
||||
|
||||
/**
|
||||
* config_file_load_file:
|
||||
*
|
||||
* Loads the file at @path (through the registered io interface)
|
||||
* into an already-initialized @conf. Building block for the
|
||||
* path-based constructors in config_file_io.c.
|
||||
*
|
||||
* Returns 0 on success, 1 if the file could not be read (@conf is
|
||||
* untouched), -1 on allocation failure (@conf must be freed).
|
||||
**/
|
||||
int config_file_load_file(config_file_t *conf, const char *path,
|
||||
config_file_cb_t *cb);
|
||||
|
||||
/**
|
||||
* config_file_free:
|
||||
*
|
||||
@@ -153,6 +293,18 @@ bool config_file_deinitialize(config_file_t *conf);
|
||||
**/
|
||||
bool config_append_file(config_file_t *conf, const char *path);
|
||||
|
||||
/**
|
||||
* config_file_append_conf:
|
||||
*
|
||||
* Appends the entries of @new_conf to @conf - the merge half of
|
||||
* config_append_file, usable directly when the second config was
|
||||
* obtained some other way (parsed from a string, streamed in).
|
||||
* The key-value pairs of @new_conf take priority over @conf's.
|
||||
* Consumes @new_conf: its entries are pilfered and the struct is
|
||||
* freed regardless of outcome.
|
||||
**/
|
||||
bool config_file_append_conf(config_file_t *conf, config_file_t *new_conf);
|
||||
|
||||
/* All extract functions return true when value is valid and exists.
|
||||
* Returns false otherwise. */
|
||||
|
||||
@@ -161,9 +313,32 @@ struct config_entry_list
|
||||
char *key;
|
||||
char *value;
|
||||
struct config_entry_list *next;
|
||||
/* Cached strlen of key and value, so the write path does not
|
||||
* re-measure every string it just assembled. Zero means
|
||||
* "not known" - readers must fall back to strlen - which covers
|
||||
* entries whose strings were replaced without updating these,
|
||||
* and the pathological case of a string longer than 65535 bytes.
|
||||
* Both fit in what was structure padding, so caching them costs
|
||||
* no memory per entry. */
|
||||
uint16_t key_len;
|
||||
uint16_t value_len;
|
||||
/* If we got this from an #include,
|
||||
* do not allow overwrite. */
|
||||
bool readonly;
|
||||
/* CONF_ENTRY_FLG_* ownership bits */
|
||||
uint8_t flags;
|
||||
};
|
||||
|
||||
/* A block of entry structs owned by a config_file (see
|
||||
* CONF_ENTRY_FLG_POOLED). Entries are bump-allocated from 'slab';
|
||||
* the block is released whole at deinitialize and spliced to the
|
||||
* parent on include/append pilfering, exactly like owned_bufs. */
|
||||
struct config_file_entry_pool
|
||||
{
|
||||
struct config_file_entry_pool *next;
|
||||
size_t used;
|
||||
size_t cap;
|
||||
struct config_entry_list slab[1]; /* over-allocated to cap */
|
||||
};
|
||||
|
||||
struct config_file_entry
|
||||
@@ -262,6 +437,25 @@ bool config_get_char(config_file_t *conf, const char *entry, char *in);
|
||||
**/
|
||||
bool config_get_string(config_file_t *conf, const char *entry, char **in);
|
||||
|
||||
/**
|
||||
* config_take_string:
|
||||
*
|
||||
* Removes @key's value from the config and returns it as a heap
|
||||
* string the caller owns (release with free()). Returns NULL if the
|
||||
* key is absent or its value empty, leaving the entry untouched.
|
||||
*
|
||||
* This is the safe form of the historical idiom that lifted
|
||||
* entry->value out of the entry and NULLed it: entries parsed from a
|
||||
* path or taken string BORROW their strings from a buffer the conf
|
||||
* owns (see CONF_ENTRY_FLG_*), so that idiom now hands out a pointer
|
||||
* into the middle of the conf's buffer - reading it after
|
||||
* config_file_free is use-after-free, and free()ing it corrupts the
|
||||
* heap. This function copies when the value is borrowed and steals
|
||||
* it when the entry owns it, so the caller holds a real allocation
|
||||
* either way.
|
||||
**/
|
||||
char *config_take_string(config_file_t *conf, const char *key);
|
||||
|
||||
/* Extracts a string to a preallocated buffer. Avoid memory allocation. */
|
||||
bool config_get_array(config_file_t *conf, const char *entry, char *s, size_t len);
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ COMMON_SOURCES := \
|
||||
$(LIBRETRO_COMM_DIR)/encodings/encoding_utf.c \
|
||||
$(LIBRETRO_COMM_DIR)/features/features_cpu.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/config_file.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/config_file_io.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/config_file_userdata.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/file_path.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/file_path_io.c \
|
||||
|
||||
@@ -12,6 +12,7 @@ SOURCES := \
|
||||
$(LIBRETRO_COMM_DIR)/file/file_path.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/file_path_io.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/config_file.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/config_file_io.c \
|
||||
$(LIBRETRO_COMM_DIR)/lists/string_list.c \
|
||||
$(LIBRETRO_COMM_DIR)/streams/file_stream.c \
|
||||
$(LIBRETRO_COMM_DIR)/vfs/vfs_implementation.c \
|
||||
@@ -21,6 +22,11 @@ OBJS := $(SOURCES:.c=.o)
|
||||
|
||||
CFLAGS += -Wall -pedantic -std=gnu99 -g -I$(LIBRETRO_COMM_DIR)/include
|
||||
|
||||
ifneq ($(SANITIZER),)
|
||||
CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer $(CFLAGS)
|
||||
LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS)
|
||||
endif
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
%.o: %.c
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/* Differential fuzz target for config_file structural integrity.
|
||||
*
|
||||
* For every input, the same bytes are parsed two ways:
|
||||
* (a) in one piece via config_file_new_from_string
|
||||
* (b) through the streaming push API in pseudo-random packet
|
||||
* sizes derived from the input itself
|
||||
* and the results must agree on three structural invariants:
|
||||
*
|
||||
* 1. ENTRY-LIST EQUALITY: same entries, same order, same
|
||||
* key/value bytes, same count.
|
||||
* 2. MAP COHERENCE: for every key in the list, config_get_entry
|
||||
* returns exactly the FIRST list entry holding that key (the
|
||||
* duplicate policy), in both configs. This cross-checks the
|
||||
* fused parse-time hash against the real rhmap_hash_string
|
||||
* used at lookup time on arbitrary byte sequences, not just
|
||||
* the curated alphabet in the unit test.
|
||||
* 3. TAIL VALIDITY: conf->tail is the actual last list node.
|
||||
*
|
||||
* No io interface is registered, so '#include' directives are
|
||||
* recorded but never touch the file system - both paths behave
|
||||
* identically and the target stays hermetic.
|
||||
*
|
||||
* Build (clang):
|
||||
* clang -O1 -g -fsanitize=fuzzer,address,undefined \
|
||||
* -fno-sanitize-recover=all -I../../../include \
|
||||
* -o config_file_fuzz config_file_fuzz.c \
|
||||
* ../../../file/config_file.c <usual libretro-common deps>
|
||||
* Run:
|
||||
* ./config_file_fuzz -max_len=1024 corpus/
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <file/config_file.h>
|
||||
|
||||
static uint32_t xs32(uint32_t *s)
|
||||
{
|
||||
uint32_t x = *s;
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
return (*s = x ? x : 0x9e3779b9u);
|
||||
}
|
||||
|
||||
static void die(const char *what, const uint8_t *data, size_t size)
|
||||
{
|
||||
size_t i;
|
||||
fprintf(stderr, "STRUCTURAL DIVERGENCE: %s\ninput (%u bytes):", what,
|
||||
(unsigned)size);
|
||||
for (i = 0; i < size; i++)
|
||||
fprintf(stderr, " %02x", data[i]);
|
||||
fprintf(stderr, "\n");
|
||||
abort();
|
||||
}
|
||||
|
||||
static int str_eq(const char *a, const char *b)
|
||||
{
|
||||
if (!a || !b)
|
||||
return a == b;
|
||||
return !strcmp(a, b);
|
||||
}
|
||||
|
||||
/* Invariant 2+3 for one conf. The exact first-entry check walks
|
||||
* the list per key (quadratic), so it runs only on small inputs
|
||||
* where it is cheap; larger inputs get the linear form - every
|
||||
* listed key must resolve through the map to SOME entry carrying
|
||||
* that exact key. Small inputs dominate coverage of the map
|
||||
* logic anyway; large ones mostly exercise the window mechanics,
|
||||
* which invariant 1 covers at every size. */
|
||||
static void check_conf(config_file_t *c, const uint8_t *data, size_t size)
|
||||
{
|
||||
const struct config_entry_list *e;
|
||||
const struct config_entry_list *last = NULL;
|
||||
int exact = (size <= 2048);
|
||||
for (e = c->entries; e; e = e->next)
|
||||
{
|
||||
last = e;
|
||||
if (e->key)
|
||||
{
|
||||
const struct config_entry_list *hit = config_get_entry(c, e->key);
|
||||
if (!hit || !hit->key || strcmp(hit->key, e->key))
|
||||
die("map lookup missed or mismatched a listed key",
|
||||
data, size);
|
||||
if (exact)
|
||||
{
|
||||
const struct config_entry_list *first = NULL;
|
||||
const struct config_entry_list *w;
|
||||
for (w = c->entries; w; w = w->next)
|
||||
if (w->key && !strcmp(w->key, e->key))
|
||||
{
|
||||
first = w;
|
||||
break;
|
||||
}
|
||||
if (hit != first)
|
||||
die("map does not resolve key to first list entry",
|
||||
data, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (c->tail != last)
|
||||
die("conf->tail is not the last list node", data, size);
|
||||
}
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
|
||||
{
|
||||
config_file_t *slurped;
|
||||
config_file_t *streamed;
|
||||
config_file_stream_t *st;
|
||||
const struct config_entry_list *a;
|
||||
const struct config_entry_list *b;
|
||||
uint32_t rng = 0x811c9dc5u ^ (uint32_t)size;
|
||||
size_t off = 0;
|
||||
char *copy;
|
||||
|
||||
if (size > 65536)
|
||||
return 0;
|
||||
|
||||
/* (a) slurp */
|
||||
if (!(copy = (char*)malloc(size + 1)))
|
||||
return 0;
|
||||
memcpy(copy, data, size);
|
||||
copy[size] = '\0';
|
||||
slurped = config_file_new_from_string(copy, NULL);
|
||||
free(copy);
|
||||
if (!slurped)
|
||||
return 0;
|
||||
|
||||
/* (b) stream, packet sizes 1..37 driven by the input */
|
||||
if (!(st = config_file_stream_new(NULL)))
|
||||
{
|
||||
config_file_free(slurped);
|
||||
return 0;
|
||||
}
|
||||
while (off < size)
|
||||
{
|
||||
size_t n = 1 + (xs32(&rng) % 37);
|
||||
if (n > size - off)
|
||||
n = size - off;
|
||||
if (!config_file_stream_push(st, data + off, n))
|
||||
{
|
||||
config_file_stream_free(st);
|
||||
config_file_free(slurped);
|
||||
return 0;
|
||||
}
|
||||
off += n;
|
||||
}
|
||||
if (!(streamed = config_file_stream_finish(st)))
|
||||
{
|
||||
config_file_free(slurped);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Invariant 1: entry-list equality */
|
||||
a = slurped->entries;
|
||||
b = streamed->entries;
|
||||
while (a && b)
|
||||
{
|
||||
if ( !str_eq(a->key, b->key)
|
||||
|| !str_eq(a->value, b->value)
|
||||
|| a->readonly != b->readonly)
|
||||
die("entry mismatch", data, size);
|
||||
a = a->next;
|
||||
b = b->next;
|
||||
}
|
||||
if (a || b)
|
||||
die("entry count mismatch", data, size);
|
||||
|
||||
/* Invariants 2+3 on both */
|
||||
check_conf(slurped, data, size);
|
||||
check_conf(streamed, data, size);
|
||||
|
||||
config_file_free(slurped);
|
||||
config_file_free(streamed);
|
||||
return 0;
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <errno.h>
|
||||
|
||||
#include <file/config_file.h>
|
||||
#include <file/file_path.h>
|
||||
|
||||
static void test_config_file_parse_contains(
|
||||
const char *cfgtext,
|
||||
@@ -46,7 +47,15 @@ static void test_config_file_parse_contains(
|
||||
if (ok != (bool)val)
|
||||
abort();
|
||||
if (!val)
|
||||
{
|
||||
/* The harness frees everything it allocates so that under
|
||||
* strict LeakSanitizer any surviving allocation belongs to
|
||||
* config_file itself - this early return previously leaked
|
||||
* the conf (and masked real parser leaks behind harness
|
||||
* noise). */
|
||||
config_file_free(cfg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!out)
|
||||
out = strdup("");
|
||||
@@ -57,6 +66,7 @@ static void test_config_file_parse_contains(
|
||||
}
|
||||
printf("[SUCCESS] Key [%s] contains val [%s]\n", key, val);
|
||||
free(out);
|
||||
config_file_free(cfg);
|
||||
}
|
||||
|
||||
/* Regression for commit 87f2d0b (memcmp OOB on short '#' comment lines).
|
||||
@@ -109,6 +119,7 @@ static void test_config_file_short_comments(void)
|
||||
}
|
||||
printf("[SUCCESS] short '#' comment lines parsed without OOB\n");
|
||||
free(out);
|
||||
config_file_free(cfg);
|
||||
}
|
||||
|
||||
/* Regression for commit <round2-TBD> (config_get_int family silent zero).
|
||||
@@ -373,6 +384,487 @@ static void test_config_file_high_bit_bytes_smoke(void)
|
||||
printf("[SUCCESS] high-bit byte in config parsed without crash\n");
|
||||
}
|
||||
|
||||
static void test_config_file_hash_map_agreement(void)
|
||||
{
|
||||
/* The parser computes each entry's map hash inline during its key
|
||||
* scan instead of re-walking the key with rhmap_hash_string; every
|
||||
* *lookup* still hashes with the real rhmap_hash_string. So if the
|
||||
* inline fold ever diverges from rhmap_hash_string for any key
|
||||
* byte, the entry becomes unfindable through the map while still
|
||||
* sitting in the entry list. Parse a corpus that covers the whole
|
||||
* accepted key alphabet (0x21..0x7e), then require that every
|
||||
* listed key resolves through config_get_entry to the entry
|
||||
* holding it. */
|
||||
char cfgtext[8192];
|
||||
size_t _len = 0;
|
||||
int i;
|
||||
int checked = 0;
|
||||
int misses = 0;
|
||||
config_file_t *cfg = NULL;
|
||||
struct config_file_entry ent;
|
||||
|
||||
/* One key per accepted byte: k<byte>x = "v" */
|
||||
for (i = 0x21; i <= 0x7e; i++)
|
||||
{
|
||||
if (i == '=' || i == '#' || i == '"')
|
||||
continue; /* structural characters cannot appear bare */
|
||||
_len += snprintf(cfgtext + _len, sizeof(cfgtext) - _len,
|
||||
"k%cx = \"v%d\"\n", (char)i, i);
|
||||
}
|
||||
/* Plus long keys and a duplicate */
|
||||
_len += snprintf(cfgtext + _len, sizeof(cfgtext) - _len,
|
||||
"%s = \"long\"\n", "a_rather_long_configuration_key_name_to_cross_hash_word_sizes");
|
||||
_len += snprintf(cfgtext + _len, sizeof(cfgtext) - _len,
|
||||
"dup = \"first\"\ndup = \"second\"\n");
|
||||
|
||||
{
|
||||
char *copy = strdup(cfgtext);
|
||||
cfg = config_file_new_from_string(copy, NULL);
|
||||
free(copy);
|
||||
}
|
||||
|
||||
if (!cfg)
|
||||
{
|
||||
printf("[FAILED] hash-map agreement: parse failed\n");
|
||||
abort();
|
||||
}
|
||||
|
||||
if (config_get_entry_list_head(cfg, &ent))
|
||||
{
|
||||
do
|
||||
{
|
||||
if (ent.key)
|
||||
{
|
||||
const struct config_entry_list *hit =
|
||||
config_get_entry(cfg, ent.key);
|
||||
checked++;
|
||||
if (!hit)
|
||||
misses++;
|
||||
/* The map keeps the *first* entry for a duplicated
|
||||
* key; for unique keys the value must match. */
|
||||
else if (strcmp(ent.key, "dup") && strcmp(hit->value, ent.value))
|
||||
misses++;
|
||||
}
|
||||
} while (config_get_entry_list_next(&ent));
|
||||
}
|
||||
|
||||
if (misses == 0 && checked > 80)
|
||||
printf("[SUCCESS] all %d parsed keys resolve through the hash map\n",
|
||||
checked);
|
||||
else
|
||||
{
|
||||
printf("[FAILED] hash-map agreement: %d misses of %d keys\n",
|
||||
misses, checked);
|
||||
abort();
|
||||
}
|
||||
|
||||
config_file_free(cfg);
|
||||
}
|
||||
|
||||
static void test_config_file_stream_matches_from_string(void)
|
||||
{
|
||||
/* Push the same text through the streaming parser in packet
|
||||
* sizes that cross every line boundary (1 and 7 bytes) plus one
|
||||
* larger than the whole text, and require the entry list to
|
||||
* match from_string exactly. The window logic (tail retention,
|
||||
* NUL displacement, slide-back) has its off-by-ones exercised
|
||||
* hardest by the 1-byte case. */
|
||||
static const char *cfgtext =
|
||||
"alpha = \"1\"\n"
|
||||
"# comment line\n"
|
||||
"beta = \"two words\"\n"
|
||||
" gamma = bare\r\n"
|
||||
"delta = \"has # inside\"\n"
|
||||
"epsilon = \"\"\n"
|
||||
"zeta = last_line_without_newline";
|
||||
static const size_t packets[] = { 1, 7, 4096 };
|
||||
size_t pi;
|
||||
|
||||
for (pi = 0; pi < sizeof(packets) / sizeof(packets[0]); pi++)
|
||||
{
|
||||
size_t off;
|
||||
size_t text_len = strlen(cfgtext);
|
||||
char *copy = strdup(cfgtext);
|
||||
config_file_t *slurped = config_file_new_from_string(copy, NULL);
|
||||
config_file_stream_t *st = config_file_stream_new(NULL);
|
||||
config_file_t *streamed = NULL;
|
||||
struct config_file_entry a;
|
||||
struct config_file_entry b;
|
||||
bool more_a, more_b;
|
||||
|
||||
free(copy);
|
||||
if (!slurped || !st)
|
||||
abort();
|
||||
|
||||
for (off = 0; off < text_len; off += packets[pi])
|
||||
{
|
||||
size_t n = (text_len - off < packets[pi])
|
||||
? (text_len - off) : packets[pi];
|
||||
if (!config_file_stream_push(st, cfgtext + off, n))
|
||||
abort();
|
||||
}
|
||||
if (!(streamed = config_file_stream_finish(st)))
|
||||
abort();
|
||||
|
||||
more_a = config_get_entry_list_head(slurped, &a);
|
||||
more_b = config_get_entry_list_head(streamed, &b);
|
||||
while (more_a && more_b)
|
||||
{
|
||||
if ( !!a.key != !!b.key
|
||||
|| !!a.value != !!b.value
|
||||
|| (a.key && strcmp(a.key, b.key))
|
||||
|| (a.value && strcmp(a.value, b.value)))
|
||||
{
|
||||
printf("[FAILED] stream/packet=%u entry mismatch: [%s]=[%s] vs [%s]=[%s]\n",
|
||||
(unsigned)packets[pi],
|
||||
a.key ? a.key : "(null)", a.value ? a.value : "(null)",
|
||||
b.key ? b.key : "(null)", b.value ? b.value : "(null)");
|
||||
abort();
|
||||
}
|
||||
more_a = config_get_entry_list_next(&a);
|
||||
more_b = config_get_entry_list_next(&b);
|
||||
}
|
||||
if (more_a != more_b)
|
||||
{
|
||||
printf("[FAILED] stream/packet=%u entry count mismatch\n",
|
||||
(unsigned)packets[pi]);
|
||||
abort();
|
||||
}
|
||||
|
||||
config_file_free(slurped);
|
||||
config_file_free(streamed);
|
||||
}
|
||||
printf("[SUCCESS] streamed parse matches from_string at packet sizes 1/7/4096\n");
|
||||
}
|
||||
|
||||
static void test_config_file_stream_nul_ends_stream(void)
|
||||
{
|
||||
/* An embedded NUL must end the stream the way it ends the slurp
|
||||
* path's line walk: the truncated line parses as final, and
|
||||
* everything after - even in later packets - is dropped. Push
|
||||
* byte-at-a-time so a packet boundary is guaranteed to fall
|
||||
* between the NUL and the later lines (the divergent case the
|
||||
* clamp exists for). */
|
||||
static const char raw[] = "a = \"1\"\nb\0c = \"2\"\nd = \"3\"\n";
|
||||
size_t raw_len = sizeof(raw) - 1; /* keep embedded NUL */
|
||||
config_file_stream_t *st = config_file_stream_new(NULL);
|
||||
config_file_t *conf;
|
||||
char *out = NULL;
|
||||
size_t i;
|
||||
|
||||
if (!st)
|
||||
abort();
|
||||
for (i = 0; i < raw_len; i++)
|
||||
if (!config_file_stream_push(st, raw + i, 1))
|
||||
abort();
|
||||
if (!(conf = config_file_stream_finish(st)))
|
||||
abort();
|
||||
|
||||
if (!config_get_string(conf, "a", &out))
|
||||
{
|
||||
printf("[FAILED] NUL-ended stream lost the entry before the NUL\n");
|
||||
abort();
|
||||
}
|
||||
free(out);
|
||||
out = NULL;
|
||||
if (config_get_string(conf, "d", &out) || config_get_string(conf, "c", &out))
|
||||
{
|
||||
printf("[FAILED] NUL-ended stream parsed entries past the NUL\n");
|
||||
abort();
|
||||
}
|
||||
printf("[SUCCESS] embedded NUL ends the stream with slurp semantics\n");
|
||||
config_file_free(conf);
|
||||
}
|
||||
|
||||
static void test_config_file_pathless_reference_no_crash(void)
|
||||
{
|
||||
/* '#reference' in a pathless string config previously handed a
|
||||
* NULL base path to fill_pathname_abbreviated_or_relative,
|
||||
* whose strlcpy runs strlen on it - UB found by the
|
||||
* differential fuzzer. The reference must now be recorded
|
||||
* verbatim. */
|
||||
char *copy = strdup("#reference \"some/other.cfg\"\nfoo = \"bar\"\n");
|
||||
config_file_t *cfg = config_file_new_from_string(copy, NULL);
|
||||
|
||||
free(copy);
|
||||
if (!cfg)
|
||||
abort();
|
||||
if ( !cfg->references
|
||||
|| !cfg->references->path
|
||||
|| strcmp(cfg->references->path, "some/other.cfg"))
|
||||
{
|
||||
printf("[FAILED] pathless #reference not recorded verbatim (got %s)\n",
|
||||
(cfg->references && cfg->references->path)
|
||||
? cfg->references->path : "(none)");
|
||||
abort();
|
||||
}
|
||||
printf("[SUCCESS] pathless '#reference' recorded verbatim without UB\n");
|
||||
config_file_free(cfg);
|
||||
}
|
||||
|
||||
static void test_config_file_borrowed_entry_lifecycle(void)
|
||||
{
|
||||
/* Path-loaded entries borrow their strings from the adopted file
|
||||
* buffer. Exercise every mutation path against borrowed
|
||||
* entries - overwrite (must not free borrowed storage), unset
|
||||
* (same), append pilfering (buffers must travel), then teardown
|
||||
* (borrowed skipped, owned freed, buffers released) - under the
|
||||
* suite's sanitizers this catches any wrong free or dangling
|
||||
* borrow. */
|
||||
const char *tmp_a = "/tmp/cfg_borrow_a.cfg";
|
||||
const char *tmp_b = "/tmp/cfg_borrow_b.cfg";
|
||||
FILE *f;
|
||||
config_file_t *cfg;
|
||||
char *out = NULL;
|
||||
|
||||
f = fopen(tmp_a, "w");
|
||||
fprintf(f, "alpha = \"one\"\nbeta = \"two\"\ngamma = \"three\"\n");
|
||||
fclose(f);
|
||||
f = fopen(tmp_b, "w");
|
||||
fprintf(f, "beta = \"TWO\"\ndelta = \"four\"\n");
|
||||
fclose(f);
|
||||
|
||||
if (!(cfg = config_file_new(tmp_a)))
|
||||
abort();
|
||||
|
||||
/* Overwrite a borrowed value with an owned one, twice */
|
||||
config_set_string(cfg, "alpha", "replaced");
|
||||
config_set_string(cfg, "alpha", "replaced-again");
|
||||
if (!config_get_string(cfg, "alpha", &out) || strcmp(out, "replaced-again"))
|
||||
abort();
|
||||
free(out);
|
||||
out = NULL;
|
||||
|
||||
/* Unset a borrowed entry */
|
||||
config_unset(cfg, "gamma");
|
||||
if (config_get_entry(cfg, "gamma"))
|
||||
abort();
|
||||
|
||||
/* Append a second borrowed config: its buffers must move over */
|
||||
if (!config_append_file(cfg, tmp_b))
|
||||
abort();
|
||||
if (!config_get_string(cfg, "delta", &out) || strcmp(out, "four"))
|
||||
abort();
|
||||
free(out);
|
||||
out = NULL;
|
||||
if (!config_get_string(cfg, "beta", &out) || strcmp(out, "TWO"))
|
||||
abort();
|
||||
free(out);
|
||||
out = NULL;
|
||||
|
||||
/* New owned entry alongside borrowed ones */
|
||||
config_set_string(cfg, "epsilon", "five");
|
||||
|
||||
config_file_free(cfg);
|
||||
remove(tmp_a);
|
||||
remove(tmp_b);
|
||||
printf("[SUCCESS] borrowed-entry lifecycle (set/unset/append/free) clean\n");
|
||||
}
|
||||
|
||||
static void test_config_file_take_string(void)
|
||||
{
|
||||
/* take_string must parse identically to from_string on the same
|
||||
* bytes while owning the buffer (entries borrow from it), and
|
||||
* mutation after the take must behave like any borrowed conf. */
|
||||
static const char *cfgtext =
|
||||
"one = \"1\"\ntwo = \"double # inside\"\n three = bare\nfour = \"\"\n";
|
||||
char *copy_a = strdup(cfgtext);
|
||||
char *copy_b = strdup(cfgtext);
|
||||
config_file_t *ref = config_file_new_from_string(copy_a, NULL);
|
||||
config_file_t *took = config_file_new_take_string(copy_b,
|
||||
strlen(cfgtext), NULL);
|
||||
struct config_file_entry ea;
|
||||
struct config_file_entry eb;
|
||||
bool ma, mb;
|
||||
char *out = NULL;
|
||||
|
||||
free(copy_a); /* from_string copied; take_string owns copy_b now */
|
||||
if (!ref || !took)
|
||||
abort();
|
||||
|
||||
ma = config_get_entry_list_head(ref, &ea);
|
||||
mb = config_get_entry_list_head(took, &eb);
|
||||
while (ma && mb)
|
||||
{
|
||||
if ( strcmp(ea.key, eb.key)
|
||||
|| strcmp(ea.value, eb.value))
|
||||
{
|
||||
printf("[FAILED] take_string mismatch: [%s]=[%s] vs [%s]=[%s]\n",
|
||||
ea.key, ea.value, eb.key, eb.value);
|
||||
abort();
|
||||
}
|
||||
ma = config_get_entry_list_next(&ea);
|
||||
mb = config_get_entry_list_next(&eb);
|
||||
}
|
||||
if (ma != mb)
|
||||
abort();
|
||||
|
||||
/* Mutate through the API against the borrowed storage */
|
||||
config_set_string(took, "two", "owned-now");
|
||||
config_unset(took, "three");
|
||||
if (!config_get_string(took, "two", &out) || strcmp(out, "owned-now"))
|
||||
abort();
|
||||
free(out);
|
||||
|
||||
config_file_free(ref);
|
||||
config_file_free(took);
|
||||
printf("[SUCCESS] take_string parses identically and owns its buffer\n");
|
||||
}
|
||||
|
||||
static void test_config_take_string(void)
|
||||
{
|
||||
/* config_take_string exists because core_info's historical idiom
|
||||
* - lift entry->value out of the entry, NULL it, free it later -
|
||||
* corrupts the heap against borrowed entries: the pointer lands
|
||||
* in the middle of the conf's adopted file buffer (found as a
|
||||
* STATUS_HEAP_CORRUPTION crash in core_info_free on Windows).
|
||||
* The take must hand out a real allocation in both ownership
|
||||
* modes, outliving the conf. */
|
||||
const char *tmp_path = "/tmp/cfg_take.cfg";
|
||||
FILE *f;
|
||||
config_file_t *cfg;
|
||||
char *taken_borrowed = NULL;
|
||||
char *taken_owned = NULL;
|
||||
struct config_entry_list *entry;
|
||||
|
||||
f = fopen(tmp_path, "w");
|
||||
fprintf(f, "borrowed_key = \"from file\"\nempty_key = \"\"\n");
|
||||
fclose(f);
|
||||
|
||||
if (!(cfg = config_file_new(tmp_path))) /* borrowed entries */
|
||||
abort();
|
||||
config_set_string(cfg, "owned_key", "from set");
|
||||
|
||||
/* Borrowed: must be copied out */
|
||||
if ( !(taken_borrowed = config_take_string(cfg, "borrowed_key"))
|
||||
|| strcmp(taken_borrowed, "from file"))
|
||||
abort();
|
||||
/* Owned: stolen; either way the entry is emptied */
|
||||
if ( !(taken_owned = config_take_string(cfg, "owned_key"))
|
||||
|| strcmp(taken_owned, "from set"))
|
||||
abort();
|
||||
if ( !(entry = config_get_entry(cfg, "borrowed_key"))
|
||||
|| entry->value)
|
||||
abort();
|
||||
/* Missing and empty: NULL, entry untouched */
|
||||
if (config_take_string(cfg, "no_such_key"))
|
||||
abort();
|
||||
if (config_take_string(cfg, "empty_key"))
|
||||
abort();
|
||||
if ( !(entry = config_get_entry(cfg, "empty_key"))
|
||||
|| !entry->value)
|
||||
abort();
|
||||
|
||||
/* The taken strings must outlive the conf and be free()-able:
|
||||
* under this suite's sanitizers, a borrowed pointer leaking
|
||||
* through here is a bad-free. */
|
||||
config_file_free(cfg);
|
||||
if (strcmp(taken_borrowed, "from file") || strcmp(taken_owned, "from set"))
|
||||
abort();
|
||||
free(taken_borrowed);
|
||||
free(taken_owned);
|
||||
remove(tmp_path);
|
||||
printf("[SUCCESS] config_take_string owns its result in both ownership modes\n");
|
||||
}
|
||||
|
||||
static void test_config_entry_cached_lengths(void)
|
||||
{
|
||||
/* key_len/value_len are consumed by the write path, so a stale
|
||||
* one silently writes a truncated or over-long line. Walk every
|
||||
* entry after a parse and after each kind of mutation and assert
|
||||
* the cache either matches strlen exactly or is 0 ("unknown",
|
||||
* which readers must handle by measuring). */
|
||||
const char *tmp_path = "/tmp/cfg_lens.cfg";
|
||||
FILE *f;
|
||||
config_file_t *cfg;
|
||||
struct config_file_entry it;
|
||||
bool more;
|
||||
int checked = 0;
|
||||
|
||||
f = fopen(tmp_path, "w");
|
||||
fprintf(f, "short = \"1\"\n");
|
||||
fprintf(f, "longer_key_name = \"a rather longer value with spaces\"\n");
|
||||
fprintf(f, "bare = unquoted\n");
|
||||
fprintf(f, "empty = \"\"\n");
|
||||
fclose(f);
|
||||
|
||||
if (!(cfg = config_file_new(tmp_path)))
|
||||
abort();
|
||||
|
||||
/* Mutations: overwrite a parsed (borrowed) value, add a fresh
|
||||
* owned entry, overwrite that again, and unset one. */
|
||||
config_set_string(cfg, "short", "replaced with something longer");
|
||||
config_set_string(cfg, "added", "brand new");
|
||||
config_set_string(cfg, "added", "shorter");
|
||||
config_unset(cfg, "bare");
|
||||
|
||||
{
|
||||
const struct config_entry_list *e;
|
||||
for (e = cfg->entries; e; e = e->next)
|
||||
{
|
||||
if (e->key)
|
||||
{
|
||||
if (e->key_len && e->key_len != (uint16_t)strlen(e->key))
|
||||
{
|
||||
printf("[FAILED] key_len %u != strlen %u for [%s]\n",
|
||||
(unsigned)e->key_len, (unsigned)strlen(e->key), e->key);
|
||||
abort();
|
||||
}
|
||||
checked++;
|
||||
}
|
||||
else if (e->key_len)
|
||||
abort(); /* no key, but a cached key length */
|
||||
|
||||
if (e->value)
|
||||
{
|
||||
if (e->value_len && e->value_len != (uint16_t)strlen(e->value))
|
||||
{
|
||||
printf("[FAILED] value_len %u != strlen %u for [%s]\n",
|
||||
(unsigned)e->value_len, (unsigned)strlen(e->value),
|
||||
e->key ? e->key : "?");
|
||||
abort();
|
||||
}
|
||||
}
|
||||
else if (e->value_len)
|
||||
abort(); /* no value, but a cached value length */
|
||||
}
|
||||
}
|
||||
if (checked < 4)
|
||||
abort();
|
||||
|
||||
/* And the write path must still round-trip: read the dump back
|
||||
* and confirm the values survive intact. */
|
||||
if (!config_file_write(cfg, "/tmp/cfg_lens_out.cfg", false))
|
||||
abort();
|
||||
config_file_free(cfg);
|
||||
|
||||
if (!(cfg = config_file_new("/tmp/cfg_lens_out.cfg")))
|
||||
abort();
|
||||
more = config_get_entry_list_head(cfg, &it);
|
||||
while (more)
|
||||
{
|
||||
if (!it.key || !it.value)
|
||||
abort();
|
||||
more = config_get_entry_list_next(&it);
|
||||
}
|
||||
{
|
||||
char *v = NULL;
|
||||
if ( !config_get_string(cfg, "short", &v)
|
||||
|| strcmp(v, "replaced with something longer"))
|
||||
abort();
|
||||
free(v);
|
||||
v = NULL;
|
||||
if ( !config_get_string(cfg, "longer_key_name", &v)
|
||||
|| strcmp(v, "a rather longer value with spaces"))
|
||||
abort();
|
||||
free(v);
|
||||
}
|
||||
config_file_free(cfg);
|
||||
remove(tmp_path);
|
||||
remove("/tmp/cfg_lens_out.cfg");
|
||||
printf("[SUCCESS] cached entry lengths agree with strlen and round-trip\n");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_config_file_parse_contains("foo = \"bar\"\n", "foo", "bar");
|
||||
@@ -416,4 +908,12 @@ int main(void)
|
||||
|
||||
test_config_file_deinitialize_clears_fields();
|
||||
test_config_file_high_bit_bytes_smoke();
|
||||
test_config_file_hash_map_agreement();
|
||||
test_config_file_stream_matches_from_string();
|
||||
test_config_file_stream_nul_ends_stream();
|
||||
test_config_file_pathless_reference_no_crash();
|
||||
test_config_file_borrowed_entry_lifecycle();
|
||||
test_config_file_take_string();
|
||||
test_config_take_string();
|
||||
test_config_entry_cached_lengths();
|
||||
}
|
||||
@@ -6379,6 +6379,13 @@ int rarch_main(int argc, char *argv[], void *data)
|
||||
settings_t *settings;
|
||||
struct rarch_state *p_rarch = &rarch_st;
|
||||
runloop_state_t *runloop_st = runloop_state_get_ptr();
|
||||
/* Register file access for the config parser before anything can
|
||||
* load a config - including config-from-string paths that carry
|
||||
* '#include' directives, which resolve through this interface.
|
||||
* The constructors in config_file_io.c self-register too; this
|
||||
* explicit call just removes any ordering dependency on which
|
||||
* config API gets used first. */
|
||||
config_file_set_io_default(config_file_io_filestream());
|
||||
#if defined(HAVE_CG) || defined(HAVE_GLSL) || defined(HAVE_SLANG) || defined(HAVE_HLSL)
|
||||
video_driver_modify_disp_flags(VIDEO_FLAG_SHADER_PRESETS_NEED_RELOAD, 0);
|
||||
#endif
|
||||
|
||||
@@ -107,6 +107,7 @@ SOURCES_C := \
|
||||
$(CORE_DIR)/libretro-db/rmsgpack_dom.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/archive_file.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/config_file.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/config_file_io.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/file_path.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/file_path_io.c \
|
||||
$(LIBRETRO_COMM_DIR)/file/retro_dirent.c \
|
||||
|
||||
+539
-147
@@ -25,7 +25,6 @@
|
||||
#include <retro_dirent.h>
|
||||
#include <string/stdstring.h>
|
||||
#include <file/config_file.h>
|
||||
#include <compat/strcasestr.h>
|
||||
#include <streams/file_stream.h>
|
||||
|
||||
#include "../configuration.h"
|
||||
@@ -59,12 +58,31 @@ typedef struct
|
||||
* has, and this task runs on the main loop. */
|
||||
struct RDIR *scan_rdir;
|
||||
config_file_t *scan_best;
|
||||
/* Verified-hint index (see input_autoconfigure_index_try):
|
||||
* index_build accumulates identity tuples for the directory
|
||||
* currently being scanned so the index can be (re)written when
|
||||
* the walk completes; index_build_count numbers its entries. */
|
||||
config_file_t *index_build;
|
||||
unsigned index_build_count;
|
||||
/* Affinity high-water mark when the current directory's walk
|
||||
* began, to tell whether THIS directory produced a match. */
|
||||
unsigned scan_dir_start_affinity;
|
||||
unsigned scan_max_affinity;
|
||||
unsigned scan_dir_idx;
|
||||
unsigned port;
|
||||
input_device_info_t device_info; /* unsigned alignment */
|
||||
uint8_t flags;
|
||||
uint8_t scan_done;
|
||||
/* A tuple did not fit its index value buffer, so this
|
||||
* directory's index would be incomplete: abandon the build (see
|
||||
* input_autoconfigure_index_collect). */
|
||||
uint8_t index_build_abandoned;
|
||||
/* The current directory's index was fresh (valid header, file
|
||||
* count matches) but ranked no candidate at or above the match
|
||||
* bar. If the full walk then agrees that nothing matches, the
|
||||
* index was simply right, and rewriting an identical one on
|
||||
* every connect of an unrecognised device would be waste. */
|
||||
uint8_t index_fresh_no_candidate;
|
||||
} autoconfig_handle_t;
|
||||
|
||||
/*********************/
|
||||
@@ -76,6 +94,12 @@ static void free_autoconfig_handle(autoconfig_handle_t *autoconfig_handle)
|
||||
if (!autoconfig_handle)
|
||||
return;
|
||||
|
||||
if (autoconfig_handle->index_build)
|
||||
{
|
||||
config_file_free(autoconfig_handle->index_build);
|
||||
autoconfig_handle->index_build = NULL;
|
||||
}
|
||||
|
||||
/* A scan may be part way through a directory. */
|
||||
if (autoconfig_handle->scan_rdir)
|
||||
{
|
||||
@@ -130,6 +154,68 @@ static void input_autoconfigure_free(retro_task_t *task)
|
||||
* > 30-39: VID+PID match
|
||||
* > 50-59: Both device name and VID+PID match
|
||||
* > A physical port match adds 10, a physical port mismatch subtracts 10. */
|
||||
/* Affinity of one identity tuple (one of a profile's main entry or
|
||||
* its up-to-9 alternatives) against the connected device. Single
|
||||
* source of truth: both the per-file computation below and the
|
||||
* in-memory index matcher rank with this exact function, so the
|
||||
* index can never order candidates differently than a real scan
|
||||
* would. */
|
||||
static unsigned input_autoconfigure_tuple_affinity(
|
||||
autoconfig_handle_t *autoconfig_handle,
|
||||
uint16_t config_vid, uint16_t config_pid,
|
||||
const char *config_device, const char *config_phys,
|
||||
int alt)
|
||||
{
|
||||
unsigned affinity = 0;
|
||||
|
||||
/* Check for matching VID+PID */
|
||||
#ifdef HAVE_BLISSBOX
|
||||
/* > Bliss-Box shenanigans: its adapters report the wrapped
|
||||
* pad's ids at a fixed pid, so profiles are matched on vid
|
||||
* alone with the pid treated as the Bliss-Box constant. */
|
||||
if (autoconfig_handle->device_info.vid == BLISSBOX_VID)
|
||||
config_pid = BLISSBOX_PID;
|
||||
|
||||
if ( (autoconfig_handle->device_info.vid == config_vid)
|
||||
&& (autoconfig_handle->device_info.pid == config_pid)
|
||||
&& (config_vid != 0)
|
||||
&& (config_pid != 0)
|
||||
&& (autoconfig_handle->device_info.vid != BLISSBOX_VID)
|
||||
&& (autoconfig_handle->device_info.pid != BLISSBOX_PID))
|
||||
affinity += 30;
|
||||
#else
|
||||
if ( (autoconfig_handle->device_info.vid == config_vid)
|
||||
&& (autoconfig_handle->device_info.pid == config_pid)
|
||||
&& (config_vid != 0)
|
||||
&& (config_pid != 0))
|
||||
affinity += 30;
|
||||
#endif
|
||||
|
||||
/* Check for matching device name */
|
||||
if ( config_device
|
||||
&& *config_device
|
||||
&& string_is_equal(config_device,
|
||||
autoconfig_handle->device_info.name))
|
||||
affinity += 20;
|
||||
|
||||
/* Check for matching physical location */
|
||||
if ( affinity >= 20
|
||||
&& config_phys
|
||||
&& *config_phys)
|
||||
{
|
||||
if (strstr(autoconfig_handle->device_info.phys, config_phys))
|
||||
affinity += 10;
|
||||
else
|
||||
affinity -= 10;
|
||||
}
|
||||
|
||||
/* Store the selected alternative as last digit of affinity. */
|
||||
if (affinity > 0)
|
||||
affinity += alt;
|
||||
|
||||
return affinity;
|
||||
}
|
||||
|
||||
static unsigned input_autoconfigure_get_config_file_affinity(
|
||||
autoconfig_handle_t *autoconfig_handle,
|
||||
config_file_t *config)
|
||||
@@ -148,6 +234,8 @@ static unsigned input_autoconfigure_get_config_file_affinity(
|
||||
uint16_t config_pid = 0;
|
||||
int tmp_int = 0;
|
||||
unsigned affinity = 0;
|
||||
const char *config_device = NULL;
|
||||
const char *config_phys = NULL;
|
||||
|
||||
if (i == 0)
|
||||
config_key_postfix[0] = '\0';
|
||||
@@ -170,57 +258,25 @@ static unsigned input_autoconfigure_get_config_file_affinity(
|
||||
if (config_get_int(config, config_key, &tmp_int))
|
||||
config_pid = (uint16_t)tmp_int;
|
||||
|
||||
/* Check for matching VID+PID */
|
||||
#ifdef HAVE_BLISSBOX
|
||||
/* > Bliss-Box shenanigans... */
|
||||
if (autoconfig_handle->device_info.vid == BLISSBOX_VID)
|
||||
config_pid = BLISSBOX_PID;
|
||||
|
||||
if ( (autoconfig_handle->device_info.vid == config_vid)
|
||||
&& (autoconfig_handle->device_info.pid == config_pid)
|
||||
&& (config_vid != 0)
|
||||
&& (config_pid != 0)
|
||||
&& (autoconfig_handle->device_info.vid != BLISSBOX_VID)
|
||||
&& (autoconfig_handle->device_info.pid != BLISSBOX_PID))
|
||||
affinity += 30;
|
||||
#else
|
||||
if ( (autoconfig_handle->device_info.vid == config_vid)
|
||||
&& (autoconfig_handle->device_info.pid == config_pid)
|
||||
&& (config_vid != 0)
|
||||
&& (config_pid != 0))
|
||||
affinity += 30;
|
||||
#endif
|
||||
|
||||
/* Check for matching device name */
|
||||
_len = strlcpy(config_key, "input_device",
|
||||
sizeof(config_key));
|
||||
strlcpy(config_key + _len, config_key_postfix,
|
||||
sizeof(config_key) - _len);
|
||||
if ( (entry = config_get_entry(config, config_key))
|
||||
&& (entry->value && *entry->value)
|
||||
&& string_is_equal(entry->value,
|
||||
autoconfig_handle->device_info.name))
|
||||
affinity += 20;
|
||||
&& (entry->value))
|
||||
config_device = entry->value;
|
||||
|
||||
/* Check for matching physical location */
|
||||
_len = strlcpy(config_key, "input_phys",
|
||||
sizeof(config_key));
|
||||
_len += strlcpy(config_key + _len, config_key_postfix,
|
||||
sizeof(config_key) - _len);
|
||||
if ( affinity >= 20
|
||||
&& (entry = config_get_entry(config, config_key))
|
||||
&& (entry->value && *entry->value))
|
||||
{
|
||||
if (strstr(autoconfig_handle->device_info.phys,
|
||||
entry->value))
|
||||
affinity += 10;
|
||||
else
|
||||
affinity -= 10;
|
||||
}
|
||||
if ( (entry = config_get_entry(config, config_key))
|
||||
&& (entry->value))
|
||||
config_phys = entry->value;
|
||||
|
||||
/* Store the selected alternative as last digit of affinity. */
|
||||
if (affinity > 0)
|
||||
affinity += i;
|
||||
affinity = input_autoconfigure_tuple_affinity(
|
||||
autoconfig_handle, config_vid, config_pid,
|
||||
config_device, config_phys, i);
|
||||
|
||||
if (max_affinity < affinity)
|
||||
max_affinity = affinity;
|
||||
@@ -274,85 +330,380 @@ static void input_autoconfigure_set_config_file(
|
||||
* (in the autoconfig directory) matching the connected
|
||||
* input device
|
||||
* > Returns 'true' if successful */
|
||||
/* Could this file possibly score above zero for the device we are
|
||||
* looking for?
|
||||
|
||||
/*******************************/
|
||||
/* Verified-hint profile index */
|
||||
/*******************************/
|
||||
|
||||
/* A per-directory index of profile identity tuples, so that a device
|
||||
* connect can rank every profile with two file operations (read the
|
||||
* index, open the winner) instead of one open per profile - the
|
||||
* historical slowness of this task is exactly that per-file open
|
||||
* latency on cold or slow storage.
|
||||
*
|
||||
* A profile is only ever selected on a positive affinity, and affinity
|
||||
* only becomes positive through a matching vendor+product id or a
|
||||
* matching device name - input_phys merely adjusts a score that is
|
||||
* already at least 20. So a file containing neither the name nor the
|
||||
* ids cannot win, and does not need parsing.
|
||||
* The index is a hint, never an authority:
|
||||
* - the ranking uses input_autoconfigure_tuple_affinity, the same
|
||||
* function the real scan uses, over tuples recorded verbatim;
|
||||
* - the winning candidate is opened and re-scored against its real
|
||||
* file contents, and accepted only if the real affinity is at
|
||||
* least what the index claimed;
|
||||
* - anything else - no index, unparseable index, stale file count,
|
||||
* winner unreadable, size drift, verify shortfall, or no
|
||||
* candidate reaching the same >= 20 bar a scan match needs -
|
||||
* falls back to the full directory scan, which also rebuilds the
|
||||
* index from data it necessarily reads anyway.
|
||||
* A stale index can therefore cost one wasted open, but can never
|
||||
* select a profile the full scan would not have selected.
|
||||
*
|
||||
* That matters because parsing is the expensive part by a wide margin:
|
||||
* building a config_file_t reads and hashes every binding in the file,
|
||||
* some fifty entries, to answer questions about four of them. Reading
|
||||
* the bytes is cheap - one open, one read - and the file is read here
|
||||
* either way, so a candidate is parsed from the buffer already in hand
|
||||
* rather than opened a second time.
|
||||
*
|
||||
* The test is deliberately generous. A false positive costs one parse
|
||||
* that would have happened anyway; a false negative would silently
|
||||
* pick the wrong profile, so anything uncertain must pass. Hence:
|
||||
*
|
||||
* - the name is matched as a substring, which is sound because the
|
||||
* affinity check compares it with string_is_equal - the exact text
|
||||
* has to be present for that to succeed;
|
||||
* - the ids are read by config_get_int, which uses strtol with base
|
||||
* 0, so decimal, hexadecimal and octal are all legal spellings of
|
||||
* the same number and all three are looked for.
|
||||
*/
|
||||
static bool input_autoconfigure_file_may_match(
|
||||
const char *buf,
|
||||
const char *device_name,
|
||||
unsigned vid, unsigned pid)
|
||||
* The file lives beside the profiles it describes (no .cfg extension,
|
||||
* so the scan's extension filter never sees it). If the directory is
|
||||
* not writable the write fails silently and every connect simply
|
||||
* performs the full scan, exactly as before this mechanism existed.
|
||||
* Freshness is keyed on the directory's *.cfg count (the VFS layer
|
||||
* exposes no mtime): additions and removals are caught by the count,
|
||||
* a renamed or deleted winner by the failed open, an edited winner by
|
||||
* the re-score, and a profile hand-edited to match a previously
|
||||
* unmatched device by the no-candidate fallback. The one blind spot
|
||||
* is an in-place edit that would promote a profile that was neither
|
||||
* the winner nor previously acceptable; deleting the index file (or
|
||||
* any add/remove in the directory) clears it. */
|
||||
|
||||
#define AUTOCONFIG_INDEX_NAME ".autoconfig_index"
|
||||
#define AUTOCONFIG_INDEX_VERSION 1
|
||||
|
||||
/* Count the *.cfg entries in a directory: two getdents syscalls,
|
||||
* no per-file opens. Must apply the same filter as the scan walk
|
||||
* so the count is comparable. */
|
||||
static int input_autoconfigure_index_dir_count(const char *dir)
|
||||
{
|
||||
char num[24];
|
||||
struct RDIR *rdir;
|
||||
int count = 0;
|
||||
|
||||
if (!buf)
|
||||
return false;
|
||||
if (!(rdir = retro_opendir(dir)))
|
||||
return -1;
|
||||
|
||||
if (device_name && *device_name && strstr(buf, device_name))
|
||||
return true;
|
||||
|
||||
if (vid && pid)
|
||||
while (retro_readdir(rdir))
|
||||
{
|
||||
bool have_vid = false;
|
||||
bool have_pid = false;
|
||||
|
||||
snprintf(num, sizeof(num), "%u", vid);
|
||||
have_vid = (strstr(buf, num) != NULL);
|
||||
if (!have_vid)
|
||||
{
|
||||
snprintf(num, sizeof(num), "0x%x", vid);
|
||||
have_vid = (compat_strcasestr(buf, num) != NULL);
|
||||
}
|
||||
if (!have_vid)
|
||||
{
|
||||
snprintf(num, sizeof(num), "0%o", vid);
|
||||
have_vid = (strstr(buf, num) != NULL);
|
||||
}
|
||||
|
||||
if (have_vid)
|
||||
{
|
||||
snprintf(num, sizeof(num), "%u", pid);
|
||||
have_pid = (strstr(buf, num) != NULL);
|
||||
if (!have_pid)
|
||||
{
|
||||
snprintf(num, sizeof(num), "0x%x", pid);
|
||||
have_pid = (compat_strcasestr(buf, num) != NULL);
|
||||
}
|
||||
if (!have_pid)
|
||||
{
|
||||
snprintf(num, sizeof(num), "0%o", pid);
|
||||
have_pid = (strstr(buf, num) != NULL);
|
||||
}
|
||||
}
|
||||
|
||||
if (have_vid && have_pid)
|
||||
return true;
|
||||
const char *entry_name = retro_dirent_get_name(rdir);
|
||||
if ( entry_name
|
||||
&& *entry_name
|
||||
&& string_is_equal_noncase(
|
||||
path_get_extension(entry_name), "cfg"))
|
||||
count++;
|
||||
}
|
||||
|
||||
return false;
|
||||
retro_closedir(rdir);
|
||||
return count;
|
||||
}
|
||||
|
||||
/* Record one profile's identity tuples into the index being built.
|
||||
* Reads exactly the keys input_autoconfigure_get_config_file_affinity
|
||||
* reads, and stores them verbatim (tab-separated inside the quoted
|
||||
* value; a device name containing a tab or quote would corrupt only
|
||||
* its own entry, which the verify step then rejects). */
|
||||
static void input_autoconfigure_index_collect(
|
||||
autoconfig_handle_t *autoconfig_handle,
|
||||
const char *entry_name, int64_t f_size, config_file_t *config)
|
||||
{
|
||||
int i;
|
||||
char index_key[32];
|
||||
char index_val[NAME_MAX_LENGTH + 128];
|
||||
unsigned file_idx = autoconfig_handle->index_build_count;
|
||||
|
||||
if (autoconfig_handle->index_build_abandoned)
|
||||
return;
|
||||
|
||||
if (!autoconfig_handle->index_build)
|
||||
{
|
||||
if (!(autoconfig_handle->index_build = config_file_new_alloc()))
|
||||
return;
|
||||
autoconfig_handle->index_build->flags |=
|
||||
CONF_FILE_FLG_GUARANTEED_NO_DUPLICATES;
|
||||
}
|
||||
|
||||
snprintf(index_key, sizeof(index_key), "f%u", file_idx);
|
||||
config_set_string(autoconfig_handle->index_build, index_key,
|
||||
entry_name);
|
||||
snprintf(index_key, sizeof(index_key), "s%u", file_idx);
|
||||
/* Written as int to stay symmetric with the config_get_int the
|
||||
* verify step reads it back with; a profile anywhere near that
|
||||
* bound is not a profile. Oversized files record 0, which the
|
||||
* size-drift check treats as 'unknown' and skips. */
|
||||
snprintf(index_val, sizeof(index_val), "%d",
|
||||
(f_size > 0 && f_size <= 0x7fffffff) ? (int)f_size : 0);
|
||||
config_set_string(autoconfig_handle->index_build, index_key,
|
||||
index_val);
|
||||
|
||||
for (i = 0; i < 10; i++)
|
||||
{
|
||||
size_t _len;
|
||||
char config_key[30];
|
||||
char config_key_postfix[7];
|
||||
struct config_entry_list *entry = NULL;
|
||||
uint16_t config_vid = 0;
|
||||
uint16_t config_pid = 0;
|
||||
int tmp_int = 0;
|
||||
const char *config_device = "";
|
||||
const char *config_phys = "";
|
||||
|
||||
if (i == 0)
|
||||
config_key_postfix[0] = '\0';
|
||||
else
|
||||
snprintf(config_key_postfix, sizeof(config_key_postfix),
|
||||
"_alt%d", i);
|
||||
|
||||
_len = strlcpy(config_key, "input_vendor_id",
|
||||
sizeof(config_key));
|
||||
strlcpy(config_key + _len, config_key_postfix,
|
||||
sizeof(config_key) - _len);
|
||||
if (config_get_int(config, config_key, &tmp_int))
|
||||
config_vid = (uint16_t)tmp_int;
|
||||
|
||||
_len = strlcpy(config_key, "input_product_id",
|
||||
sizeof(config_key));
|
||||
strlcpy(config_key + _len, config_key_postfix,
|
||||
sizeof(config_key) - _len);
|
||||
if (config_get_int(config, config_key, &tmp_int))
|
||||
config_pid = (uint16_t)tmp_int;
|
||||
|
||||
_len = strlcpy(config_key, "input_device",
|
||||
sizeof(config_key));
|
||||
strlcpy(config_key + _len, config_key_postfix,
|
||||
sizeof(config_key) - _len);
|
||||
if ( (entry = config_get_entry(config, config_key))
|
||||
&& (entry->value))
|
||||
config_device = entry->value;
|
||||
|
||||
_len = strlcpy(config_key, "input_phys",
|
||||
sizeof(config_key));
|
||||
strlcpy(config_key + _len, config_key_postfix,
|
||||
sizeof(config_key) - _len);
|
||||
if ( (entry = config_get_entry(config, config_key))
|
||||
&& (entry->value))
|
||||
config_phys = entry->value;
|
||||
|
||||
/* Absent alternatives are not stored */
|
||||
if ( (i > 0)
|
||||
&& (config_vid == 0)
|
||||
&& (config_pid == 0)
|
||||
&& (*config_device == '\0'))
|
||||
continue;
|
||||
|
||||
snprintf(index_key, sizeof(index_key), "i%u_%d", file_idx, i);
|
||||
/* Device names and physical locations come from the file and
|
||||
* are not length-bounded, so the composed tuple can overflow
|
||||
* this buffer. A truncated tuple must never be stored: the
|
||||
* verify step only catches an index that *overstates* a
|
||||
* candidate (real < claimed), and truncation can just as
|
||||
* easily understate one - a clipped physical location can
|
||||
* drop the +10 phys bonus, and a clipped separator makes the
|
||||
* parser skip the tuple entirely, hiding the profile from
|
||||
* ranking. Either way the index could rank the true winner
|
||||
* below a rival whose own claim is honest, so the rival would
|
||||
* verify successfully and be selected where a full scan would
|
||||
* not have chosen it. Abandon the whole index for this
|
||||
* directory instead; connects then scan in full, exactly as
|
||||
* they did before this mechanism existed. */
|
||||
if ((size_t)snprintf(index_val, sizeof(index_val),
|
||||
"%u\t%u\t%s\t%s",
|
||||
(unsigned)config_vid, (unsigned)config_pid,
|
||||
config_device, config_phys) >= sizeof(index_val))
|
||||
{
|
||||
autoconfig_handle->index_build_abandoned = 1;
|
||||
config_file_free(autoconfig_handle->index_build);
|
||||
autoconfig_handle->index_build = NULL;
|
||||
autoconfig_handle->index_build_count = 0;
|
||||
return;
|
||||
}
|
||||
config_set_string(autoconfig_handle->index_build, index_key,
|
||||
index_val);
|
||||
}
|
||||
|
||||
autoconfig_handle->index_build_count++;
|
||||
}
|
||||
|
||||
/* Write the accumulated index beside the profiles. Failure (read
|
||||
* only directory, out of space) is ignored: the index is an
|
||||
* optimisation, and without one every connect scans as before. */
|
||||
static void input_autoconfigure_index_write(
|
||||
autoconfig_handle_t *autoconfig_handle, const char *dir)
|
||||
{
|
||||
char index_path[PATH_MAX_LENGTH];
|
||||
char index_val[32];
|
||||
config_file_t *index_build = autoconfig_handle->index_build;
|
||||
|
||||
if (!index_build)
|
||||
return;
|
||||
|
||||
snprintf(index_val, sizeof(index_val), "%d",
|
||||
AUTOCONFIG_INDEX_VERSION);
|
||||
config_set_string(index_build, "__version", index_val);
|
||||
snprintf(index_val, sizeof(index_val), "%u",
|
||||
autoconfig_handle->index_build_count);
|
||||
config_set_string(index_build, "__file_count", index_val);
|
||||
|
||||
fill_pathname_join_special(index_path, dir,
|
||||
AUTOCONFIG_INDEX_NAME, sizeof(index_path));
|
||||
index_build->flags |= CONF_FILE_FLG_MODIFIED;
|
||||
config_file_write(index_build, index_path, false);
|
||||
|
||||
config_file_free(index_build);
|
||||
autoconfig_handle->index_build = NULL;
|
||||
autoconfig_handle->index_build_count = 0;
|
||||
}
|
||||
|
||||
/* Try to resolve the connect from the directory's index. On a
|
||||
* verified hit, returns the winning profile's parsed config with
|
||||
* *affinity set to its (re-verified) affinity. Returns NULL on any
|
||||
* miss or doubt, and the caller performs the full scan. */
|
||||
static config_file_t *input_autoconfigure_index_try(
|
||||
autoconfig_handle_t *autoconfig_handle, const char *dir,
|
||||
unsigned *affinity)
|
||||
{
|
||||
char index_path[PATH_MAX_LENGTH];
|
||||
config_file_t *index_conf = NULL;
|
||||
config_file_t *winner = NULL;
|
||||
int file_count = 0;
|
||||
int version = 0;
|
||||
int best_file = -1;
|
||||
unsigned best_affinity = 0;
|
||||
unsigned fi;
|
||||
|
||||
fill_pathname_join_special(index_path, dir,
|
||||
AUTOCONFIG_INDEX_NAME, sizeof(index_path));
|
||||
if (!(index_conf = config_file_new(index_path)))
|
||||
return NULL;
|
||||
|
||||
/* Header and freshness */
|
||||
if ( !config_get_int(index_conf, "__version", &version)
|
||||
|| (version != AUTOCONFIG_INDEX_VERSION)
|
||||
|| !config_get_int(index_conf, "__file_count", &file_count)
|
||||
|| (file_count <= 0)
|
||||
|| (file_count !=
|
||||
input_autoconfigure_index_dir_count(dir)))
|
||||
{
|
||||
config_file_free(index_conf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Rank every recorded tuple with the real affinity function */
|
||||
for (fi = 0; fi < (unsigned)file_count; fi++)
|
||||
{
|
||||
int i;
|
||||
char index_key[32];
|
||||
for (i = 0; i < 10; i++)
|
||||
{
|
||||
struct config_entry_list *entry;
|
||||
const char *p;
|
||||
char *endp;
|
||||
char config_device[NAME_MAX_LENGTH];
|
||||
unsigned config_vid, config_pid, a;
|
||||
const char *config_phys;
|
||||
size_t device_len;
|
||||
|
||||
snprintf(index_key, sizeof(index_key), "i%u_%d", fi, i);
|
||||
if ( !(entry = config_get_entry(index_conf, index_key))
|
||||
|| !entry->value)
|
||||
continue;
|
||||
|
||||
/* vid \t pid \t device \t phys */
|
||||
p = entry->value;
|
||||
config_vid = (unsigned)strtoul(p, &endp, 10);
|
||||
if (*endp != '\t')
|
||||
continue;
|
||||
p = endp + 1;
|
||||
config_pid = (unsigned)strtoul(p, &endp, 10);
|
||||
if (*endp != '\t')
|
||||
continue;
|
||||
p = endp + 1;
|
||||
if (!(endp = strchr(p, '\t')))
|
||||
continue;
|
||||
device_len = (size_t)(endp - p);
|
||||
if (device_len >= sizeof(config_device))
|
||||
device_len = sizeof(config_device) - 1;
|
||||
memcpy(config_device, p, device_len);
|
||||
config_device[device_len] = '\0';
|
||||
config_phys = endp + 1;
|
||||
|
||||
a = input_autoconfigure_tuple_affinity(autoconfig_handle,
|
||||
(uint16_t)config_vid, (uint16_t)config_pid,
|
||||
config_device, config_phys, i);
|
||||
if (a > best_affinity)
|
||||
{
|
||||
best_affinity = a;
|
||||
best_file = (int)fi;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* The same bar a scan match needs; below it, scan in full (this
|
||||
* is also what finds a profile hand-edited to newly match).
|
||||
* The header was valid and current, so flag it: if the scan
|
||||
* agrees nothing matches, the index needs no rewrite. */
|
||||
if ((best_affinity < 20) || (best_file < 0))
|
||||
{
|
||||
autoconfig_handle->index_fresh_no_candidate = 1;
|
||||
config_file_free(index_conf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Open only the winner and verify the claim against the file */
|
||||
{
|
||||
char index_key[32];
|
||||
char profile_path[PATH_MAX_LENGTH];
|
||||
struct config_entry_list *entry;
|
||||
int64_t f_size = 0;
|
||||
int64_t f_len = 0;
|
||||
char *f_buf = NULL;
|
||||
unsigned real_affinity;
|
||||
|
||||
snprintf(index_key, sizeof(index_key), "f%d", best_file);
|
||||
if ( !(entry = config_get_entry(index_conf, index_key))
|
||||
|| !entry->value
|
||||
|| !*entry->value)
|
||||
{
|
||||
config_file_free(index_conf);
|
||||
return NULL;
|
||||
}
|
||||
fill_pathname_join_special(profile_path, dir, entry->value,
|
||||
sizeof(profile_path));
|
||||
|
||||
snprintf(index_key, sizeof(index_key), "s%d", best_file);
|
||||
{
|
||||
int tmp_int = 0;
|
||||
if (config_get_int(index_conf, index_key, &tmp_int))
|
||||
f_size = (int64_t)tmp_int;
|
||||
}
|
||||
config_file_free(index_conf);
|
||||
|
||||
if ( !filestream_read_file(profile_path,
|
||||
(void**)&f_buf, &f_len)
|
||||
|| !f_buf)
|
||||
return NULL;
|
||||
|
||||
/* Size drift: the file changed since indexing */
|
||||
if ((f_size > 0) && (f_len != f_size))
|
||||
{
|
||||
free(f_buf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!(winner = config_file_new_take_string(f_buf, 0,
|
||||
profile_path)))
|
||||
return NULL;
|
||||
|
||||
real_affinity = input_autoconfigure_get_config_file_affinity(
|
||||
autoconfig_handle, winner);
|
||||
if (real_affinity < best_affinity)
|
||||
{
|
||||
/* The index lied (in-place edit): scan in full instead */
|
||||
config_file_free(winner);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
*affinity = real_affinity;
|
||||
return winner;
|
||||
}
|
||||
}
|
||||
|
||||
/* How many directory entries one tick will look at.
|
||||
@@ -398,8 +749,32 @@ static bool input_autoconfigure_scan_config_files_external(
|
||||
|
||||
if (!autoconfig_handle->scan_rdir)
|
||||
{
|
||||
if (!(autoconfig_handle->scan_rdir = retro_opendir(
|
||||
dirs[autoconfig_handle->scan_dir_idx])))
|
||||
const char *dir = dirs[autoconfig_handle->scan_dir_idx];
|
||||
|
||||
/* Entering a new directory: try its index first. A
|
||||
* verified hit answers the connect with two file
|
||||
* operations instead of one open per profile. Any miss
|
||||
* or doubt falls through to the full walk below, which
|
||||
* rebuilds the index as it goes. */
|
||||
budget--;
|
||||
autoconfig_handle->index_fresh_no_candidate = 0;
|
||||
autoconfig_handle->index_build_abandoned = 0;
|
||||
autoconfig_handle->scan_dir_start_affinity =
|
||||
autoconfig_handle->scan_max_affinity;
|
||||
if ((config = input_autoconfigure_index_try(
|
||||
autoconfig_handle, dir, &affinity)))
|
||||
{
|
||||
if (autoconfig_handle->scan_best)
|
||||
config_file_free(autoconfig_handle->scan_best);
|
||||
autoconfig_handle->scan_best = config;
|
||||
autoconfig_handle->scan_max_affinity = affinity;
|
||||
/* Same policy as a scan match: a directory that
|
||||
* produced one ends the search. */
|
||||
autoconfig_handle->scan_dir_idx = num_dirs;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!(autoconfig_handle->scan_rdir = retro_opendir(dir)))
|
||||
{
|
||||
autoconfig_handle->scan_dir_idx++;
|
||||
continue;
|
||||
@@ -410,6 +785,27 @@ static bool input_autoconfigure_scan_config_files_external(
|
||||
{
|
||||
retro_closedir(autoconfig_handle->scan_rdir);
|
||||
autoconfig_handle->scan_rdir = NULL;
|
||||
/* The walk read and parsed every profile: write the
|
||||
* rebuilt index beside them (silently a no-op on a read
|
||||
* only directory) - unless a fresh index already said,
|
||||
* correctly, that nothing here matches this device, in
|
||||
* which case the rebuilt one is identical and the write
|
||||
* is skipped. A scan that *contradicts* a fresh index
|
||||
* (a profile was hand-edited in place to match) does
|
||||
* write, healing the staleness the header cannot see. */
|
||||
if ( !autoconfig_handle->index_fresh_no_candidate
|
||||
|| ( (autoconfig_handle->scan_max_affinity >=
|
||||
20)
|
||||
&& (autoconfig_handle->scan_max_affinity >
|
||||
autoconfig_handle->scan_dir_start_affinity)))
|
||||
input_autoconfigure_index_write(autoconfig_handle,
|
||||
dirs[autoconfig_handle->scan_dir_idx]);
|
||||
else if (autoconfig_handle->index_build)
|
||||
{
|
||||
config_file_free(autoconfig_handle->index_build);
|
||||
autoconfig_handle->index_build = NULL;
|
||||
autoconfig_handle->index_build_count = 0;
|
||||
}
|
||||
autoconfig_handle->scan_dir_idx++;
|
||||
/* A directory that produced a match ends the search; the
|
||||
* later directory is only a fallback. */
|
||||
@@ -430,43 +826,30 @@ static bool input_autoconfigure_scan_config_files_external(
|
||||
dirs[autoconfig_handle->scan_dir_idx], entry_name,
|
||||
sizeof(config_file_path));
|
||||
|
||||
/* Read once, and only parse what could win. The Bliss-Box path
|
||||
* rewrites the product id before comparing, so its devices skip
|
||||
* the filter and are parsed as before. */
|
||||
/* Read and parse every profile. An earlier revision skipped
|
||||
* parsing files a raw-buffer prefilter rejected; the index
|
||||
* rebuild needs every file's identity tuples regardless, the
|
||||
* bytes are already in memory either way, and a parse costs
|
||||
* microseconds against the milliseconds the read itself costs
|
||||
* cold. Ranking parsed values also retires the prefilter's
|
||||
* substring heuristics, which were a standing source of
|
||||
* false-negative encodings. */
|
||||
{
|
||||
int64_t buf_len = 0;
|
||||
char *buf = NULL;
|
||||
bool candidate;
|
||||
|
||||
if (!filestream_read_file(config_file_path,
|
||||
(void**)&buf, &buf_len) || !buf)
|
||||
continue;
|
||||
|
||||
#ifdef HAVE_BLISSBOX
|
||||
candidate = (autoconfig_handle->device_info.vid == BLISSBOX_VID)
|
||||
|| input_autoconfigure_file_may_match(buf,
|
||||
autoconfig_handle->device_info.name,
|
||||
autoconfig_handle->device_info.vid,
|
||||
autoconfig_handle->device_info.pid);
|
||||
#else
|
||||
candidate = input_autoconfigure_file_may_match(buf,
|
||||
autoconfig_handle->device_info.name,
|
||||
autoconfig_handle->device_info.vid,
|
||||
autoconfig_handle->device_info.pid);
|
||||
#endif
|
||||
if (!candidate)
|
||||
{
|
||||
free(buf);
|
||||
if (!(config = config_file_new_take_string(buf, 0,
|
||||
config_file_path)))
|
||||
continue;
|
||||
}
|
||||
|
||||
config = config_file_new_from_string(buf, config_file_path);
|
||||
free(buf);
|
||||
input_autoconfigure_index_collect(autoconfig_handle,
|
||||
entry_name, buf_len, config);
|
||||
}
|
||||
|
||||
if (!config)
|
||||
continue;
|
||||
|
||||
affinity = input_autoconfigure_get_config_file_affinity(
|
||||
autoconfig_handle, config);
|
||||
|
||||
@@ -479,12 +862,22 @@ static bool input_autoconfigure_scan_config_files_external(
|
||||
config = NULL;
|
||||
autoconfig_handle->scan_max_affinity = affinity;
|
||||
|
||||
/* A vendor, product and physical location match is as good as
|
||||
* it gets; nothing later can beat it. */
|
||||
/* A vendor, product and physical location match is as good
|
||||
* as it gets; nothing later can beat it. The partial
|
||||
* tuple collection is discarded rather than written - a
|
||||
* device this profile fits will hit the >= 60 early exit
|
||||
* with or without an index, so rebuilding is left to a
|
||||
* connect that walks the whole directory. */
|
||||
if (affinity >= 60)
|
||||
{
|
||||
retro_closedir(autoconfig_handle->scan_rdir);
|
||||
autoconfig_handle->scan_rdir = NULL;
|
||||
if (autoconfig_handle->index_build)
|
||||
{
|
||||
config_file_free(autoconfig_handle->index_build);
|
||||
autoconfig_handle->index_build = NULL;
|
||||
autoconfig_handle->index_build_count = 0;
|
||||
}
|
||||
autoconfig_handle->scan_dir_idx = num_dirs;
|
||||
break;
|
||||
}
|
||||
@@ -539,13 +932,12 @@ static bool input_autoconfigure_scan_config_files_internal(
|
||||
if (!input_builtin_autoconfs[i] || !*input_builtin_autoconfs[i])
|
||||
continue;
|
||||
|
||||
/* Load autoconfig string */
|
||||
/* Load autoconfig string. The strdup stays (the builtin is
|
||||
* const and the parse mutates), but the copy is handed over:
|
||||
* the conf adopts it and entries borrow from it. */
|
||||
autoconfig_str = strdup(input_builtin_autoconfs[i]);
|
||||
config = config_file_new_from_string(
|
||||
autoconfig_str, NULL);
|
||||
|
||||
/* > String no longer required - clean up */
|
||||
free(autoconfig_str);
|
||||
config = config_file_new_take_string(
|
||||
autoconfig_str, 0, NULL);
|
||||
autoconfig_str = NULL;
|
||||
|
||||
/* Check for a match */
|
||||
|
||||
@@ -1223,8 +1223,10 @@ bool task_push_overlay_load_default(
|
||||
return false;
|
||||
}
|
||||
str[buf_len] = '\0';
|
||||
conf = config_file_new_from_string(str, overlay_path);
|
||||
free(str);
|
||||
/* Hand the buffer over: the conf adopts it and entries
|
||||
* borrow from it - no per-entry copies. */
|
||||
conf = config_file_new_take_string(str, (size_t)buf_len,
|
||||
overlay_path);
|
||||
}
|
||||
if (conf)
|
||||
RARCH_DBG("[Overlay] Config parsed successfully from archive.\n");
|
||||
|
||||
Executable
+182
@@ -0,0 +1,182 @@
|
||||
#!/bin/bash
|
||||
# Regression tests for gamepad autoconfiguration (task_autodetect),
|
||||
# driven end to end through the real binary with the test input
|
||||
# driver and the .ratst step scripts.
|
||||
#
|
||||
# Usage: tests-other/test_autoconf.sh [path-to-retroarch]
|
||||
#
|
||||
# Requires a build with HAVE_TEST_DRIVERS (the default). Runs
|
||||
# headless under timeout(1).
|
||||
#
|
||||
# Assertions match on device+port *fragments* rather than whole log
|
||||
# lines on purpose: autoconfig runs as asynchronous tasks and the
|
||||
# log is written from more than one thread, so an occasional line
|
||||
# arrives with another thread's fragment spliced into it. That
|
||||
# interleaving is cosmetic - the message content is intact - so a
|
||||
# fragment grep is both correct and robust, where a whole-line
|
||||
# grep -c is flaky. A run is also repeated a few times where a
|
||||
# cold-vs-warm (index absent vs present) difference is being
|
||||
# checked, so a one-off splice cannot pass or fail a phase alone.
|
||||
#
|
||||
# Any sanitizer report in any phase fails the suite, so running
|
||||
# this against an ASan/UBSan build extends the coverage for free.
|
||||
|
||||
set -u
|
||||
|
||||
RETROARCH="${1:-./retroarch}"
|
||||
RATST_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
WORK="$(mktemp -d "${TMPDIR:-/tmp}/ratest.XXXXXX")"
|
||||
HOME_DIR="$WORK/home"
|
||||
AUTOCONF="$WORK/autoconf"
|
||||
LOG="$WORK/run.log"
|
||||
INDEX="$AUTOCONF/.autoconfig_index"
|
||||
FAILED=0
|
||||
|
||||
say() { printf '%s\n' "$*"; }
|
||||
pass() { say "[PASS] $*"; }
|
||||
fail() { say "[FAIL] $*"; FAILED=1; }
|
||||
|
||||
# assert_seen <pattern> <description>: pattern present at least once
|
||||
assert_seen() {
|
||||
if grep -qE "$1" "$LOG"; then pass "$2"
|
||||
else fail "$2 (missing /$1/)"; fi
|
||||
}
|
||||
assert_absent() {
|
||||
if grep -qE "$1" "$LOG"; then fail "$2 (found /$1/)"
|
||||
else pass "$2"; fi
|
||||
}
|
||||
assert_clean() {
|
||||
if grep -qE "ERROR: (Address|Leak|Undefined|Thread)Sanitizer|runtime error:|Segmentation fault" "$LOG"; then
|
||||
fail "sanitizer/crash output in log"
|
||||
grep -E "ERROR: (Address|Leak|Undefined|Thread)Sanitizer|runtime error:" "$LOG" | head -3
|
||||
fi
|
||||
}
|
||||
|
||||
setup() {
|
||||
rm -rf "$HOME_DIR" "$AUTOCONF"
|
||||
mkdir -p "$HOME_DIR/.config/retroarch" "$AUTOCONF"
|
||||
cp "$RATST_DIR"/autoconf/*.cfg "$AUTOCONF/"
|
||||
cat > "$HOME_DIR/.config/retroarch/retroarch.cfg" <<CFG
|
||||
input_driver = "test"
|
||||
input_joypad_driver = "test"
|
||||
joypad_autoconfig_dir = "$AUTOCONF"
|
||||
test_input_file_joypad = "$RATST_DIR/$1"
|
||||
video_driver = "null"
|
||||
audio_driver = "null"
|
||||
menu_driver = "null"
|
||||
network_cmd_enable = "false"
|
||||
CFG
|
||||
}
|
||||
|
||||
run() {
|
||||
# Pass the config explicitly rather than relying on HOME discovery:
|
||||
# the default search path varies with the environment (XDG base,
|
||||
# portable-mode marker, an existing user config), and CI showed a
|
||||
# run that found no config produces no [Autoconf] lines and fails
|
||||
# every assertion identically. --config is unambiguous. HOME is
|
||||
# still pinned so any writes land in the sandbox.
|
||||
HOME="$HOME_DIR" timeout 25 "$RETROARCH" \
|
||||
--config "$HOME_DIR/.config/retroarch/retroarch.cfg" \
|
||||
--verbose > "$LOG" 2>&1
|
||||
assert_clean
|
||||
# Guard against a silent no-op: if the run produced no autoconfig
|
||||
# activity at all, every content assertion below would fail with
|
||||
# the same misleading 'missing pattern' - surface the real cause.
|
||||
if ! grep -qE '\[Autoconf\]' "$LOG"; then
|
||||
fail "run produced no [Autoconf] output (binary ran? config loaded? test drivers built?)"
|
||||
tail -5 "$LOG" | sed 's/^/ /'
|
||||
fi
|
||||
}
|
||||
|
||||
index_hash() { md5sum "$INDEX" 2>/dev/null | cut -d' ' -f1; }
|
||||
|
||||
say "== autoconf regression suite: $RETROARCH"
|
||||
[ -x "$RETROARCH" ] || { say "binary not found/executable"; exit 1; }
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
say "-- P1: fresh directory - full scan, index build, full cycle"
|
||||
setup test_input_autoconf_cycle.ratst
|
||||
[ ! -e "$INDEX" ] || fail "index present before first run"
|
||||
run
|
||||
# Device A (0001:0002) is won by TestpadD's vid/pid tuple (aff 30)
|
||||
# over TestpadA's name-only 20, and displays as 'device name D' -
|
||||
# asserting this pins vid/pid-tuple ranking above name ranking.
|
||||
assert_seen 'device name D configured in port 1' "P1 connect: vid/pid winner on port 1"
|
||||
assert_seen 'device B configured in port 2' "P1 connect: name match on port 2"
|
||||
# Disconnect assertions check the port, not the device name. The
|
||||
# notice carries the port's stored display name when the connect task
|
||||
# had already applied it, the raw driver-reported name when it had
|
||||
# not, and a "not available" placeholder when neither is set - see the
|
||||
# fallbacks in input_autoconfigure_disconnect. Which one appears
|
||||
# depends on connect/disconnect task ordering, so pinning the name
|
||||
# pins a race. Device identity is asserted on the connect side above,
|
||||
# where it is stable and where it actually exercises profile matching.
|
||||
assert_seen 'disconnected from port 1' "P1 disconnect clears port 1"
|
||||
assert_seen 'device C configured in port 1' "P1 reconnect: different device reconfigures port 1"
|
||||
assert_seen 'disconnected from port 2' "P1 disconnect clears port 2"
|
||||
[ -s "$INDEX" ] && pass "P1 index written" || fail "P1 index missing/empty"
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
say "-- P2: warm rerun - index present, same outcome, index stable"
|
||||
H_before="$(index_hash)"
|
||||
run
|
||||
assert_seen 'device name D configured in port 1' "P2 vid/pid winner via index"
|
||||
assert_seen 'device B configured in port 2' "P2 name match via index"
|
||||
[ "$H_before" = "$(index_hash)" ] && pass "P2 index untouched by index-hit connects" \
|
||||
|| fail "P2 index rewritten on pure hits"
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
say "-- P3: in-place edit of the winner's vid/pid - verify catch"
|
||||
# Break the actual winning signal: TestpadD wins device A only on its
|
||||
# vid/pid tuple. Zeroing the product id in place (file count
|
||||
# unchanged, so the freshness header cannot notice) must be caught by
|
||||
# the winner re-score, and the scan must then fall back to TestpadA's
|
||||
# name match - which displays as the plain device name 'A'.
|
||||
setup test_input_autoconf_cycle.ratst
|
||||
run # build a fresh index
|
||||
H2="$(index_hash)"
|
||||
sed -i 's/input_product_id = 2/input_product_id = 0/' "$AUTOCONF/TestpadD_alternative.cfg"
|
||||
run
|
||||
assert_seen 'device A configured in port 1' "P3 fallback: name match after winner's vid/pid broken"
|
||||
assert_absent 'device name D configured in port 1' "P3 stale vid/pid winner not selected"
|
||||
[ "$H2" != "$(index_hash)" ] && pass "P3 index healed after verify caught the edit" \
|
||||
|| fail "P3 index not rewritten after verify failure"
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
say "-- P4: profile added - file-count freshness invalidates"
|
||||
setup test_input_autoconf_cycle.ratst
|
||||
run # fresh index
|
||||
cp "$AUTOCONF/TestpadA.cfg" "$AUTOCONF/zz_added.cfg"
|
||||
H3="$(index_hash)"
|
||||
run
|
||||
assert_seen 'device name D configured in port 1' "P4 count-stale index falls back; scan finds winner"
|
||||
[ "$H3" != "$(index_hash)" ] && pass "P4 index rebuilt at new file count" \
|
||||
|| fail "P4 index not rebuilt after count change"
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
say "-- P5: winning profile deleted - freshness + fallback recover"
|
||||
setup test_input_autoconf_cycle.ratst
|
||||
run # fresh index
|
||||
rm "$AUTOCONF/TestpadD_alternative.cfg"
|
||||
run
|
||||
assert_seen 'device A configured in port 1' "P5 deletion caught; name match selected"
|
||||
assert_absent 'device name D configured in port 1' "P5 deleted winner not selected"
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
say "-- P6: unrecognised device - full scan, no index churn"
|
||||
setup test_input_autoconf_unknown.ratst
|
||||
run # first run builds index
|
||||
assert_seen 'not configured' "P6 unknown device reported unconfigured"
|
||||
H6="$(index_hash)"
|
||||
run
|
||||
assert_seen 'not configured' "P6 unknown device unconfigured on rerun"
|
||||
[ -n "$H6" ] && [ "$H6" = "$(index_hash)" ] \
|
||||
&& pass "P6 identical index not rewritten for a no-match device" \
|
||||
|| fail "P6 index churned on an unrecognised device"
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
rm -rf "$WORK"
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
say "== autoconf regression suite: ALL PASS"; exit 0
|
||||
fi
|
||||
say "== autoconf regression suite: FAILURES"; exit 1
|
||||
@@ -0,0 +1,31 @@
|
||||
[
|
||||
{
|
||||
"comment": "Autoconf regression cycle. Device A (0001:0002) is won by TestpadD_alternative on its vid/pid tuple (affinity 30) over TestpadA name-only 20, displaying as 'device name D'; the disconnect notice uses the raw name 'device A'. Device B (0003:0004) matches TestpadB by name. Sequence: connect A and B, disconnect A from port 1, connect device C to port 1 (a DIFFERENT device, so no same-device reconnect-notification suppression - the reconfigure line is deterministic), then disconnect B. Frames stay small: the null video step clock advances slowly, so steps must sit low enough to be reached in the time budget.",
|
||||
"action": 1,
|
||||
"param_num": 0,
|
||||
"param_str": "(0001:0002) Test joypad device A",
|
||||
"frame": 0
|
||||
},
|
||||
{
|
||||
"action": 1,
|
||||
"param_num": 1,
|
||||
"param_str": "(0003:0004) Test joypad device B",
|
||||
"frame": 30
|
||||
},
|
||||
{
|
||||
"action": 2,
|
||||
"param_num": 0,
|
||||
"frame": 90
|
||||
},
|
||||
{
|
||||
"action": 1,
|
||||
"param_num": 0,
|
||||
"param_str": "(0005:0006) Test joypad device C",
|
||||
"frame": 150
|
||||
},
|
||||
{
|
||||
"action": 2,
|
||||
"param_num": 1,
|
||||
"frame": 210
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"comment": "A device no profile matches: the connect must fall back to a full scan and report unconfigured - and a fresh index that already (correctly) said so must not be rewritten with an identical copy on every such connect.",
|
||||
"action": 1,
|
||||
"param_num": 0,
|
||||
"param_str": "(dead:beef) Completely Unknown Pad",
|
||||
"frame": 0
|
||||
}
|
||||
]
|
||||
Reference in new issue
Block a user