public inbox for linux-bluetooth@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH BlueZ v3 0/1] Support for config fragments (conf.d style dirs)
@ 2026-01-15  2:28 Manuel A. Fernandez Montecelo
  2026-01-15  2:28 ` [PATCH BlueZ v3 1/1] " Manuel A. Fernandez Montecelo
  0 siblings, 1 reply; 6+ messages in thread
From: Manuel A. Fernandez Montecelo @ 2026-01-15  2:28 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Manuel A. Fernandez Montecelo

Support config fragments, to read config from conf.d directories.

This is commonly supported by other tools, as an extension of the main
config file(s).  It is useful and convenient in several situations,
for example:

* distributions can set different values from the defaults shipped
  upstream, without having to modify the config file

* different packages or config-management tools can change config just
  by adding, removing or modifying files in that directory; instead of
  editing the main config files

---
Notes on v3 non-changes:

I considered other changes for v3 as requested in message
https://marc.info/?l=linux-bluetooth&m=176557679130526 , but as
explained in my next reply
(https://marc.info/?l=linux-bluetooth&m=176558570102172&w=2), I didn't
understand part of the request, so in the meantime I tried my best to
interpret, to make some progress.

Experimenting and pondering for quite a while about the question of how
to do the loading and parsing (as discussed in those messages), I came
to the conclusion that in general it makes sense to keep the main idea
of this patches' implementation:
* to consider the config fragments (files in conf.d) part of the config
  in each area (an extension of main.conf, input.conf, etc);
* treat the whole set as a single unit, loading base and then one
  fragment after another in place for that area (which means also
  parsing the config file in text to convert it to a key_file in mem);
* and only after an area is complete and all bits are in, invoke
  parse_config(main_conf)-like functions to transfer values to code
  variables, and do it only once.

Problems mentioned in the message, like wrong groups in the base
main.conf being then possible in the fragments, should not have a bad
effect in practice -- there are specific checks with hardcoded "valid
groups" and "keys" for each area.  And if the values of the keys
overriden are not valid, it will also be noticed later in the
processing.

Another problem, perhaps more theoretical (I don't think that it will
have much impact in the real-world, but nevertheless somewhat wasteful
effort), is to do the loading and parsing over and over.
parse_config()-like functions are trying to find values for all possible
keys, that in case of fragments typically do not exist -- because
fragments typically only contain one, or perhaps a handful of key=values
only.  As I see it, the pre-existing code is based on the idea that the
key_file representing each section (main.conf, input.conf, etc) contains
all the config for that area, and it only loads and parses once each
area.

As an additional, more practical problem, for example main.conf (as
represented in a key_file) is preserved for the whole lifetime of the
program running, and there's the function:
  GKeyFile *btd_get_main_conf(void) {
          return main_conf;
  }

Then plugins/policy.c invokes this function and acts upon the values.  I
think that the returned value of that function should include the extra
config fragments, and not only exclusively the representation of the
original main.conf file, both in theoretical terms but also in practice
-- because otherwise this policy.c would not be able to act upon config
coming via fragments.

So, all in all, I think that merging all loaded config in a single
key_file, and then transferring those values to program variables only
once, is roughly the correct approach.

But I'm completely open to change it if you are still not convinced or
see other problems of this approach.  And please clarify your previous
message if I misunderstood.

---
Changes in v3:
- Minor rephrasing of commit message, fixing conf paths and others
- Emit DBG message if network.conf fails to parse (silently ignored now)
- Add err parameter to confd_process_config(), modify returned message
  to include file that fails to parse, and check returned errors from
  calling sites.
- On the calling sites, next after loading the base config file, act on
  errors in the same way as an err when loading each base config file
  (the effect in all cases is to use the default hardcoded values, when
  config is not present / not loadable)

Changes in v2:
- Use alternate functions to not change (raise) required Glib version
- Reformat code (styling)

---
Manuel A. Fernandez Montecelo (1):
  Support for config fragments (conf.d style dirs)

 Makefile.am                |   1 +
 profiles/input/hog.c       |   9 ++
 profiles/input/manager.c   |   9 ++
 profiles/network/manager.c |   9 ++
 src/conf_d.c               | 208 +++++++++++++++++++++++++++++++++++++
 src/conf_d.h               |  83 +++++++++++++++
 src/main.c                 |   9 ++
 7 files changed, 328 insertions(+)
 create mode 100644 src/conf_d.c
 create mode 100644 src/conf_d.h

-- 
2.51.0


^ permalink raw reply	[flat|nested] 6+ messages in thread
* [PATCH BlueZ v2 1/1] Support for config fragments (conf.d style dirs)
@ 2025-12-12 20:22 Manuel A. Fernandez Montecelo
  2025-12-12 21:28 ` bluez.test.bot
  0 siblings, 1 reply; 6+ messages in thread
From: Manuel A. Fernandez Montecelo @ 2025-12-12 20:22 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Manuel A. Fernandez Montecelo

