Subject: [PATCH 4/7] gnu: jgrf: Add search path mechanism.

From: Maxim Cournoyer <[email-masked]>
Date: Thu, 26 Sep 2024 12:09:58 +0900

This makes it possible to automatically find core files in a profile.

* gnu/packages/patches/jgrf-implement-search-paths.patch: New file.
* gnu/local.mk (dist_patch_DATA): Register it.
* gnu/packages/emulators.scm (jgrf) [source]: Apply patch.
[native-search-paths]: New field.

Change-Id: Ibd78d44dcdf23f4310b2f838d73b8e57d7f31b2a
---

 gnu/local.mk                                  |   1 +
 gnu/packages/emulators.scm                    |  10 +-
 .../patches/jgrf-implement-search-paths.patch | 302 ++++++++++++++++++
 3 files changed, 312 insertions(+), 1 deletion(-)
 create mode 100644 gnu/packages/patches/jgrf-implement-search-paths.patch

diff --git a/gnu/local.mk b/gnu/local.mk
index 507cbfebca..1e85ab5352 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -1589,6 +1589,7 @@ dist_patch_DATA =						\
   %D%/packages/patches/jfsutils-add-sysmacros.patch		\
   %D%/packages/patches/jfsutils-gcc-compat.patch		\
   %D%/packages/patches/jfsutils-include-systypes.patch		\
+  %D%/packages/patches/jgrf-implement-search-paths.patch	\
   %D%/packages/patches/john-the-ripper-jumbo-with-gcc-11.patch	\
   %D%/packages/patches/json-c-0.13-CVE-2020-12762.patch		\
   %D%/packages/patches/json-c-0.12-CVE-2020-12762.patch		\
