]> git.unchartedbackwaters.co.uk Git - francis/winuae.git/commitdiff
od-unix: persist host settings in winuae.ini
authorStefan Reinauer <stefan.reinauer@coreboot.org>
Fri, 12 Jun 2026 07:03:14 +0000 (00:03 -0700)
committerStefan Reinauer <stefan.reinauer@coreboot.org>
Wed, 17 Jun 2026 19:24:40 +0000 (12:24 -0700)
The Unix port had no equivalent of the Windows registry/winuae.ini
store: the Qt Paths page reverted to hardcoded defaults on every start
(GitHub issue #2).

Port the ini-file mode of the od-win32 registry abstraction into
od-unix/registry.cpp on top of the shared ini.cpp, with the same
registry.h API. The store resolves to winuae.ini next to the executable
when present (portable mode, matching Windows), otherwise
$XDG_CONFIG_HOME/winuae/winuae.ini on Linux and
~/Library/Application Support/WinUAE/winuae.ini on macOS; the
WINUAE_INI environment variable overrides the location.

The Qt launcher loads the Paths page directories and flags from the
store and saves them when the dialog closes, through new host-setting
hooks on the provider struct. The core path fetchers consult the stored
values between per-config unix.*_path overrides and the built-in
defaults, so the configured directories also apply outside the GUI.
Settings flush on dialog close and at gui_exit.

Covered by winuae_unix_registry_test (value types, subtrees,
enumeration, deletion, and persistence across reopen).

CMakeLists.txt
README_unix.md
od-unix/config.cpp
od-unix/gui.cpp
od-unix/qt/launcher.cpp
od-unix/qt/launcher.h
od-unix/qt/launcher_bridge.cpp
od-unix/registry.cpp [new file with mode: 0644]
od-unix/registry.h [new file with mode: 0644]
od-unix/registry_test.cpp [new file with mode: 0644]

index 3ed0420dcb2761a22b869cbe886da75315bb1fd5..e2b3c4c2e70f49992d15f68cc3afbac54c9e7df2 100644 (file)
@@ -1389,6 +1389,7 @@ set(WINUAE_CORE_SOURCES
     od-unix/mman.cpp
     od-unix/path_expand.cpp
     od-unix/parallel.cpp
+    od-unix/registry.cpp
     od-unix/romscan.cpp
     od-unix/rtg.cpp
     od-unix/screenshot.cpp
@@ -1467,6 +1468,18 @@ target_include_directories(winuae_unix_path_expand_test PRIVATE
 )
 winuae_unix_register_test(winuae_unix_path_expand_test)
 
+add_executable(winuae_unix_registry_test EXCLUDE_FROM_ALL
+    od-unix/registry.cpp
+    od-unix/registry_test.cpp
+    ini.cpp
+    missing.cpp
+)
+target_include_directories(winuae_unix_registry_test PRIVATE
+    "${CMAKE_CURRENT_SOURCE_DIR}/od-unix"
+    "${CMAKE_CURRENT_SOURCE_DIR}/include"
+)
+winuae_unix_register_test(winuae_unix_registry_test)
+
 add_executable(winuae_unix_threading_test EXCLUDE_FROM_ALL
     od-unix/threading.cpp
     od-unix/threading_test.cpp
index c2933d0a2761ca1cab68ded42233519708fee8fc..5db82e1203bc16013d58970708e292945411db6e 100644 (file)
@@ -334,6 +334,15 @@ When the integrated Qt UI is built, `winuae_unix` opens the configuration UI by
 
 The executable also tries to load `default.uae` from the configuration path by default, and the Qt configuration UI starts with it loaded when it exists — save a configuration named `default` to set your startup defaults, like on Windows. Configs given on the command line take precedence. A missing default config is ignored silently; explicit `-config` / `-f` load failures are still reported.
 
+Host settings (the Paths page directories and flags) persist in `winuae.ini`:
+a `winuae.ini` next to the executable is used when present (portable mode,
+matching Windows); otherwise `$XDG_CONFIG_HOME/winuae/winuae.ini` on Linux
+(default `~/.config/winuae/winuae.ini`) and
+`~/Library/Application Support/WinUAE/winuae.ini` on macOS. The `WINUAE_INI`
+environment variable overrides the location. Configs loaded per session can
+still override individual paths through the `unix.*_path` settings, which win
+over the stored host defaults.
+
 Unix path expansion is supported for `~/`, `$VAR`, and `${VAR}` in core config paths and Qt file/config boundaries. Relative config and media paths are resolved against the process working directory, matching the non-relative Windows save mode; the Windows-style relative-path save option is not enabled on Unix yet. The Qt Paths page saves runtime-visible target path settings such as `unix.screenshot_path`, `unix.rip_path`, `unix.video_path`, and `statefile_path`; legacy `unix.ui.*` path settings are still accepted when loading older configs. `~user` expansion is not implemented; use an absolute path for another user's home directory.
 
 For SANA-II `uaenet.device` startup testing, add `-s sana2=true` or use the smoke target below. Guest TCP/IP stack validation is still pending.
index 40d51ca835b81094df37a828111026d118d3c250..b8e529695b024b7e04f52dab4eab7d491d9e06c8 100644 (file)
@@ -13,6 +13,7 @@ extern int video_recording_active;
 static int unix_avi_audio_codec = AVIAUDIO_AVI;
 #endif
 #include "path_expand.h"
+#include "registry.h"
 #include "romscan.h"
 #include "savestate.h"
 #include "sound_unix.h"
@@ -770,27 +771,38 @@ static void fetch_user_data_path(TCHAR *out, int size, const char *subdir)
     fixtrailing(out);
 }
 
-static void fetch_user_data_path_override(TCHAR *out, int size, const TCHAR *override_path, const char *subdir)
+static void fetch_user_data_path_override(TCHAR *out, int size, const TCHAR *override_path, const char *regkey, const char *subdir)
 {
     if (override_path && override_path[0]) {
         uae_tcslcpy(out, override_path, size);
         fixtrailing(out);
-    } else {
-        fetch_user_data_path(out, size, subdir);
+        return;
+    }
+    /* Per-config overrides win; otherwise consult the persistent host
+     * settings (winuae.ini) before falling back to built-in defaults. */
+    if (regkey) {
+        TCHAR stored[MAX_DPATH];
+        int stored_size = MAX_DPATH;
+        if (regquerystr(NULL, regkey, stored, &stored_size) && stored[0]) {
+            target_expand_environment(stored, out, size);
+            fixtrailing(out);
+            return;
+        }
     }
+    fetch_user_data_path(out, size, subdir);
 }
 
-void fetch_saveimagepath(TCHAR *out, int size, int) { fetch_user_data_path_override(out, size, path_saveimage, "SaveImages"); }
-void fetch_configurationpath(TCHAR *out, int size) { fetch_user_data_path_override(out, size, path_configuration, "Configuration"); }
-void fetch_nvrampath(TCHAR *out, int size) { fetch_user_data_path_override(out, size, path_nvram, "NVRAMs"); }
+void fetch_saveimagepath(TCHAR *out, int size, int) { fetch_user_data_path_override(out, size, path_saveimage, "SaveimagePath", "SaveImages"); }
+void fetch_configurationpath(TCHAR *out, int size) { fetch_user_data_path_override(out, size, path_configuration, "ConfigurationPath", "Configuration"); }
+void fetch_nvrampath(TCHAR *out, int size) { fetch_user_data_path_override(out, size, path_nvram, "NVRAMPath", "NVRAMs"); }
 void fetch_luapath(TCHAR *out, int size) { fetch_home_path(out, size); }
-void fetch_screenshotpath(TCHAR *out, int size) { fetch_user_data_path_override(out, size, path_screenshot, "Screenshots"); }
-void fetch_ripperpath(TCHAR *out, int size) { fetch_user_data_path_override(out, size, path_ripper, "Rips"); }
-void fetch_statefilepath(TCHAR *out, int size) { fetch_user_data_path_override(out, size, path_statefile, "Save States"); }
+void fetch_screenshotpath(TCHAR *out, int size) { fetch_user_data_path_override(out, size, path_screenshot, "ScreenshotPath", "Screenshots"); }
+void fetch_ripperpath(TCHAR *out, int size) { fetch_user_data_path_override(out, size, path_ripper, "RipperPath", "Rips"); }
+void fetch_statefilepath(TCHAR *out, int size) { fetch_user_data_path_override(out, size, path_statefile, "StatefilePath", "Save States"); }
 void fetch_inputfilepath(TCHAR *out, int size) { fetch_home_path(out, size); }
-void fetch_datapath(TCHAR *out, int size) { fetch_user_data_path_override(out, size, path_data, NULL); }
-void fetch_rompath(TCHAR *out, int size) { fetch_user_data_path_override(out, size, path_rom, "Kickstarts"); }
-void fetch_videopath(TCHAR *out, int size) { fetch_user_data_path_override(out, size, path_video, "Videos"); }
+void fetch_datapath(TCHAR *out, int size) { fetch_user_data_path_override(out, size, path_data, "DataPath", NULL); }
+void fetch_rompath(TCHAR *out, int size) { fetch_user_data_path_override(out, size, path_rom, "KickstartPath", "Kickstarts"); }
+void fetch_videopath(TCHAR *out, int size) { fetch_user_data_path_override(out, size, path_video, "VideoPath", "Videos"); }
 
 void target_getdate(int *y, int *m, int *d)
 {
index aef839ee7f308ed4934377bd54ba2203bde4ef84..3a197558710c6920fee58f050c601d04891d5ade 100644 (file)
@@ -11,6 +11,7 @@
 #include "custom.h"
 #include "inputdevice.h"
 #include "gui.h"
+#include "registry.h"
 #include "target.h"
 #include "target_main.h"
 #include "savestate.h"
@@ -69,7 +70,10 @@ int gui_init(void)
 #endif
 }
 int gui_update(void) { return 1; }
-void gui_exit(void) {}
+void gui_exit(void)
+{
+    registry_flush();
+}
 void gui_led(int led, int on, int brightness)
 {
     if (on >= 0 && led >= 0 && led < int(sizeof(gui_ledstate) * 8)) {
index 79b79e12e6dd9c72247937334c454e4e9e5f4528..602e34a1113597cf783e2d83eeb3dcd53019c274 100644 (file)
@@ -13336,6 +13336,9 @@ private:
         relativePaths->setChecked(false);
         portableMode->setChecked(false);
         pathDefaultType->setCurrentText(QStringLiteral("User data directory"));
+        /* Paths and path flags are host settings: restore the persisted
+         * values instead of the factory defaults. */
+        loadHostSettings();
         logSelect->setCurrentText(QStringLiteral("winuaebootlog.txt"));
         fullLogging->setChecked(false);
         logWindow->setChecked(false);
@@ -13352,6 +13355,85 @@ private:
         status->setText(QStringLiteral("Ready"));
     }
 
+    struct HostPathBinding {
+        const char *key;
+        QLineEdit *edit;
+    };
+
+    QVector<HostPathBinding> hostPathBindings() const
+    {
+        return {
+            { "KickstartPath", romsPath },
+            { "ConfigurationPath", configsPath },
+            { "NVRAMPath", nvramPath },
+            { "ScreenshotPath", screenshotsPath },
+            { "StatefilePath", stateFilesPath },
+            { "VideoPath", videosPath },
+            { "SaveimagePath", saveImagesPath },
+            { "RipperPath", ripsPath },
+            { "DataPath", dataPath },
+        };
+    }
+
+    struct HostFlagBinding {
+        const char *key;
+        QCheckBox *box;
+    };
+
+    QVector<HostFlagBinding> hostFlagBindings() const
+    {
+        return {
+            { "RecursiveROMScan", recursiveRoms },
+            { "ConfigurationCache", cacheConfigurations },
+            { "ArtCache", cacheBoxArt },
+            { "SaveImageOriginalPath", saveImageOriginalPath },
+        };
+    }
+
+    void loadHostSettings()
+    {
+        if (!hardwareProvider.hostSettingGet) {
+            return;
+        }
+        char value[4096];
+        for (const HostPathBinding &binding : hostPathBindings()) {
+            if (binding.edit && hardwareProvider.hostSettingGet(hardwareProvider.context, binding.key, value, sizeof value)) {
+                binding.edit->setText(QString::fromLocal8Bit(value));
+            }
+        }
+        for (const HostFlagBinding &binding : hostFlagBindings()) {
+            if (binding.box && hardwareProvider.hostSettingGet(hardwareProvider.context, binding.key, value, sizeof value)) {
+                binding.box->setChecked(QString::fromLocal8Bit(value).toInt() != 0);
+            }
+        }
+    }
+
+    void saveHostSettings()
+    {
+        if (!hardwareProvider.hostSettingSet) {
+            return;
+        }
+        for (const HostPathBinding &binding : hostPathBindings()) {
+            if (binding.edit) {
+                hardwareProvider.hostSettingSet(hardwareProvider.context, binding.key, binding.edit->text().toLocal8Bit().constData());
+            }
+        }
+        for (const HostFlagBinding &binding : hostFlagBindings()) {
+            if (binding.box) {
+                hardwareProvider.hostSettingSet(hardwareProvider.context, binding.key, binding.box->isChecked() ? "1" : "0");
+            }
+        }
+        if (hardwareProvider.hostSettingsFlush) {
+            hardwareProvider.hostSettingsFlush(hardwareProvider.context);
+        }
+    }
+
+    void done(int result) override
+    {
+        saveHostSettings();
+        QDialog::done(result);
+    }
+
     void applyModelPreset(const QString &model)
     {
         if (model == QStringLiteral("A1200") || model == QStringLiteral("CD32")) {
index 2ceaae8e1ed30ca297b7c9e166ec4c2bdd628f60..381b304ee69276076b09f66135d6de8a369e39ae 100644 (file)
@@ -116,6 +116,9 @@ struct WinUaeQtBoardCatalog {
 
 struct WinUaeQtHardwareInfoProvider {
     void *context = nullptr;
+    bool (*hostSettingGet)(void *context, const char *key, char *out, int outLen) = nullptr;
+    void (*hostSettingSet)(void *context, const char *key, const char *value) = nullptr;
+    void (*hostSettingsFlush)(void *context) = nullptr;
     WinUaeQtBoardCatalog (*boardCatalog)(void *context) = nullptr;
     bool (*applyConfig)(void *context, const WinUaeQtConfig &config) = nullptr;
     QVector<WinUaeQtHardwareBoard> (*boards)(void *context) = nullptr;
index 92844d30564efb9d3a34adde104fde9d3418371b..3a18df98e87353078551d56166be37911d9eec6b 100644 (file)
@@ -29,6 +29,7 @@
 #include "zfile.h"
 #include "gfxboard.h"
 #include "ethernet.h"
+#include "registry.h"
 #ifdef PROWIZARD
 #include "moduleripper.h"
 #endif
@@ -480,10 +481,30 @@ static void bridgePollHostWindowEvents(void *)
     unix_host_check_quit();
 }
 
+static bool bridgeHostSettingGet(void *, const char *key, char *out, int outLen)
+{
+    int size = outLen;
+    out[0] = 0;
+    return regquerystr(nullptr, key, out, &size) != 0;
+}
+
+static void bridgeHostSettingSet(void *, const char *key, const char *value)
+{
+    regsetstr(nullptr, key, value);
+}
+
+static void bridgeHostSettingsFlush(void *)
+{
+    registry_flush();
+}
+
 static WinUaeQtHardwareInfoProvider bridgeHardwareProvider(struct uae_prefs *prefs, bool runtimeActions)
 {
     WinUaeQtHardwareInfoProvider provider;
     provider.context = prefs;
+    provider.hostSettingGet = bridgeHostSettingGet;
+    provider.hostSettingSet = bridgeHostSettingSet;
+    provider.hostSettingsFlush = bridgeHostSettingsFlush;
     provider.boardCatalog = bridgeBoardCatalog;
     provider.applyConfig = bridgeApplyHardwareConfig;
     provider.boards = bridgeHardwareBoards;
diff --git a/od-unix/registry.cpp b/od-unix/registry.cpp
new file mode 100644 (file)
index 0000000..e8a7ba6
--- /dev/null
@@ -0,0 +1,400 @@
+#include "sysconfig.h"
+#include "sysdeps.h"
+
+#include <sys/stat.h>
+#include <unistd.h>
+#if defined(UAE_HOST_DARWIN)
+#include <mach-o/dyld.h>
+#endif
+
+#include "registry.h"
+#include "ini.h"
+#include "uae/string.h"
+
+/* Unix port of the od-win32 registry abstraction. Only the ini-file mode
+ * exists here; trees map to slash-joined ini section names below the
+ * WinUAE root section, exactly like winuae.ini on Windows. */
+
+#define ROOT_TREE _T("WinUAE")
+
+static int initialized;
+static TCHAR inipath[MAX_DPATH];
+static TCHAR explicit_inipath[MAX_DPATH];
+static struct ini_data *inidata;
+
+static bool registry_executable_dir(TCHAR *out, size_t out_size)
+{
+#if defined(UAE_HOST_DARWIN)
+    uint32_t size = out_size;
+    if (_NSGetExecutablePath(out, &size) != 0) {
+        return false;
+    }
+#elif defined(UAE_HOST_LINUX)
+    ssize_t len = readlink("/proc/self/exe", out, out_size - 1);
+    if (len <= 0 || (size_t)len >= out_size) {
+        return false;
+    }
+    out[len] = 0;
+#else
+    return false;
+#endif
+    TCHAR *slash = _tcsrchr(out, '/');
+    if (!slash) {
+        return false;
+    }
+    *slash = 0;
+    return true;
+}
+
+static void registry_user_ini_path(TCHAR *out, size_t out_size)
+{
+    const char *home = getenv("HOME");
+    if (!home || !home[0]) {
+        home = "/tmp";
+    }
+#if defined(UAE_HOST_DARWIN)
+    snprintf(out, out_size, "%s/Library/Application Support/WinUAE/winuae.ini", home);
+#else
+    const char *config_home = getenv("XDG_CONFIG_HOME");
+    if (config_home && config_home[0]) {
+        snprintf(out, out_size, "%s/winuae/winuae.ini", config_home);
+    } else {
+        snprintf(out, out_size, "%s/.config/winuae/winuae.ini", home);
+    }
+#endif
+}
+
+static void registry_resolve_path(void)
+{
+    if (explicit_inipath[0]) {
+        uae_tcslcpy(inipath, explicit_inipath, MAX_DPATH);
+        return;
+    }
+    const char *env = getenv("WINUAE_INI");
+    if (env && env[0]) {
+        uae_tcslcpy(inipath, env, MAX_DPATH);
+        return;
+    }
+    TCHAR exedir[MAX_DPATH];
+    if (registry_executable_dir(exedir, MAX_DPATH)
+        && _tcslen(exedir) + 12 < MAX_DPATH) {
+        _stprintf(inipath, _T("%s/winuae.ini"), exedir);
+        if (access(inipath, F_OK) == 0) {
+            /* Portable mode, matching winuae.ini next to winuae.exe. */
+            return;
+        }
+    }
+    registry_user_ini_path(inipath, MAX_DPATH);
+}
+
+static void registry_mkdirs(const TCHAR *path)
+{
+    TCHAR tmp[MAX_DPATH];
+    uae_tcslcpy(tmp, path, MAX_DPATH);
+    TCHAR *slash = _tcsrchr(tmp, '/');
+    if (!slash || slash == tmp) {
+        return;
+    }
+    *slash = 0;
+    for (TCHAR *p = tmp + 1; *p; p++) {
+        if (*p == '/') {
+            *p = 0;
+            mkdir(tmp, 0700);
+            *p = '/';
+        }
+    }
+    mkdir(tmp, 0700);
+}
+
+static void registry_init(void)
+{
+    if (initialized) {
+        return;
+    }
+    initialized = 1;
+    registry_resolve_path();
+    inidata = ini_load(inipath, true);
+    if (!inidata) {
+        inidata = ini_new();
+    }
+}
+
+static const TCHAR *gs (UAEREG *root)
+{
+    if (!root)
+        return ROOT_TREE;
+    return root->inipath;
+}
+
+static TCHAR *gsn (UAEREG *root, const TCHAR *name)
+{
+    const TCHAR *r;
+    TCHAR *s;
+    if (!root)
+        return my_strdup (name);
+    r = gs (root);
+    s = xmalloc (TCHAR, _tcslen (r) + 1 + _tcslen (name) + 1);
+    _stprintf (s, _T("%s/%s"), r, name);
+    return s;
+}
+
+void registry_set_ini_path (const TCHAR *path)
+{
+    if (path && path[0]) {
+        uae_tcslcpy(explicit_inipath, path, MAX_DPATH);
+    } else {
+        explicit_inipath[0] = 0;
+    }
+    if (initialized) {
+        registry_flush();
+        ini_free(inidata);
+        inidata = NULL;
+        initialized = 0;
+    }
+}
+
+void registry_flush (void)
+{
+    if (!initialized || !inidata || !inidata->modified) {
+        return;
+    }
+    registry_mkdirs(inipath);
+    if (ini_save(inidata, inipath)) {
+        inidata->modified = false;
+    } else {
+        write_log(_T("Failed to save host settings to '%s'\n"), inipath);
+    }
+}
+
+int regsetstr (UAEREG *root, const TCHAR *name, const TCHAR *str)
+{
+    registry_init();
+    return ini_addstring(inidata, gs(root), name, str);
+}
+
+int regsetint (UAEREG *root, const TCHAR *name, int val)
+{
+    TCHAR tmp[100];
+    _stprintf (tmp, _T("%d"), val);
+    return regsetstr(root, name, tmp);
+}
+
+int regqueryint (UAEREG *root, const TCHAR *name, int *val)
+{
+    int ret = 0;
+    TCHAR *tmp = NULL;
+    registry_init();
+    if (ini_getstring(inidata, gs(root), name, &tmp)) {
+        *val = _tstol (tmp);
+        ret = 1;
+    }
+    xfree(tmp);
+    return ret;
+}
+
+int regsetlonglong (UAEREG *root, const TCHAR *name, unsigned long long val)
+{
+    TCHAR tmp[100];
+    _stprintf (tmp, _T("%lld"), (long long)val);
+    return regsetstr(root, name, tmp);
+}
+
+int regquerylonglong (UAEREG *root, const TCHAR *name, unsigned long long *val)
+{
+    int ret = 0;
+    TCHAR *tmp = NULL;
+    *val = 0;
+    registry_init();
+    if (ini_getstring(inidata, gs(root), name, &tmp)) {
+        *val = strtoll (tmp, NULL, 10);
+        ret = 1;
+    }
+    xfree(tmp);
+    return ret;
+}
+
+int regquerystr (UAEREG *root, const TCHAR *name, TCHAR *str, int *size)
+{
+    int ret = 0;
+    TCHAR *tmp = NULL;
+    registry_init();
+    if (ini_getstring(inidata, gs(root), name, &tmp)) {
+        if (_tcslen(tmp) >= (size_t)*size)
+            tmp[(*size) - 1] = 0;
+        _tcscpy (str, tmp);
+        *size = (int)_tcslen(str);
+        ret = 1;
+    }
+    xfree (tmp);
+    return ret;
+}
+
+int regenumstr (UAEREG *root, int idx, TCHAR *name, int *nsize, TCHAR *str, int *size)
+{
+    TCHAR *name2 = NULL;
+    TCHAR *str2 = NULL;
+    name[0] = 0;
+    str[0] = 0;
+    registry_init();
+    int ret = ini_getsectionstring(inidata, gs(root), idx, &name2, &str2);
+    if (ret) {
+        if (_tcslen(name2) >= (size_t)*nsize) {
+            name2[(*nsize) - 1] = 0;
+        }
+        if (_tcslen(str2) >= (size_t)*size) {
+            str2[(*size) - 1] = 0;
+        }
+        _tcscpy(name, name2);
+        _tcscpy(str, str2);
+    }
+    xfree(str2);
+    xfree(name2);
+    return ret;
+}
+
+int regquerydatasize (UAEREG *root, const TCHAR *name, int *size)
+{
+    int ret = 0;
+    int csize = 65536;
+    TCHAR *tmp = xmalloc (TCHAR, csize);
+    if (regquerystr (root, name, tmp, &csize)) {
+        *size = (int)_tcslen(tmp) / 2;
+        ret = 1;
+    }
+    xfree (tmp);
+    return ret;
+}
+
+int regsetdata (UAEREG *root, const TCHAR *name, const void *str, int size)
+{
+    uae_u8 *in = (uae_u8*)str;
+    int ret;
+    TCHAR *tmp = xmalloc (TCHAR, size * 2 + 1);
+    for (int i = 0; i < size; i++)
+        _stprintf (tmp + i * 2, _T("%02X"), in[i]);
+    registry_init();
+    ret = ini_addstring(inidata, gs(root), name, tmp);
+    xfree (tmp);
+    return ret;
+}
+
+int regquerydata (UAEREG *root, const TCHAR *name, void *str, int *size)
+{
+    int csize = (*size) * 2 + 1;
+    int i, j;
+    int ret = 0;
+    TCHAR *tmp = xmalloc (TCHAR, csize);
+    uae_u8 *out = (uae_u8*)str;
+
+    if (!regquerystr (root, name, tmp, &csize))
+        goto err;
+    j = 0;
+    for (i = 0; i < (int)_tcslen (tmp); i += 2) {
+        TCHAR c1 = _totupper(tmp[i + 0]);
+        TCHAR c2 = _totupper(tmp[i + 1]);
+        if (c1 >= 'A')
+            c1 -= 'A' - 10;
+        else if (c1 >= '0')
+            c1 -= '0';
+        if (c1 > 15)
+            goto err;
+        if (c2 >= 'A')
+            c2 -= 'A' - 10;
+        else if (c2 >= '0')
+            c2 -= '0';
+        if (c2 > 15)
+            goto err;
+        out[j++] = c1 * 16 + c2;
+    }
+    ret = 1;
+err:
+    xfree (tmp);
+    return ret;
+}
+
+int regdelete (UAEREG *root, const TCHAR *name)
+{
+    registry_init();
+    ini_delete(inidata, gs(root), name);
+    return 1;
+}
+
+int regexists (UAEREG *root, const TCHAR *name)
+{
+    registry_init();
+    if (!inidata)
+        return 0;
+    return ini_getstring(inidata, gs(root), name, NULL);
+}
+
+void regdeletetree (UAEREG *root, const TCHAR *name)
+{
+    TCHAR *s = gsn (root, name);
+    if (!s)
+        return;
+    registry_init();
+    ini_delete(inidata, s, NULL);
+    xfree (s);
+}
+
+int regexiststree (UAEREG *root, const TCHAR *name)
+{
+    int ret = 0;
+    TCHAR *s = gsn (root, name);
+    if (!s)
+        return 0;
+    registry_init();
+    ret = ini_getstring(inidata, s, NULL, NULL);
+    xfree (s);
+    return ret;
+}
+
+UAEREG *regcreatetree (UAEREG *root, const TCHAR *name)
+{
+    UAEREG *fkey;
+    TCHAR *ininame;
+
+    registry_init();
+    if (!root) {
+        if (!name)
+            ininame = my_strdup (gs (NULL));
+        else
+            ininame = my_strdup (name);
+    } else {
+        ininame = xmalloc (TCHAR, _tcslen (root->inipath) + 1 + _tcslen (name) + 1);
+        _stprintf (ininame, _T("%s/%s"), root->inipath, name);
+    }
+    fkey = xcalloc (UAEREG, 1);
+    fkey->inipath = ininame;
+    return fkey;
+}
+
+void regclosetree (UAEREG *key)
+{
+    registry_flush();
+    if (!key)
+        return;
+    xfree (key->inipath);
+    xfree (key);
+}
+
+int reginitializeinit (TCHAR **pppath)
+{
+    if (pppath && *pppath) {
+        registry_set_ini_path(*pppath);
+    }
+    registry_init();
+    return 1;
+}
+
+void regstatus (void)
+{
+    registry_init();
+    write_log (_T("Host settings: '%s'\n"), inipath);
+}
+
+const TCHAR *getregmode (void)
+{
+    registry_init();
+    return inipath;
+}
diff --git a/od-unix/registry.h b/od-unix/registry.h
new file mode 100644 (file)
index 0000000..9483093
--- /dev/null
@@ -0,0 +1,39 @@
+#pragma once
+
+/* Unix host settings store with the Windows registry.cpp API, always in
+ * ini-file mode. Values persist in winuae.ini next to the executable
+ * (portable mode) or in the per-user configuration directory. */
+
+typedef struct UAEREG {
+    TCHAR *inipath;
+} UAEREG;
+
+extern const TCHAR *getregmode (void);
+extern int reginitializeinit (TCHAR **path);
+extern void regstatus (void);
+
+extern int regsetstr (UAEREG*, const TCHAR *name, const TCHAR *str);
+extern int regsetint (UAEREG*, const TCHAR *name, int val);
+extern int regqueryint (UAEREG*, const TCHAR *name, int *val);
+extern int regquerystr (UAEREG*, const TCHAR *name, TCHAR *str, int *size);
+extern int regsetlonglong (UAEREG *root, const TCHAR *name, unsigned long long val);
+extern int regquerylonglong (UAEREG *root, const TCHAR *name, unsigned long long *val);
+
+extern int regdelete (UAEREG*, const TCHAR *name);
+extern void regdeletetree (UAEREG*, const TCHAR *name);
+
+extern int regexists (UAEREG*, const TCHAR *name);
+extern int regexiststree (UAEREG *, const TCHAR *name);
+
+extern int regquerydatasize (UAEREG *root, const TCHAR *name, int *size);
+extern int regsetdata (UAEREG*, const TCHAR *name, const void *str, int size);
+extern int regquerydata (UAEREG *root, const TCHAR *name, void *str, int *size);
+
+extern int regenumstr (UAEREG*, int idx, TCHAR *name, int *nsize, TCHAR *str, int *size);
+
+extern UAEREG *regcreatetree (UAEREG*, const TCHAR *name);
+extern void regclosetree (UAEREG *key);
+
+/* Unix additions. */
+extern void registry_set_ini_path (const TCHAR *path);
+extern void registry_flush (void);
diff --git a/od-unix/registry_test.cpp b/od-unix/registry_test.cpp
new file mode 100644 (file)
index 0000000..07220f6
--- /dev/null
@@ -0,0 +1,109 @@
+#include "sysconfig.h"
+#include "sysdeps.h"
+
+#include <cstdarg>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <unistd.h>
+
+#include "registry.h"
+
+void write_log(const char *format, ...)
+{
+    va_list ap;
+    va_start(ap, format);
+    vfprintf(stderr, format, ap);
+    va_end(ap);
+}
+
+int uaetcslen(const TCHAR *s)
+{
+    return (int)strlen(s);
+}
+
+static int failures;
+
+static void check(bool ok, const char *what)
+{
+    if (!ok) {
+        fprintf(stderr, "FAIL: %s\n", what);
+        failures++;
+    }
+}
+
+int main(void)
+{
+    char path[256];
+    snprintf(path, sizeof path, "/tmp/winuae_registry_test_%ld/sub/winuae.ini", (long)getpid());
+    registry_set_ini_path(path);
+
+    /* Root values. */
+    check(regsetstr(NULL, "TestString", "hello world") == 1, "regsetstr");
+    check(regsetint(NULL, "TestInt", -42) == 1, "regsetint");
+    check(regsetlonglong(NULL, "TestLong", 123456789012345ULL) == 1, "regsetlonglong");
+
+    TCHAR buf[256];
+    int size = sizeof buf;
+    check(regquerystr(NULL, "TestString", buf, &size) == 1 && !strcmp(buf, "hello world"), "regquerystr");
+    int v = 0;
+    check(regqueryint(NULL, "TestInt", &v) == 1 && v == -42, "regqueryint");
+    unsigned long long lv = 0;
+    check(regquerylonglong(NULL, "TestLong", &lv) == 1 && lv == 123456789012345ULL, "regquerylonglong");
+    check(regexists(NULL, "TestString") == 1, "regexists");
+    check(regexists(NULL, "Missing") == 0, "regexists missing");
+
+    /* Subtrees. */
+    UAEREG *tree = regcreatetree(NULL, "DetectedROMs");
+    check(tree != NULL, "regcreatetree");
+    check(regsetstr(tree, "ROM_1", "kick13.rom") == 1, "tree regsetstr");
+    size = sizeof buf;
+    check(regquerystr(tree, "ROM_1", buf, &size) == 1 && !strcmp(buf, "kick13.rom"), "tree regquerystr");
+    check(regexiststree(NULL, "DetectedROMs") == 1, "regexiststree");
+
+    /* Enumeration. */
+    TCHAR name[64];
+    int nsize = sizeof name;
+    size = sizeof buf;
+    check(regenumstr(tree, 0, name, &nsize, buf, &size) == 1 && !strcmp(name, "ROM_1"), "regenumstr");
+    regclosetree(tree);
+
+    /* Binary data. */
+    const unsigned char data[4] = { 0xde, 0xad, 0xbe, 0xef };
+    check(regsetdata(NULL, "TestData", data, sizeof data) == 1, "regsetdata");
+    unsigned char out[4] = { 0 };
+    size = sizeof out;
+    check(regquerydata(NULL, "TestData", out, &size) == 1 && !memcmp(out, data, sizeof data), "regquerydata");
+    size = 0;
+    check(regquerydatasize(NULL, "TestData", &size) == 1 && size == 4, "regquerydatasize");
+
+    /* Delete. */
+    check(regdelete(NULL, "TestInt") == 1, "regdelete");
+    check(regexists(NULL, "TestInt") == 0, "deleted value gone");
+
+    /* Persistence: flush, drop state, re-open the same file. */
+    registry_flush();
+    registry_set_ini_path(path);
+    size = sizeof buf;
+    check(regquerystr(NULL, "TestString", buf, &size) == 1 && !strcmp(buf, "hello world"), "persisted string");
+    UAEREG *tree2 = regcreatetree(NULL, "DetectedROMs");
+    size = sizeof buf;
+    check(regquerystr(tree2, "ROM_1", buf, &size) == 1 && !strcmp(buf, "kick13.rom"), "persisted tree value");
+    regclosetree(tree2);
+    check(regexists(NULL, "TestInt") == 0, "deleted value stays gone");
+
+    /* Tree deletion. */
+    regdeletetree(NULL, "DetectedROMs");
+    check(regexiststree(NULL, "DetectedROMs") == 0, "regdeletetree");
+
+    char cmd[300];
+    snprintf(cmd, sizeof cmd, "rm -rf /tmp/winuae_registry_test_%ld", (long)getpid());
+    system(cmd);
+
+    if (failures) {
+        fprintf(stderr, "%d failure(s)\n", failures);
+        return 1;
+    }
+    printf("winuae_unix_registry_test: all checks passed\n");
+    return 0;
+}