Support config fragments, to read config from conf.d directories.
Those dirs will be main.conf.d for main.conf, analog for input.conf
and network.conf.

This is commonly supported by other tools, as an extension of the main
config file(s).  It is useful and convenient in several situations,
for example:

* distributions can set different values from the defaults shipped
  upstream, without having to modify the config file

* different packages or config-management tools can change config just
  by adding, removing or modifying files in that directory; instead of
  editing the main config files

The main or base config files will be processed first, and then files
in the conf.d dirs, if existing.

When reading these config files in conf.d dirs, they override values
for keys in the base config files (or default config set in code).
For example, for "main.conf" the directory to be processed will be
"main.conf.d", in the same basedir as the config file
(e.g. /etc/main.conf, /etc/main.conf.d/).  The same for input.conf and
network.conf.

Within the conf.d directory, the format of the filename should be
'^([0-9][0-9])-([a-zA-Z0-9-_])*\.conf$', that is, starting with "00-"
to "99-", ending in ".conf", and with a mix of alphanumeric characters
with dashes and underscores in between.  For example:
'01-override-general-secureconnections.conf'.

Files named differently will not be considered, and accepting groups
or keys not present in the base config depends on the code, currently
set to "NOT accept new groups" but "YES to accept new keys".  This is
because the base config files contain all the groups, but most keys
are commented-out, with the values set in code.

The candidate files within the given directory are sorted (with
g_strcmp0(), so the ordering will be as with strcmp()).  The
configuration in the files being processed later will override
previous config, in particular the main/base config files, but also
the one from previous files processed, if the Group and Key coincide.

For example, consider 'main.conf' that contains the defaults:

  [General]
  DiscoverableTimeout=0
  PairableTimeout=0

and there is a file 'main.conf.d/70-default-timeouts-vendor.conf'
containing settings for these keys:

  [General]
  DiscoverableTimeout=30
  PairableTimeout=30

and another 'main.conf.d/99-default-timeouts-local.conf'
containing settings only for 'PairableTimeout':

  [General]
  PairableTimeout=15

What happens is:
1) First, the 'main.conf' is processed as usual;
2) then 'main.conf.d/70-default-timeouts-vendor.conf' is processed,
   overriding the two values from the main config file with the given
   values;
3) and finally 'main.conf.d/99-default-timeouts-local.conf' is
   processed, overriding once again only 'PairableTimeout'.

The final, effective values are:

  DiscoverableTimeout=30
  PairableTimeout=15
---
 Makefile.am                |   1 +
 profiles/input/hog.c       |   3 +
 profiles/input/manager.c   |   3 +
 profiles/network/manager.c |   3 +
 src/conf_d.c               | 202 +++++++++++++++++++++++++++++++++++++
 src/conf_d.h               |  76 ++++++++++++++
 src/main.c                 |   3 +
 7 files changed, 291 insertions(+)
 create mode 100644 src/conf_d.c
 create mode 100644 src/conf_d.h

diff --git a/Makefile.am b/Makefile.am
index e152ae648..f8516bacd 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -307,6 +307,7 @@ pkglibexec_PROGRAMS += src/bluetoothd
 bluetoothd_internal_sources = \
 			$(attrib_sources) $(btio_sources) \
 			src/log.h src/log.c \
+			src/conf_d.h src/conf_d.c \
 			src/backtrace.h src/backtrace.c \
 			src/rfkill.c src/btd.h src/sdpd.h \
 			src/sdpd-server.c src/sdpd-request.c \
diff --git a/profiles/input/hog.c b/profiles/input/hog.c
index f50a0f217..6d2348c26 100644
--- a/profiles/input/hog.c
+++ b/profiles/input/hog.c
@@ -39,6 +39,7 @@
 #include "src/shared/att.h"
 #include "src/shared/gatt-client.h"
 #include "src/plugin.h"
+#include "src/conf_d.h"
 
 #include "suspend.h"
 #include "attrib/att.h"
@@ -246,6 +247,8 @@ static void hog_read_config(void)
 		return;
 	}
 
+	confd_process_config(config, filename, FALSE, TRUE);
+
 	config_auto_sec = g_key_file_get_boolean(config, "General",
 					"LEAutoSecurity", &err);
 	if (!err) {
diff --git a/profiles/input/manager.c b/profiles/input/manager.c
index 0fcd6728c..68691ec52 100644
--- a/profiles/input/manager.c
+++ b/profiles/input/manager.c
@@ -27,6 +27,7 @@
 #include "src/device.h"
 #include "src/profile.h"
 #include "src/service.h"
+#include "src/conf_d.h"
 
 #include "device.h"
 #include "server.h"
@@ -75,6 +76,8 @@ static GKeyFile *load_config_file(const char *file)
 		return NULL;
 	}
 
+	confd_process_config(keyfile, file, FALSE, TRUE);
+
 	return keyfile;
 }
 
