From: Tobias Stoeckmann <tobias@stoeckmann.org>
Date: Mon, 15 Sep 2025 22:35:40 +0200
Subject: fuzzing: add special dirs fuzzing test

Split custom user-dirs.dirs parser into its own gutilsprivate.c file
for easier fuzzing support and add a fuzzing test.

Origin: upstream, 2.86.1, commit:ed594d819e94ad2f399c9167bfe37f728e1b6544
---
 fuzzing/fuzz_special_dirs.c |  43 ++++++++++++++
 fuzzing/meson.build         |   1 +
 glib/gutils.c               | 118 +++----------------------------------
 glib/gutilsprivate.c        | 138 ++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 189 insertions(+), 111 deletions(-)
 create mode 100644 fuzzing/fuzz_special_dirs.c
 create mode 100644 glib/gutilsprivate.c

diff --git a/fuzzing/fuzz_special_dirs.c b/fuzzing/fuzz_special_dirs.c
new file mode 100644
index 0000000..db20e4f
--- /dev/null
+++ b/fuzzing/fuzz_special_dirs.c
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2025 Philip Withnall
+ * Copyright 2025 Tobias Stoeckmann
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "fuzz.h"
+#include "glib.h"
+#include "gutilsprivate.c"
+
+int
+LLVMFuzzerTestOneInput (const unsigned char *data, size_t size)
+{
+  gchar *special_dirs[G_USER_N_DIRECTORIES] = { 0 };
+
+  fuzz_set_logging_func ();
+
+  load_user_special_dirs_from_data ((const gchar *) data, "/dev/null", special_dirs);
+
+  /* Test directories and make sure that, if they exist, they are absolute. */
+  for (GUserDirectory dir_type = G_USER_DIRECTORY_DESKTOP; dir_type < G_USER_N_DIRECTORIES; dir_type++)
+    {
+      char *dir = special_dirs[dir_type];
+      g_assert_true (dir == NULL || g_path_is_absolute (dir));
+      g_free (dir);
+    }
+
+  return 0;
+}
diff --git a/fuzzing/meson.build b/fuzzing/meson.build
index d3107a1..addbe90 100644
--- a/fuzzing/meson.build
+++ b/fuzzing/meson.build
@@ -34,6 +34,7 @@ fuzz_targets = [
   'fuzz_network_address_parse_uri',
   'fuzz_paths',
   'fuzz_resolver',
+  'fuzz_special_dirs',
   'fuzz_string',
   'fuzz_uri_escape',
   'fuzz_uri_parse',
diff --git a/glib/gutils.c b/glib/gutils.c
index 3102b07..df744c7 100644
--- a/glib/gutils.c
+++ b/glib/gutils.c
@@ -2241,6 +2241,8 @@ load_user_special_dirs (void)
 
 #else /* default is unix */
 
+#include "gutilsprivate.c"
+
 /* adapted from xdg-user-dir-lookup.c
  *
  * Copyright (C) 2007 Red Hat Inc.
@@ -2269,11 +2271,10 @@ static void
 load_user_special_dirs (void)
 {
   const gchar *config_dir = NULL;
+  const gchar *home_dir;
   gchar *config_file;
   gchar *data;
-  gchar **lines;
-  gint n_lines, i;
-  
+
   config_dir = g_get_user_config_dir_unlocked ();
   config_file = g_build_filename (config_dir,
                                   "user-dirs.dirs",
@@ -2285,115 +2286,10 @@ load_user_special_dirs (void)
       return;
     }
 
-  lines = g_strsplit (data, "\n", -1);
-  n_lines = g_strv_length (lines);
-  g_free (data);
-  
-  for (i = 0; i < n_lines; i++)
-    {
-      gchar *buffer = lines[i];
-      gchar *d, *p;
-      size_t len;
-      gboolean is_relative = FALSE;
-      GUserDirectory directory;
-
-      /* Remove newline at end */
-      len = strlen (buffer);
-      if (len > 0 && buffer[len - 1] == '\n')
-	buffer[len - 1] = 0;
-      
-      p = buffer;
-      while (*p == ' ' || *p == '\t')
-	p++;
-      
-      if (strncmp (p, "XDG_DESKTOP_DIR", strlen ("XDG_DESKTOP_DIR")) == 0)
-        {
-          directory = G_USER_DIRECTORY_DESKTOP;
-          p += strlen ("XDG_DESKTOP_DIR");
-        }
-      else if (strncmp (p, "XDG_DOCUMENTS_DIR", strlen ("XDG_DOCUMENTS_DIR")) == 0)
-        {
-          directory = G_USER_DIRECTORY_DOCUMENTS;
-          p += strlen ("XDG_DOCUMENTS_DIR");
-        }
-      else if (strncmp (p, "XDG_DOWNLOAD_DIR", strlen ("XDG_DOWNLOAD_DIR")) == 0)
-        {
-          directory = G_USER_DIRECTORY_DOWNLOAD;
-          p += strlen ("XDG_DOWNLOAD_DIR");
-        }
-      else if (strncmp (p, "XDG_MUSIC_DIR", strlen ("XDG_MUSIC_DIR")) == 0)
-        {
-          directory = G_USER_DIRECTORY_MUSIC;
-          p += strlen ("XDG_MUSIC_DIR");
-        }
-      else if (strncmp (p, "XDG_PICTURES_DIR", strlen ("XDG_PICTURES_DIR")) == 0)
-        {
-          directory = G_USER_DIRECTORY_PICTURES;
-          p += strlen ("XDG_PICTURES_DIR");
-        }
-      else if (strncmp (p, "XDG_PUBLICSHARE_DIR", strlen ("XDG_PUBLICSHARE_DIR")) == 0)
-        {
-          directory = G_USER_DIRECTORY_PUBLIC_SHARE;
-          p += strlen ("XDG_PUBLICSHARE_DIR");
-        }
-      else if (strncmp (p, "XDG_TEMPLATES_DIR", strlen ("XDG_TEMPLATES_DIR")) == 0)
-        {
-          directory = G_USER_DIRECTORY_TEMPLATES;
-          p += strlen ("XDG_TEMPLATES_DIR");
-        }
-      else if (strncmp (p, "XDG_VIDEOS_DIR", strlen ("XDG_VIDEOS_DIR")) == 0)
-        {
-          directory = G_USER_DIRECTORY_VIDEOS;
-          p += strlen ("XDG_VIDEOS_DIR");
-        }
-      else
-	continue;
-
-      while (*p == ' ' || *p == '\t')
-	p++;
-
-      if (*p != '=')
-	continue;
-      p++;
-
-      while (*p == ' ' || *p == '\t')
-	p++;
-
-      if (*p != '"')
-	continue;
-      p++;
-
-      if (strncmp (p, "$HOME", 5) == 0)
-        {
-          p += 5;
-          if (*p != '/' && *p != '"')
-            continue;
-          is_relative = TRUE;
-        }
-      else if (*p != '/')
-        continue;
-
-      d = strrchr (p, '"');
-      if (d < p)
-        continue;
-      *d = 0;
-
-      d = p;
-
-      /* remove trailing slashes */
-      for (len = strlen (d); len > 1 && d[len - 1] == '/'; len--)
-        d[len - 1] = 0;
-
-      if (is_relative)
-        {
-          const gchar *home_dir = g_get_home_dir_unlocked ();
-          g_user_special_dirs[directory] = g_build_filename (home_dir, d, NULL);
-        }
-      else
-	g_user_special_dirs[directory] = g_strdup (d);
-    }
+  home_dir = g_get_home_dir_unlocked ();
+  load_user_special_dirs_from_data ((const gchar *) data, home_dir, g_user_special_dirs);
 
-  g_strfreev (lines);
+  g_free (data);
   g_free (config_file);
 }
 
diff --git a/glib/gutilsprivate.c b/glib/gutilsprivate.c
new file mode 100644
index 0000000..4b48f91
--- /dev/null
+++ b/glib/gutilsprivate.c
@@ -0,0 +1,138 @@
+/* adapted from xdg-user-dir-lookup.c
+ *
+ * Copyright (C) 2007 Red Hat Inc.
+ *
+ * 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.
+ */
+static void
+load_user_special_dirs_from_data (const gchar *data, const gchar *home_dir, gchar **special_dirs)
+{
+  gchar **lines;
+  gint n_lines, i;
+
+  lines = g_strsplit (data, "\n", -1);
+  n_lines = g_strv_length (lines);
+
+  for (i = 0; i < n_lines; i++)
+    {
+      gchar *buffer = lines[i];
+      gchar *d, *p;
+      size_t len;
+      gboolean is_relative = FALSE;
+      GUserDirectory directory;
+
+      /* Remove newline at end */
+      len = strlen (buffer);
+      if (len > 0 && buffer[len - 1] == '\n')
+        buffer[len - 1] = 0;
+
+      p = buffer;
+      while (*p == ' ' || *p == '\t')
+        p++;
+
+      if (strncmp (p, "XDG_DESKTOP_DIR", strlen ("XDG_DESKTOP_DIR")) == 0)
+        {
+          directory = G_USER_DIRECTORY_DESKTOP;
+          p += strlen ("XDG_DESKTOP_DIR");
+        }
+      else if (strncmp (p, "XDG_DOCUMENTS_DIR", strlen ("XDG_DOCUMENTS_DIR")) == 0)
+        {
+          directory = G_USER_DIRECTORY_DOCUMENTS;
+          p += strlen ("XDG_DOCUMENTS_DIR");
+        }
+      else if (strncmp (p, "XDG_DOWNLOAD_DIR", strlen ("XDG_DOWNLOAD_DIR")) == 0)
+        {
+          directory = G_USER_DIRECTORY_DOWNLOAD;
+          p += strlen ("XDG_DOWNLOAD_DIR");
+        }
+      else if (strncmp (p, "XDG_MUSIC_DIR", strlen ("XDG_MUSIC_DIR")) == 0)
+        {
+          directory = G_USER_DIRECTORY_MUSIC;
+          p += strlen ("XDG_MUSIC_DIR");
+        }
+      else if (strncmp (p, "XDG_PICTURES_DIR", strlen ("XDG_PICTURES_DIR")) == 0)
+        {
+          directory = G_USER_DIRECTORY_PICTURES;
+          p += strlen ("XDG_PICTURES_DIR");
+        }
+      else if (strncmp (p, "XDG_PUBLICSHARE_DIR", strlen ("XDG_PUBLICSHARE_DIR")) == 0)
+        {
+          directory = G_USER_DIRECTORY_PUBLIC_SHARE;
+          p += strlen ("XDG_PUBLICSHARE_DIR");
+        }
+      else if (strncmp (p, "XDG_TEMPLATES_DIR", strlen ("XDG_TEMPLATES_DIR")) == 0)
+        {
+          directory = G_USER_DIRECTORY_TEMPLATES;
+          p += strlen ("XDG_TEMPLATES_DIR");
+        }
+      else if (strncmp (p, "XDG_VIDEOS_DIR", strlen ("XDG_VIDEOS_DIR")) == 0)
+        {
+          directory = G_USER_DIRECTORY_VIDEOS;
+          p += strlen ("XDG_VIDEOS_DIR");
+        }
+      else
+        continue;
+
+      while (*p == ' ' || *p == '\t')
+        p++;
+
+      if (*p != '=')
+        continue;
+      p++;
+
+      while (*p == ' ' || *p == '\t')
+        p++;
+
+      if (*p != '"')
+        continue;
+      p++;
+
+      if (strncmp (p, "$HOME", 5) == 0)
+        {
+          p += 5;
+          if (*p != '/' && *p != '"')
+            continue;
+          is_relative = TRUE;
+        }
+      else if (*p != '/')
+        continue;
+
+      d = strrchr (p, '"');
+      if (d < p)
+        continue;
+      *d = 0;
+
+      d = p;
+
+      /* remove trailing slashes */
+      for (len = strlen (d); len > 1 && d[len - 1] == '/'; len--)
+        d[len - 1] = 0;
+
+      if (is_relative)
+        {
+          special_dirs[directory] = g_build_filename (home_dir, d, NULL);
+        }
+      else
+        special_dirs[directory] = g_strdup (d);
+    }
+
+  g_strfreev (lines);
+}