diff --git a/gnu/packages/emulators.scm b/gnu/packages/emulators.scm
index 6a5ff6cc19..49208f302d 100644
--- a/gnu/packages/emulators.scm
+++ b/gnu/packages/emulators.scm
@@ -2308,7 +2308,8 @@ (define-public jgrf
                           (delete-file-recursively "deps/miniz")))
               (sha256
                (base32
-                "19n6h8l3vy5g2bqvxhxwqxlg070hjz22384yisadzwl3gjkkgpxk"))))
+                "19n6h8l3vy5g2bqvxhxwqxlg070hjz22384yisadzwl3gjkkgpxk"))
+              (patches (search-patches "jgrf-implement-search-paths.patch"))))
     (build-system gnu-build-system)
     (arguments
      (list #:tests? #f                  ;no test suite
@@ -2331,6 +2332,13 @@ (define-public jgrf
            sdl2
            zlib
            `(,zstd "lib")))
+    (native-search-paths
+     (list (search-path-specification
+            (variable "JOLLYGOOD_CORE_DIRS")
+            (files '("lib/jollygood")))
+           (search-path-specification
+            (variable "JOLLYGOOD_ASSETS_DIRS")
+            (files '("share/jollygood")))))
     (home-page "https://gitlab.com/jgemu/jgrf")
     (synopsis "Jolly Good Reference Frontend")
     (description "The Jolly Good Reference Frontend (accessible via the
diff --git a/gnu/packages/patches/jgrf-implement-search-paths.patch b/gnu/packages/patches/jgrf-implement-search-paths.patch
new file mode 100644
index 0000000000..f4c6f20568
--- /dev/null
+++ b/gnu/packages/patches/jgrf-implement-search-paths.patch
@@ -0,0 +1,302 @@
+Upstream status: https://gitlab.com/jgemu/jgrf/-/merge_requests/61
+
+Add support for searching core files and core files assets via the
+JOLLYGOOD_CORE_DIRS and JOLLYGOOD_ASSETS_DIRS environment variables.
+
+diff --git a/Makefile b/Makefile
+index ed6eb1c..fb0709c 100644
+--- a/Makefile
++++ b/Makefile
+@@ -20,7 +20,9 @@ INCLUDES = -I$(DEPDIR) $(CFLAGS_JG) $(CFLAGS_EPOXY) $(CFLAGS_MINIZ) \
+ 
+ LIBS = -lm
+ 
+-DEFINES :=
++# Define the '_GNU_SOURCE' macro to make the `strdup' function
++# available on GNU systems.
++DEFINES := -D_GNU_SOURCE
+ 
+ # Conditions for DEFINES
+ ifneq ($(OS), Windows_NT)
+diff --git a/deps/ezmenu.h b/deps/ezmenu.h
+index 8400a02..6c8e14b 100644
+--- a/deps/ezmenu.h
++++ b/deps/ezmenu.h
+@@ -66,10 +66,10 @@ enum ezmenu_input {
+ 
+ static void ezmenu_init(struct ezmenu *m, int hres, int vres,
+ 	int fontw, int fonth) {
+-	memset(m, 0, sizeof *m);
++	memset(m, 0, sizeof(*m));
+ 	m->w = hres/fontw;
+ 	m->h = vres/fonth;
+-	m->vislines = calloc(sizeof(char*), m->h);
++	m->vislines = calloc(m->h, sizeof(char*));
+ }
+ 
+ static void ezmenu_setlines(struct ezmenu *m, char**lines, unsigned linecount) {
+diff --git a/jollygood.6 b/jollygood.6
+index 9f36407..17fef9c 100644
+--- a/jollygood.6
++++ b/jollygood.6
+@@ -336,6 +336,14 @@ The directory for user-specific data files.
+ This path is used to find data files used by the cores.
+ Set by default to
+ .Pa $HOME/.local/share/jollygood/ .
++.It JOLLYGOOD_CORE_DIRS
++Colon-separated (or semicolon, on Windows) directories containing core
++files. If set, these core files directories take precedence over the
++default ones.
++.It JOLLYGOOD_ASSETS_DIRS
++Colon-separated (or semicolon, on Windows) directories containing core
++assets files. If set, these core assets directories take precedence
++over the default ones.
+ .El
+ .Sh EXAMPLES
+ .Bl -tag -width indent
+diff --git a/src/jgrf.c b/src/jgrf.c
+index 3a40d2d..12eede3 100644
+--- a/src/jgrf.c
++++ b/src/jgrf.c
+@@ -161,6 +161,12 @@ static void mkdirr(const char *dir) {
+     #endif
+ }
+ 
++#if defined(__MINGW32__) || defined(__MINGW64__)
++// Avoid a deprecation warning on Windows, where strdup exists but is
++// deprecated in favor of _strdup.
++#define strdup _strdup
++#endif
++
+ // Create user directories
+ static void jgrf_mkdirs(void) {
+     mkdirr(gdata.configpath);
+@@ -1096,6 +1102,88 @@ void jgrf_frametime(double frametime) {
+     corefps = frametime + 0.5;
+ }
+ 
++// Wrapper that logs and errors in case of realloc problems.
++static void* jgrf_realloc(void* array, const size_t size) {
++    void* new_array;
++    if (size > 0 && !(new_array = realloc(array, size)))
++        jgrf_log(JG_LOG_ERR, "Realloc failure\n");
++    return new_array;
++}
++
++static void jgrf_strip_trailing_sep(char* word) {
++    int end = strlen(word) - 1;
++    while (word[end] == SEP) {
++        word[end] = '\0';
++        end -= 1;
++    }
++}
++
++// Tokenize PATH based on the platform path separator and return an
++// array of strings, or NULL if there nothing could be tokenized.  The
++// count of the number of items is written at the location pointed by
++// the COUNT pointer.  The returned array is dynamically allocated and
++// should be freed when no longer needed, along its inner strings.
++// `transform' can be provided to manipulate the recovered path items;
++// it must be the pointer of a procedure accepting a single string
++// (char*) path item as argument or NULL.
++static char** tokenize_path(int* count, const char* path,
++                            void (*transform) (char*)) {
++    int length = 10;
++    int index = 0;
++    char* save_ptr;
++    char** items = jgrf_realloc(NULL, sizeof(char*) * length);
++    char* item;
++    char pathsep_str[2] = {PATHSEP, '\0'};
++    char* wr_path = strdup(path);
++    if (!wr_path)
++        jgrf_log(JG_LOG_ERR, "strdup memory allocation failure\n");
++
++    item = strtok_r(wr_path, pathsep_str, &save_ptr);
++    while (item) {
++        if (transform)
++            transform(item);
++
++        // Resize the array if needed.
++        if (index >= length) {
++            length += 10;
++            items = jgrf_realloc(items, sizeof(char*) * length);
++        }
++
++	// Assign the component.
++ 	items[index] = strdup(item);
++	if (!items[index])
++	    jgrf_log(JG_LOG_ERR, "stdup memory allocation failure\n");
++
++        index += 1;
++        item = strtok_r(NULL, pathsep_str, &save_ptr);
++    }
++    free(wr_path);
++    jgrf_realloc(items, sizeof(char*) * index);
++
++    *count = index;
++    return items;
++}
++
++// Look if a core named NAME exists under the directory CORE_DIR. Set
++// CORE_FILE as a side-effect. Return 1 if found, 0 otherwise.
++static int search_core_file(char* core_file, size_t max_length,
++                             const char* core_dir, const char* name) {
++    struct stat fbuf;
++    snprintf(core_file, max_length, "%s%c%s.%s",
++             core_dir, SEP, name, SOEXT);
++    return !stat(core_file, &fbuf);
++}
++
++// Look if an core assets directory for core named NAME exists under
++// the directory ASSETS_DIR. Set CORE_ASSETS as a side-effect. Return
++// 1 if found, 0 otherwise.
++static int search_core_assets(char* core_assets, size_t max_length,
++                              const char* assets_dir, const char* name) {
++    struct stat fbuf;
++    snprintf(core_assets, max_length, "%s%c%s", assets_dir, SEP, name);
++    return !stat(core_assets, &fbuf);
++}
++
+ int main(int argc, char *argv[]) {
+     if (argc < 2) {
+         jgrf_cli_usage(argv[0]);
+@@ -1188,7 +1276,7 @@ int main(int argc, char *argv[]) {
+         jg_get_coreinfo("")->name);
+ 
+ #if defined(LIBDIR) && defined(DATADIR) // Check for core assets system-wide
+-    char coreassets[256];
++    char coreassets[384];
+     snprintf(coreassets, sizeof(coreassets),
+         "%s%cjollygood%c%s", DATADIR, SEP, SEP, gdata.corename);
+ 
+@@ -1204,7 +1292,7 @@ int main(int argc, char *argv[]) {
+     }
+     else if (!jgrf_core_default())
+         jgrf_log(JG_LOG_ERR,
+-            "Cannot detect default core, or invalid file. Exiting...\n");
++                 "Cannot detect default core, or invalid file. Exiting...\n");
+ 
+     // Set the core path to the local core path
+     char corepath[384];
+@@ -1222,20 +1310,85 @@ int main(int argc, char *argv[]) {
+     }
+ #if defined(LIBDIR) && defined(DATADIR) // Check for the core system-wide
+     else {
+-        snprintf(corepath, sizeof(corepath), "%s%cjollygood%c%s.%s",
+-            LIBDIR, SEP, SEP, gdata.corename, SOEXT);
+-
+-        // If it was found, set the core assets path
+-        if (stat(corepath, &fbuf) == 0) {
+-            snprintf(gdata.coreassets, sizeof(gdata.coreassets),
+-                "%s%cjollygood%c%s", DATADIR, SEP, SEP, gdata.corename);
+-            corefound = 1;
++        int core_assets_found = 0;
++        int count = 0;
++
++        // Look for the core file in the JOLLYGOOD_CORE_DIRS search
++        // path.
++        const char* core_dirs_env = getenv("JOLLYGOOD_CORE_DIRS");
++        if (core_dirs_env) {
++            char** core_dirs = tokenize_path(&count, core_dirs_env,
++                                             jgrf_strip_trailing_sep);
++            for (int i=0; i < count; ++i) {
++                const char* dir = core_dirs[i];
++                if (search_core_file(corepath, sizeof corepath,
++                                     dir, gdata.corename)) {
++                    corefound = 1;
++                    break;
++                }
++            }
++            // Free all the allocated strings.
++            for (int i=0; i < count; ++i)
++                free(core_dirs[i]);
++            free(core_dirs);
++        }
++
++        // Look in the configured LIBDIR as a fallback.
++        if (!corefound) {
++            char internal_core_dir[256];
++            snprintf(internal_core_dir, sizeof internal_core_dir, "%s%c%s",
++                     LIBDIR, SEP, "jollygood");
++            if (search_core_file(corepath, sizeof corepath,
++                                 internal_core_dir, gdata.corename)) {
++                corefound = 1;
++            } else {
++                jgrf_log(JG_LOG_ERR,
++                         "Could not locate core file. Exiting...\n");
++            }
+         }
+-    }
+ 
+-    // If no core was found, there is no reason to keep the program running
+-    if (!corefound)
+-        jgrf_log(JG_LOG_ERR, "Failed to locate core. Exiting...\n");
++        // If it was found, set the core assets path. First consider
++        // the JOLLYGOOD_ASSETS_DIRS search path.
++        if (corefound) {
++            const char* assets_dirs_env = getenv("JOLLYGOOD_ASSETS_DIRS");
++            if (assets_dirs_env) {
++                char** assets_dirs = tokenize_path(&count, assets_dirs_env,
++                                                   jgrf_strip_trailing_sep);
++                for (int i=0; i < count; ++i) {
++                    const char* dir = assets_dirs[i];
++                    if (search_core_assets(gdata.coreassets,
++                                           sizeof gdata.coreassets,
++                                           dir, gdata.corename)) {
++                        core_assets_found = 1;
++                        break;
++                    }
++                }
++                for (int i=0; i < count; ++i)
++                    free(assets_dirs[i]);
++                free(assets_dirs);
++            }
++
++            // Look in the configured DATADIR as a fallback.
++            if (!core_assets_found) {
++                char internal_core_assets_dir[256];
++                snprintf(internal_core_assets_dir,
++                         sizeof internal_core_assets_dir,
++                         "%s%c%s", DATADIR, SEP, "jollygood");
++                if (search_core_assets(gdata.coreassets,
++                                       sizeof gdata.coreassets,
++                                       internal_core_assets_dir,
++                                       gdata.corename)) {
++                    core_assets_found = 1;
++                } else {
++                    // Not all emulators have core assets,
++                    // e.g. 'cega', so this is not a critical
++                    // condition.
++                    jgrf_log(JG_LOG_INF, "No assets directory for core %s\n",
++                        gdata.corename);
++                }
++            }
++        }
++    }
+ #endif // defined(LIBDIR) && defined(DATADIR)
+ 
+ #endif // JGRF_STATIC
+diff --git a/src/jgrf.h b/src/jgrf.h
+index b3f4627..57bba0e 100644
+--- a/src/jgrf.h
++++ b/src/jgrf.h
+@@ -45,8 +45,10 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ 
+ #if defined(_WIN32) || defined(__MINGW32__) || defined(__MINGW64__)
+     #define SEP '\\'
++    #define PATHSEP ';'
+ #else
+     #define SEP '/'
++    #define PATHSEP ':'
+ #endif
+ 
+ typedef struct jgrf_gdata_t { // Global Data
+@@ -59,7 +61,7 @@ typedef struct jgrf_gdata_t { // Global Data
+     char coreversion[32]; // Core Version
+     char gamename[128]; // Internally used game name
+     char gamefname[128]; // Internally used game name with extension
+-    char coreassets[256]; // Core asset path
++    char coreassets[384]; // Core asset path
+     char userassets[128]; // User asset path
+     char biospath[128]; // BIOS path
+     char cheatpath[128]; // Cheat path
-- 
2.46.0