diff --git a/profiles/network/manager.c b/profiles/network/manager.c
index 693547d45..e9b620b0f 100644
--- a/profiles/network/manager.c
+++ b/profiles/network/manager.c
@@ -28,6 +28,7 @@
 #include "src/device.h"
 #include "src/profile.h"
 #include "src/service.h"
+#include "src/conf_d.h"
 
 #include "bnep.h"
 #include "connection.h"
@@ -47,6 +48,8 @@ static void read_config(const char *file)
 		goto done;
 	}
 
+	confd_process_config(keyfile, file, FALSE, TRUE);
+
 	conf_security = !g_key_file_get_boolean(keyfile, "General",
 						"DisableSecurity", &err);
 	if (err) {
diff --git a/src/conf_d.c b/src/conf_d.c
new file mode 100644
index 000000000..878fb9157
--- /dev/null
+++ b/src/conf_d.c
@@ -0,0 +1,202 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ *
+ *  BlueZ - Bluetooth protocol stack for Linux
+ *
+ *  Copyright (C) 2025  Valve Corporation
+ *
+ */
+
+#include "conf_d.h"
+
+#include "src/log.h"
+
+static gint confd_compare_filenames(gconstpointer a, gconstpointer b)
+{
+	return g_strcmp0(*(const gchar **)(a), *(const gchar **)(b));
+}
+
+static GPtrArray *confd_get_valid_files_sorted(const gchar *confd_path)
+{
+	const char *regex_pattern = "^([0-9][0-9])-([a-zA-Z0-9-_])*\\.conf$";
+	g_autoptr(GRegex) regex = NULL;
+	g_autoptr(GPtrArray) ret_confd_files = NULL;
+	GDir *dir = NULL;
+	GError *error = NULL;
+	const gchar *filename = NULL;
+
+	regex = g_regex_new(regex_pattern, 0, 0, &error);
+	if (!regex) {
+		DBG("Invalid regex: %s", error->message);
+		g_clear_error(&error);
+		return NULL;
+	}
+
+	dir = g_dir_open(confd_path, 0, &error);
+	if (!dir) {
+		DBG("%s", error->message);
+		g_clear_error(&error);
+		return NULL;
+	}
+
+	ret_confd_files = g_ptr_array_new_full(0, g_free);
+
+	while ((filename = g_dir_read_name(dir)) != NULL) {
+		g_autofree gchar *file_path = NULL;
+
+		if (!g_regex_match(regex, filename, 0, NULL)) {
+			DBG("Ignoring file in conf.d dir: '%s'", filename);
+			continue;
+		}
+
+		file_path = g_build_filename(confd_path, filename, NULL);
+		if (file_path)
+			g_ptr_array_add(ret_confd_files, g_strdup(file_path));
+	}
+
+	g_dir_close(dir);
+
+	if (ret_confd_files && ret_confd_files->len > 0) {
+		g_ptr_array_sort(ret_confd_files, confd_compare_filenames);
+
+		DBG("Will consider additional config files (in order):");
+		for (guint i = 0; i < ret_confd_files->len; i++) {
+			DBG(" - %s",
+			    (const gchar *)(g_ptr_array_index(ret_confd_files,
+							      i)));
+		}
+
+		return g_ptr_array_ref(ret_confd_files);
+	} else {
+		g_ptr_array_free(ret_confd_files, TRUE);
+		ret_confd_files = NULL;
+		return NULL;
+	}
+}
+
+static void confd_override_config(GKeyFile *keyfile,
+				  const gchar *new_conf_file_path,
+				  gboolean accept_new_groups,
+				  gboolean accept_new_keys)
+{
+	g_autoptr(GKeyFile) new_keyfile = NULL;
+	gchar **existing_groups = NULL;
+	gchar **groups = NULL;
+	gchar **keys = NULL;
+	gsize existing_groups_size = 0;
+	gsize groups_size = 0;
+	gsize keys_size = 0;
+	g_autoptr(GError) error = NULL;
+
+	new_keyfile = g_key_file_new();
+	if (!g_key_file_load_from_file(new_keyfile, new_conf_file_path,
+				       G_KEY_FILE_NONE, &error)) {
+		if (error) {
+			warn("%s", error->message);
+			g_clear_error(&error);
+		}
+		return;
+	}
+
+	existing_groups = g_key_file_get_groups(keyfile, &existing_groups_size);
+
+	groups = g_key_file_get_groups(new_keyfile, &groups_size);
+	for (gsize gi = 0; gi < groups_size; gi++) {
+		bool match = false;
+		const gchar *group = groups[gi];
+
+		for (gsize egi = 0; egi < existing_groups_size; egi++) {
+			if (g_str_equal(group, existing_groups[egi])) {
+				match = true;
+				break;
+			}
+		}
+
+		if (!match) {
+			if (accept_new_groups == FALSE) {
+				warn("Skipping group '%s' in '%s' "
+				     "not known in previous config",
+				     group, new_conf_file_path);
+				continue;
+			} else {
+				DBG("Accepting group '%s' in '%s' "
+				    "not known in previous config",
+				    group, new_conf_file_path);
+			}
+		}
+
+		keys = g_key_file_get_keys(new_keyfile, group, &keys_size,
+					   NULL);
+		if (keys == NULL) {
+			DBG("No keys found in '%s' for group '%s'",
+			    new_conf_file_path, group);
+			continue;
+		}
+
+		for (gsize ki = 0; ki < keys_size; ki++) {
+			const gchar *key = keys[ki];
+			g_autofree gchar *value = NULL;
+			g_autofree gchar *old_value = NULL;
+
+			value = g_key_file_get_value(new_keyfile, group, key,
+						     NULL);
+			if (!value)
+				continue;
+
+			old_value =
+				g_key_file_get_value(keyfile, group, key, NULL);
+			if (old_value != NULL) {
+				DBG("Overriding config value from "
+				    "conf.d file: [%s] %s: '%s'->'%s'",
+				    group, key, old_value, value);
+				g_key_file_set_value(keyfile, group, key,
+						     value);
+			} else {
+				if (accept_new_keys == TRUE) {
+					DBG("Adding new config value from "
+					    "conf.d file: [%s] %s: '%s'",
+					    group, key, value);
+					g_key_file_set_value(keyfile, group,
+							     key, value);
+				} else {
+					DBG("Ignoring config value from "
+					    "conf.d, unknown keys not allowed: "
+					    "[%s] %s: '%s'",
+					    group, key, value);
+				}
+			}
+		}
+		g_strfreev(keys);
+	}
+	g_strfreev(groups);
+	g_strfreev(existing_groups);
+}
+
+void confd_process_config(GKeyFile *keyfile, const gchar *base_conf_file_path,
+			  gboolean accept_new_groups, gboolean accept_new_keys)
+{
+	g_autofree gchar *confd_path = NULL;
+	g_autoptr(GPtrArray) confd_files = NULL;
+
+	confd_path = g_strconcat(base_conf_file_path, ".d", NULL);
+
+	if (!g_file_test(confd_path,
+			 (G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) {
+		DBG("'%s' does not exist or not a directory", confd_path);
+		return;
+	}
+
+	confd_files = confd_get_valid_files_sorted(confd_path);
+
+	if (confd_files && confd_files->len > 0) {
+		for (guint i = 0; i < confd_files->len; i++) {
+			const gchar *confd_file =
+				(const gchar *)(g_ptr_array_index(confd_files,
+								  i));
+			DBG("Processing config file: '%s'", confd_file);
+			confd_override_config(keyfile, confd_file,
+					      accept_new_groups,
+					      accept_new_keys);
+		}
+	}
+}
diff --git a/src/conf_d.h b/src/conf_d.h
new file mode 100644
index 000000000..812966740
--- /dev/null
+++ b/src/conf_d.h
@@ -0,0 +1,76 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ *
+ *  BlueZ - Bluetooth protocol stack for Linux
+ *
+ *  Copyright (C) 2025  Valve Corporation
+ *
+ */
+
+#include <glib.h>
+
+/**
+ * confd_process_config:
+ *
+ * @keyfile: keyfile already initialized and parsed
+ *
+ * @base_conf_file_path: base config file (e.g. /etc/bluetooth/main.conf,
+ * input.conf, network.conf).  The directory to be processed will be same path
+ * with ".d" appended.
+ *
+ * @accept_new_groups: whether to accept groups not appearing in the base config
+ * file
+ *
+ * @accept_new_keys: whether to accept keys not appearing in the base config
+ * file
+ *
+ * Helper function to process config files in conf.d style dirs (config
+ * fragments), overriding values for keys in the base config files (or default
+ * config set in code).  For example, for "main.conf" the directory to be
+ * processed will be "main.conf.d", in the same basedir as the config file.
+ *
+ * Within the .d directory, the format of the filename should be
+ * '^([0-9][0-9])-([a-zA-Z0-9-_])*\.conf$', that is, starting with "00-" to
+ * "99-", ending in ".conf", and with a mix of alphanumeric characters with
+ * dashes and underscores in between.  For example:
+ * '01-override-general-secureconnections.conf'.
+ *
+ * Files named differently will not be considered, and accepting groups or keys
+ * not present in the base config depends on the function arguments.
+ *
+ * The candidate files within the given directory are sorted (with g_strcmp0(),
+ * so the ordering will be as with strcmp()).  The configuration in the files
+ * being processed later will override previous config, in particular the main
+ * config, but also the one from previous files processed, if the Group and Key
+ * coincide.
+ *
+ * For example, consider 'main.conf' that contains the defaults:
+ *   [General]
+ *   DiscoverableTimeout=0
+ *   PairableTimeout=0
+ *
+ * and there is a file 'main.conf.d/70-default-timeouts-vendor.conf'
+ * containing settings for these keys:
+ *   [General]
+ *   DiscoverableTimeout=30
+ *   PairableTimeout=30
+ *
+ * and another 'main.conf.d/99-default-timeouts-local.conf'
+ * containing settings only for 'PairableTimeout':
+ *   [General]
+ *   PairableTimeout=15
+ *
+ * What happens is:
+ * 1) First, the 'main.conf' is processed as usual;
+ * 2) then 'main.conf.d/70-default-timeouts-vendor.conf' is processed,
+ *    overriding the two values from the main config file with the given values;
+ * 3) and finally 'main.conf.d/99-default-timeouts-local.conf' is processed,
+ *    overriding once again only 'PairableTimeout'.
+ *
+ * The final, effective values are:
+ *
+ *   DiscoverableTimeout=30
+ *
+ **/
+void confd_process_config(GKeyFile *keyfile, const gchar *base_conf_file_path,
+			  gboolean accept_new_groups, gboolean accept_new_keys);
diff --git a/src/main.c b/src/main.c
index 61e5ef983..79866616e 100644
--- a/src/main.c
+++ b/src/main.c
@@ -54,6 +54,7 @@
 #include "dbus-common.h"
 #include "agent.h"
 #include "profile.h"
+#include "conf_d.h"
 
 #define BLUEZ_NAME "org.bluez"
 
@@ -284,6 +285,8 @@ static GKeyFile *load_config(const char *name)
 		return NULL;
 	}
 
+	confd_process_config(keyfile, main_conf_file_path, FALSE, TRUE);
+
 	return keyfile;
 }
 
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread
* [PATCH BlueZ 1/1] Support for config fragments (conf.d style dirs)
@ 2025-12-11 21:13 Manuel A. Fernandez Montecelo
  2025-12-11 21:20 ` bluez.test.bot
  0 siblings, 1 reply; 6+ messages in thread
From: Manuel A. Fernandez Montecelo @ 2025-12-11 21:13 UTC (permalink / raw)
  To: linux-bluetooth; +Cc: Manuel A. Fernandez Montecelo

Support config fragments, to read config from conf.d directories.
Those dirs will be main.conf.d for main.conf, analog for input.conf
and network.conf.

This is commonly supported by other tools, as an extension of the main
config file(s).  It is useful and convenient in several situations,
for example:

- distributions can set different values from the defaults shipped
  upstream, without having to modify the config file

- different packages or config-management tools can change config just
  by adding, removing or modifying files in that directory; instead of
  editing the main config files

The main or base config files will be processed first, and then files
in the conf.d dirs, if existing.

When reading these config files in conf.d dirs, they override values
for keys in the base config files (or default config set in code).
For example, for "main.conf" the directory to be processed will be
"main.conf.d", in the same basedir as the config file
(e.g. /etc/main.conf, /etc/main.conf.d/).  The same for input.conf and
network.conf.

Within the conf.d directory, the format of the filename should be
'^([0-9][0-9])-([a-zA-Z0-9-_])*\.conf$', that is, starting with "00-"
to "99-", ending in ".conf", and with a mix of alphanumeric characters
with dashes and underscores in between.  For example:
'01-override-general-secureconnections.conf'.

Files named differently will not be considered, and accepting groups
or keys not present in the base config depends on the code, currently
set to "NOT accept new groups" but "YES to accept new keys".  This is
because the base config files contain all the groups, but most keys
are commented-out, with the values set in code.

The candidate files within the given directory are sorted (with
g_strcmp0(), so the ordering will be as with strcmp()).  The
configuration in the files being processed later will override
previous config, in particular the main/base config files, but also
the one from previous files processed, if the Group and Key coincide.

For example, consider 'main.conf' that contains the defaults:

  [General]
  DiscoverableTimeout=0
  PairableTimeout=0

and there is a file 'main.conf.d/70-default-timeouts-vendor.conf'
containing settings for these keys:

  [General]
  DiscoverableTimeout=30
  PairableTimeout=30

and another 'main.conf.d/99-default-timeouts-local.conf'
containing settings only for 'PairableTimeout':

  [General]
  PairableTimeout=15

What happens is:
1) First, the 'main.conf' is processed as usual;
2) then 'main.conf.d/70-default-timeouts-vendor.conf' is processed,
   overriding the two values from the main config file with the given
   values;
3) and finally 'main.conf.d/99-default-timeouts-local.conf' is
   processed, overriding once again only 'PairableTimeout'.

The final, effective values are:

  DiscoverableTimeout=30
  PairableTimeout=15
---
 Makefile.am                |   1 +
 acinclude.m4               |   4 +-
 profiles/input/hog.c       |   3 +
 profiles/input/manager.c   |   3 +
 profiles/network/manager.c |   3 +
 src/conf_d.c               | 177 +++++++++++++++++++++++++++++++++++++
 src/conf_d.h               |  77 ++++++++++++++++
 src/main.c                 |   3 +
 8 files changed, 269 insertions(+), 2 deletions(-)
 create mode 100644 src/conf_d.c
 create mode 100644 src/conf_d.h

diff --git a/Makefile.am b/Makefile.am
index e152ae648..f8516bacd 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -307,6 +307,7 @@ pkglibexec_PROGRAMS += src/bluetoothd
 bluetoothd_internal_sources = \
 			$(attrib_sources) $(btio_sources) \
 			src/log.h src/log.c \
+			src/conf_d.h src/conf_d.c \
 			src/backtrace.h src/backtrace.c \
 			src/rfkill.c src/btd.h src/sdpd.h \
 			src/sdpd-server.c src/sdpd-request.c \
diff --git a/acinclude.m4 b/acinclude.m4
index 8046c9a7d..560a5d44b 100644
--- a/acinclude.m4
+++ b/acinclude.m4
@@ -63,8 +63,8 @@ AC_DEFUN([COMPILER_FLAGS], [
 		with_cflags="$with_cflags -Wformat -Wformat-security"
 		with_cflags="$with_cflags -Wstringop-overflow"
 		with_cflags="$with_cflags -DG_DISABLE_DEPRECATED"
-		with_cflags="$with_cflags -DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_36"
-		with_cflags="$with_cflags -DGLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_36"
+		with_cflags="$with_cflags -DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_76"
+		with_cflags="$with_cflags -DGLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_76"
 	fi
 	AC_SUBST([WARNING_CFLAGS], $with_cflags)
 ])
diff --git a/profiles/input/hog.c b/profiles/input/hog.c
index f50a0f217..6d2348c26 100644
--- a/profiles/input/hog.c
+++ b/profiles/input/hog.c
@@ -39,6 +39,7 @@
 #include "src/shared/att.h"
 #include "src/shared/gatt-client.h"
 #include "src/plugin.h"
+#include "src/conf_d.h"
 
 #include "suspend.h"
 #include "attrib/att.h"
@@ -246,6 +247,8 @@ static void hog_read_config(void)
 		return;
 	}
 
+	confd_process_config(config, filename, FALSE, TRUE);
+
 	config_auto_sec = g_key_file_get_boolean(config, "General",
 					"LEAutoSecurity", &err);
 	if (!err) {
diff --git a/profiles/input/manager.c b/profiles/input/manager.c
index 0fcd6728c..68691ec52 100644
--- a/profiles/input/manager.c
+++ b/profiles/input/manager.c
@@ -27,6 +27,7 @@
 #include "src/device.h"
 #include "src/profile.h"
 #include "src/service.h"
+#include "src/conf_d.h"
 
 #include "device.h"
 #include "server.h"
@@ -75,6 +76,8 @@ static GKeyFile *load_config_file(const char *file)
 		return NULL;
 	}
 
+	confd_process_config(keyfile, file, FALSE, TRUE);
+
 	return keyfile;
 }
 
diff --git a/profiles/network/manager.c b/profiles/network/manager.c
index 693547d45..e9b620b0f 100644
--- a/profiles/network/manager.c
+++ b/profiles/network/manager.c
@@ -28,6 +28,7 @@
 #include "src/device.h"
 #include "src/profile.h"
 #include "src/service.h"
+#include "src/conf_d.h"
 
 #include "bnep.h"
 #include "connection.h"
@@ -47,6 +48,8 @@ static void read_config(const char *file)
 		goto done;
 	}
 
+	confd_process_config(keyfile, file, FALSE, TRUE);
+
 	conf_security = !g_key_file_get_boolean(keyfile, "General",
 						"DisableSecurity", &err);
 	if (err) {
diff --git a/src/conf_d.c b/src/conf_d.c
new file mode 100644
index 000000000..0ce06ee83
--- /dev/null
+++ b/src/conf_d.c
@@ -0,0 +1,177 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ *
+ *  BlueZ - Bluetooth protocol stack for Linux
+ *
+ *  Copyright (C) 2025  Valve Corporation
+ *
+ */
+
+#include "conf_d.h"
+
+#include "src/log.h"
+
+
+static gint confd_compare_filenames(gconstpointer a, gconstpointer b)
+{
+	return g_strcmp0((const gchar*)a, (const gchar*)b);
+}
+
+static GPtrArray *confd_get_valid_files_sorted(const gchar *confd_path)
+{
+	const char *regex_pattern = "^([0-9][0-9])-([a-zA-Z0-9-_])*\\.conf$";
+	g_autoptr (GRegex) regex = NULL;
+	g_autoptr (GPtrArray) ret_confd_files = NULL;
+	GDir *dir = NULL;
+	GError *error = NULL;
+	const gchar *filename = NULL;
+
+	regex = g_regex_new(regex_pattern, G_REGEX_DEFAULT, G_REGEX_MATCH_DEFAULT, &error);
+	if (!regex) {
+		DBG("Invalid regex: %s", error->message);
+		g_clear_error(&error);
+		return NULL;
+	}
+
+	dir = g_dir_open(confd_path, 0, &error);
+	if (!dir) {
+		DBG("%s", error->message);
+		g_clear_error(&error);
+		return NULL;
+	}
+
+	ret_confd_files = g_ptr_array_new_full(0, g_free);
+
+	while ((filename = g_dir_read_name(dir)) != NULL) {
+		g_autofree gchar *file_path = NULL;
+
+		if (!g_regex_match(regex, filename, G_REGEX_MATCH_DEFAULT, NULL)) {
+			DBG("Ignoring file in conf.d dir: '%s'", filename);
+			continue;
+		}
+
+		file_path = g_build_filename(confd_path, filename, NULL);
+		if (file_path)
+			g_ptr_array_add(ret_confd_files, g_strdup(file_path));
+	}
+
+	g_dir_close(dir);
+
+	if (ret_confd_files && ret_confd_files->len > 0) {
+		g_ptr_array_sort_values(ret_confd_files, confd_compare_filenames);
+
+		DBG("Will consider additional config files (in order):");
+		for (guint i = 0; i < ret_confd_files->len; i++) {
+			DBG(" - %s", (const gchar*)(g_ptr_array_index(ret_confd_files, i)));
+		}
+
+		return g_ptr_array_ref(ret_confd_files);
+	} else {
+		g_ptr_array_free(ret_confd_files, TRUE);
+		ret_confd_files = NULL;
+		return NULL;
+	}
+}
+
+static void confd_override_config(GKeyFile *keyfile, const gchar *new_conf_file_path, gboolean accept_new_groups, gboolean accept_new_keys)
+{
+	g_autoptr (GKeyFile) new_keyfile = NULL;
+	gchar **existing_groups = NULL;
+	gchar **groups = NULL;
+	gchar **keys = NULL;
+	gsize existing_groups_size = 0;
+	gsize groups_size = 0;
+	gsize keys_size = 0;
+	g_autoptr (GError) error = NULL;
+
+	new_keyfile = g_key_file_new();
+	if (!g_key_file_load_from_file(new_keyfile, new_conf_file_path, G_KEY_FILE_NONE, &error)) {
+		if (error) {
+			warn("%s", error->message);
+			g_clear_error(&error);
+		}
+	        return;
+	}
+
+	existing_groups = g_key_file_get_groups(keyfile, &existing_groups_size);
+
+	groups = g_key_file_get_groups(new_keyfile, &groups_size);
+	for (gsize gi = 0; gi < groups_size; gi++) {
+		bool match = false;
+		const gchar *group = groups[gi];
+
+		for (gsize egi = 0; egi < existing_groups_size; egi++) {
+			if (g_str_equal(group, existing_groups[egi])) {
+				match = true;
+				break;
+			}
+		}
+
+		if (!match) {
+			if (accept_new_groups == FALSE) {
+				warn("Skipping group '%s' in '%s' not known in previous config", group, new_conf_file_path);
+				continue;
+			} else {
+				DBG("Accepting group '%s' in '%s' not known in previous config", group, new_conf_file_path);
+			}
+		}
+
+		keys = g_key_file_get_keys(new_keyfile, group, &keys_size, NULL);
+		if (keys == NULL) {
+			DBG("No keys found in '%s' for group '%s'", new_conf_file_path, group);
+			continue;
+		}
+
+		for (gsize ki = 0; ki < keys_size; ki++) {
+			const gchar *key = keys[ki];
+			g_autofree gchar *value = NULL;
+			g_autofree gchar *old_value = NULL;
+
+			value = g_key_file_get_value(new_keyfile, group, key, NULL);
+			if (!value)
+				continue;
+
+			old_value = g_key_file_get_value(keyfile, group, key, NULL);
+			if (old_value != NULL) {
+				DBG("Overriding config value from conf.d file: [%s] %s: '%s'->'%s'",
+				    group, key, old_value, value);
+				g_key_file_set_value(keyfile, group, key, value);
+			} else {
+				if (accept_new_keys == TRUE) {
+					DBG("Adding new config value from conf.d file: [%s] %s: '%s'",
+					    group, key, value);
+					g_key_file_set_value(keyfile, group, key, value);
+				} else {
+					DBG("Ignoring config value from conf.d, unknown keys not allowed: [%s] %s: '%s'",
+					    group, key, value);
+				}
+			}
+		}
+		g_strfreev(keys);
+	}
+	g_strfreev(groups);
+	g_strfreev(existing_groups);
+}
+
+void confd_process_config(GKeyFile *keyfile, const gchar *base_conf_file_path, gboolean accept_new_groups, gboolean accept_new_keys)
+{
+	g_autofree gchar *confd_path = NULL;
+	g_autoptr (GPtrArray) confd_files = NULL;
+
+	confd_path = g_strconcat(base_conf_file_path, ".d", NULL);
+
+	if (!g_file_test(confd_path, (G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) {
+		DBG("'%s' does not exist or not a directory", confd_path);
+		return;
+	}
+
+	confd_files = confd_get_valid_files_sorted(confd_path);
+
+	if (confd_files && confd_files->len > 0) {
+		for (guint i = 0; i < confd_files->len; i++) {
+			const gchar* confd_file = (const gchar*)(g_ptr_array_index(confd_files, i));
+			DBG("Processing config file: '%s'", confd_file);
+			confd_override_config(keyfile, confd_file, accept_new_groups, accept_new_keys);
+		}
+	}
+}
diff --git a/src/conf_d.h b/src/conf_d.h
new file mode 100644
index 000000000..563d303d8
--- /dev/null
+++ b/src/conf_d.h
@@ -0,0 +1,77 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ *
+ *  BlueZ - Bluetooth protocol stack for Linux
+ *
+ *  Copyright (C) 2025  Valve Corporation
+ *
+ */
+
+#include <glib.h>
+
+
+/**
+ * confd_process_config:
+ *
+ * @keyfile: keyfile already initialized and parsed
+ *
+ * @base_conf_file_path: base config file (e.g. /etc/bluetooth/main.conf,
+ * input.conf, network.conf).  The directory to be processed will be same path
+ * with ".d" appended.
+ *
+ * @accept_new_groups: whether to accept groups not appearing in the base
+ * config file
+ *
+ * @accept_new_keys: whether to accept keys not appearing in the base
+ * config file
+ *
+ * Helper function to process config files in conf.d style dirs (config
+ * fragments), overriding values for keys in the base config files (or default
+ * config set in code).  For example, for "main.conf" the directory to be
+ * processed will be "main.conf.d", in the same basedir as the config file.
+ *
+ * Within the .d directory, the format of the filename should be
+ * '^([0-9][0-9])-([a-zA-Z0-9-_])*\.conf$', that is, starting with "00-" to
+ * "99-", ending in ".conf", and with a mix of alphanumeric characters with
+ * dashes and underscores in between.  For example:
+ * '01-override-general-secureconnections.conf'.
+ *
+ * Files named differently will not be considered, and accepting groups or keys
+ * not present in the base config depends on the function arguments.
+ *
+ * The candidate files within the given directory are sorted (with g_strcmp0(),
+ * so the ordering will be as with strcmp()).  The configuration in the files
+ * being processed later will override previous config, in particular the main
+ * config, but also the one from previous files processed, if the Group and Key
+ * coincide.
+ *
+ * For example, consider 'main.conf' that contains the defaults:
+ *   [General]
+ *   DiscoverableTimeout=0
+ *   PairableTimeout=0
+ *
+ * and there is a file 'main.conf.d/70-default-timeouts-vendor.conf'
+ * containing settings for these keys:
+ *   [General]
+ *   DiscoverableTimeout=30
+ *   PairableTimeout=30
+ *
+ * and another 'main.conf.d/99-default-timeouts-local.conf'
+ * containing settings only for 'PairableTimeout':
+ *   [General]
+ *   PairableTimeout=15
+ *
+ * What happens is:
+ * 1) First, the 'main.conf' is processed as usual;
+ * 2) then 'main.conf.d/70-default-timeouts-vendor.conf' is processed,
+ *    overriding the two values from the main config file with the given
+ *    values;
+ * 3) and finally 'main.conf.d/99-default-timeouts-local.conf' is
+ *    processed, overriding once again only 'PairableTimeout'.
+ *
+ * The final, effective values are:
+ *
+ *   DiscoverableTimeout=30
+ *
+ **/
+void confd_process_config(GKeyFile *keyfile, const gchar *base_conf_file_path, gboolean accept_new_groups, gboolean accept_new_keys);
diff --git a/src/main.c b/src/main.c
index 61e5ef983..79866616e 100644
--- a/src/main.c
+++ b/src/main.c
@@ -54,6 +54,7 @@
 #include "dbus-common.h"
 #include "agent.h"
 #include "profile.h"
+#include "conf_d.h"
 
 #define BLUEZ_NAME "org.bluez"
 
@@ -284,6 +285,8 @@ static GKeyFile *load_config(const char *name)
 		return NULL;
 	}
 
+	confd_process_config(keyfile, main_conf_file_path, FALSE, TRUE);
+
 	return keyfile;
 }
 
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-01-15 10:21 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-01-15  2:28 [PATCH BlueZ v3 0/1] Support for config fragments (conf.d style dirs) Manuel A. Fernandez Montecelo
2026-01-15  2:28 ` [PATCH BlueZ v3 1/1] " Manuel A. Fernandez Montecelo
2026-01-15  3:42   ` bluez.test.bot
2026-01-15 10:21   ` [PATCH BlueZ v3 1/1] " Bastien Nocera
  -- strict thread matches above, loose matches on Subject: below --
2025-12-12 20:22 [PATCH BlueZ v2 " Manuel A. Fernandez Montecelo
2025-12-12 21:28 ` bluez.test.bot
2025-12-11 21:13 [PATCH BlueZ 1/1] " Manuel A. Fernandez Montecelo
2025-12-11 21:20 ` bluez.test.bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox