mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2025-12-20 15:50:03 +01:00
With this change, it is possible to have a top-level object that does not start at exactly the beginning of the file, allowing comments and whitespace to exist. Also add an empty conf file in the tests to verify that it does not crash.
602 lines
18 KiB
C
602 lines
18 KiB
C
/* WirePlumber
|
|
*
|
|
* Copyright © 2022 Collabora Ltd.
|
|
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
|
*
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
|
|
#include "conf.h"
|
|
#include "log.h"
|
|
#include "json-utils.h"
|
|
#include "base-dirs.h"
|
|
#include "error.h"
|
|
|
|
#include <pipewire/pipewire.h>
|
|
#include <spa/utils/result.h>
|
|
|
|
WP_DEFINE_LOCAL_LOG_TOPIC ("wp-conf")
|
|
|
|
#define OVERRIDE_SECTION_PREFIX "override."
|
|
|
|
/*! \defgroup wpconf WpConf */
|
|
/*!
|
|
* \struct WpConf
|
|
*
|
|
* WpConf allows accessing the different sections of the wireplumber
|
|
* configuration.
|
|
*/
|
|
|
|
typedef struct _WpConfSection WpConfSection;
|
|
struct _WpConfSection
|
|
{
|
|
gchar *name;
|
|
WpSpaJson *value;
|
|
gchar *location;
|
|
};
|
|
|
|
static void
|
|
wp_conf_section_clear (WpConfSection * section)
|
|
{
|
|
g_free (section->name);
|
|
g_clear_pointer (§ion->value, wp_spa_json_unref);
|
|
g_free (section->location);
|
|
}
|
|
G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (WpConfSection, wp_conf_section_clear)
|
|
|
|
struct _WpConf
|
|
{
|
|
GObject parent;
|
|
|
|
/* Props */
|
|
gchar *name;
|
|
WpProperties *properties;
|
|
|
|
/* Private */
|
|
GArray *conf_sections; /* element-type: WpConfSection */
|
|
GPtrArray *files; /* element-type: GMappedFile* */
|
|
};
|
|
|
|
enum {
|
|
PROP_0,
|
|
PROP_NAME,
|
|
PROP_PROPERTIES,
|
|
};
|
|
|
|
G_DEFINE_TYPE (WpConf, wp_conf, G_TYPE_OBJECT)
|
|
|
|
static void
|
|
wp_conf_init (WpConf * self)
|
|
{
|
|
self->conf_sections = g_array_new (FALSE, FALSE, sizeof (WpConfSection));
|
|
g_array_set_clear_func (self->conf_sections, (GDestroyNotify) wp_conf_section_clear);
|
|
self->files = g_ptr_array_new_with_free_func ((GDestroyNotify) g_mapped_file_unref);
|
|
}
|
|
|
|
static void
|
|
wp_conf_set_property (GObject * object, guint property_id,
|
|
const GValue * value, GParamSpec * pspec)
|
|
{
|
|
WpConf *self = WP_CONF (object);
|
|
|
|
switch (property_id) {
|
|
case PROP_NAME:
|
|
self->name = g_value_dup_string (value);
|
|
break;
|
|
case PROP_PROPERTIES:
|
|
self->properties = g_value_dup_boxed (value);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
wp_conf_get_property (GObject * object, guint property_id,
|
|
GValue * value, GParamSpec * pspec)
|
|
{
|
|
WpConf *self = WP_CONF (object);
|
|
|
|
switch (property_id) {
|
|
case PROP_NAME:
|
|
g_value_set_string (value, self->name);
|
|
break;
|
|
case PROP_PROPERTIES:
|
|
g_value_take_boxed (value, wp_properties_copy (self->properties));
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
wp_conf_finalize (GObject * object)
|
|
{
|
|
WpConf *self = WP_CONF (object);
|
|
|
|
wp_conf_close (self);
|
|
g_clear_pointer (&self->properties, wp_properties_unref);
|
|
g_clear_pointer (&self->conf_sections, g_array_unref);
|
|
g_clear_pointer (&self->files, g_ptr_array_unref);
|
|
g_clear_pointer (&self->name, g_free);
|
|
|
|
G_OBJECT_CLASS (wp_conf_parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
wp_conf_class_init (WpConfClass * klass)
|
|
{
|
|
GObjectClass * object_class = G_OBJECT_CLASS (klass);
|
|
|
|
object_class->finalize = wp_conf_finalize;
|
|
object_class->set_property = wp_conf_set_property;
|
|
object_class->get_property = wp_conf_get_property;
|
|
|
|
g_object_class_install_property(object_class, PROP_NAME,
|
|
g_param_spec_string ("name", "name", "The name of the configuration file",
|
|
NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property(object_class, PROP_PROPERTIES,
|
|
g_param_spec_boxed ("properties", "properties", "WpProperties",
|
|
WP_TYPE_PROPERTIES, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
}
|
|
|
|
/*!
|
|
* \brief Creates a new WpConf object
|
|
*
|
|
* This does not open the files, it only creates the object. For most use cases,
|
|
* you should use wp_conf_new_open() instead.
|
|
*
|
|
* \ingroup wpconf
|
|
* \param name the name of the configuration file
|
|
* \param properties (transfer full) (nullable): a WpProperties with keys
|
|
* specifying how to load the WpConf object
|
|
* \returns (transfer full): a new WpConf object
|
|
*/
|
|
WpConf *
|
|
wp_conf_new (const gchar * name, WpProperties * properties)
|
|
{
|
|
g_return_val_if_fail (name, NULL);
|
|
g_autoptr (WpProperties) props = properties;
|
|
return g_object_new (WP_TYPE_CONF, "name", name,
|
|
"properties", props,
|
|
NULL);
|
|
}
|
|
|
|
/*!
|
|
* \brief Creates a new WpConf object and opens the configuration file and its
|
|
* fragments, keeping them mapped in memory for further access.
|
|
*
|
|
* \ingroup wpconf
|
|
* \param name the name of the configuration file
|
|
* \param properties (transfer full) (nullable): a WpProperties with keys
|
|
* specifying how to load the WpConf object
|
|
* \param error (out) (nullable): return location for a GError, or NULL
|
|
* \returns (transfer full) (nullable): a new WpConf object, or NULL
|
|
* if an error occurred
|
|
*/
|
|
WpConf *
|
|
wp_conf_new_open (const gchar * name, WpProperties * properties, GError ** error)
|
|
{
|
|
g_return_val_if_fail (name, NULL);
|
|
|
|
g_autoptr (WpConf) self = wp_conf_new (name, properties);
|
|
if (!wp_conf_open (self, error))
|
|
return NULL;
|
|
return g_steal_pointer (&self);
|
|
}
|
|
|
|
static gboolean
|
|
detect_old_conf_format (WpConf * self, GMappedFile *file)
|
|
{
|
|
const gchar *data = g_mapped_file_get_contents (file);
|
|
gsize size = g_mapped_file_get_length (file);
|
|
|
|
/* wireplumber 0.4 used to have components of type = config/lua */
|
|
return g_strrstr_len (data, size, "config/lua") ? TRUE : FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
open_and_load_sections (WpConf * self, const gchar *path, GError ** error)
|
|
{
|
|
const gchar *as_section = NULL;
|
|
|
|
if (self->properties) {
|
|
/* as-section="some.name" means that the entire file will be stored as a
|
|
single JSON value that will be accessible through wp_conf_get_section()
|
|
using "some.name" - no parsing is done; the value is expected to be a
|
|
container */
|
|
as_section = wp_properties_get (self->properties, "as-section");
|
|
wp_debug_object (self, "Reading config file as single section: %s", as_section);
|
|
}
|
|
|
|
g_autoptr (GMappedFile) file = g_mapped_file_new (path, FALSE, error);
|
|
if (!file)
|
|
return FALSE;
|
|
|
|
if (!g_mapped_file_get_contents (file) || g_mapped_file_get_length (file) == 0) {
|
|
wp_notice_object (self, "Ignoring empty configuration file at '%s'", path);
|
|
return TRUE;
|
|
}
|
|
|
|
/* test if the file is a relic from 0.4 */
|
|
if (detect_old_conf_format (self, file)) {
|
|
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
|
"The configuration file at '%s' is likely an old WirePlumber 0.4 config "
|
|
"and is not supported anymore. Try removing it.", path);
|
|
return FALSE;
|
|
}
|
|
|
|
g_autoptr (GArray) sections = g_array_new (FALSE, FALSE, sizeof (WpConfSection));
|
|
g_array_set_clear_func (sections, (GDestroyNotify) wp_conf_section_clear);
|
|
|
|
g_autoptr (WpSpaJson) json = wp_spa_json_new_wrap_stringn (
|
|
g_mapped_file_get_contents (file), g_mapped_file_get_length (file));
|
|
|
|
if (as_section) {
|
|
WpConfSection section = { 0, };
|
|
|
|
if (!wp_spa_json_is_container (json)) {
|
|
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
|
"invalid single-section config file start (expected an object or array): %.*s",
|
|
(int) wp_spa_json_get_size (json), wp_spa_json_get_data (json));
|
|
return FALSE;
|
|
}
|
|
|
|
section.name = g_strdup (as_section);
|
|
section.value = g_steal_pointer (&json);
|
|
section.location = g_strdup (path);
|
|
g_array_append_val (sections, section);
|
|
}
|
|
else {
|
|
g_auto (WpConfSection) section = { 0, };
|
|
g_autoptr (WpSpaJson) tmp = NULL, toplevel = NULL;
|
|
g_autoptr (WpSpaJsonParser) parser = wp_spa_json_parser_new_undefined (json);
|
|
|
|
/* get the very first token */
|
|
tmp = wp_spa_json_parser_get_json (parser);
|
|
|
|
/* if the top-level token is an object, parse that instead */
|
|
if (tmp && wp_spa_json_is_object (tmp)) {
|
|
g_clear_pointer (&parser, wp_spa_json_parser_unref);
|
|
toplevel = g_steal_pointer (&tmp);
|
|
parser = wp_spa_json_parser_new_object (toplevel);
|
|
tmp = wp_spa_json_parser_get_json (parser);
|
|
}
|
|
|
|
while (TRUE) {
|
|
if (!tmp)
|
|
break;
|
|
|
|
/* if !is_string, but we want to support strings without quotes */
|
|
if (wp_spa_json_is_container (tmp) ||
|
|
wp_spa_json_is_int (tmp) ||
|
|
wp_spa_json_is_float (tmp) ||
|
|
wp_spa_json_is_boolean (tmp) ||
|
|
wp_spa_json_is_null (tmp))
|
|
{
|
|
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
|
"invalid section name (not a string): %.*s",
|
|
(int) wp_spa_json_get_size (tmp), wp_spa_json_get_data (tmp));
|
|
return FALSE;
|
|
}
|
|
|
|
section.name = wp_spa_json_parse_string (tmp);
|
|
g_clear_pointer (&tmp, wp_spa_json_unref);
|
|
|
|
/* parse the section contents */
|
|
tmp = wp_spa_json_parser_get_json (parser);
|
|
if (!tmp) {
|
|
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
|
|
"section '%s' has no value", section.name);
|
|
return FALSE;
|
|
}
|
|
|
|
section.value = g_steal_pointer (&tmp);
|
|
section.location = g_strdup (path);
|
|
g_array_append_val (sections, section);
|
|
memset (§ion, 0, sizeof (section));
|
|
|
|
/* parse the next section name */
|
|
tmp = wp_spa_json_parser_get_json (parser);
|
|
}
|
|
}
|
|
|
|
/* store the mapped file and the sections; note that the stored WpSpaJson
|
|
still point to the data in the GMappedFile, so this is why we keep the
|
|
GMappedFile alive */
|
|
g_ptr_array_add (self->files, g_steal_pointer (&file));
|
|
g_array_append_vals (self->conf_sections, sections->data, sections->len);
|
|
g_array_set_clear_func (sections, NULL);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/*!
|
|
* \brief Opens the configuration file and its fragments and keeps them
|
|
* mapped in memory for further access.
|
|
*
|
|
* \ingroup wpconf
|
|
* \param self the configuration
|
|
* \param error (out)(nullable): return location for a GError, or NULL
|
|
* \returns TRUE on success, FALSE on error
|
|
*/
|
|
gboolean
|
|
wp_conf_open (WpConf * self, GError ** error)
|
|
{
|
|
const gchar *no_frags = NULL;
|
|
|
|
g_return_val_if_fail (WP_IS_CONF (self), FALSE);
|
|
|
|
g_autofree gchar *path = NULL;
|
|
g_autoptr (WpIterator) iterator = NULL;
|
|
g_auto (GValue) value = G_VALUE_INIT;
|
|
|
|
if (self->properties) {
|
|
no_frags = wp_properties_get (self->properties, "no-fragments");
|
|
}
|
|
|
|
/*
|
|
* open the config file - if the path supplied is absolute,
|
|
* wp_base_dirs_find_file will ignore WP_BASE_DIRS_CONFIGURATION
|
|
*/
|
|
path = wp_base_dirs_find_file (WP_BASE_DIRS_CONFIGURATION, NULL, self->name);
|
|
if (path) {
|
|
wp_info_object (self, "opening config file: %s", path);
|
|
if (!open_and_load_sections (self, path, error))
|
|
return FALSE;
|
|
}
|
|
g_clear_pointer (&path, g_free);
|
|
|
|
/* open the .conf.d/ fragments */
|
|
if (!no_frags) {
|
|
path = g_strdup_printf ("%s.d", self->name);
|
|
iterator = wp_base_dirs_new_files_iterator (WP_BASE_DIRS_CONFIGURATION, path,
|
|
".conf");
|
|
|
|
for (; wp_iterator_next (iterator, &value); g_value_unset (&value)) {
|
|
const gchar *filename = g_value_get_string (&value);
|
|
|
|
wp_info_object (self, "opening fragment file: %s", filename);
|
|
|
|
g_autoptr (GError) e = NULL;
|
|
if (!open_and_load_sections (self, filename, &e)) {
|
|
wp_warning_object (self, "failed to open '%s': %s", filename, e->message);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (self->files->len == 0) {
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
|
|
"Could not locate configuration file '%s'", self->name);
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/*!
|
|
* \brief Closes the configuration file and its fragments
|
|
*
|
|
* \ingroup wpconf
|
|
* \param self the configuration
|
|
*/
|
|
void
|
|
wp_conf_close (WpConf * self)
|
|
{
|
|
g_return_if_fail (WP_IS_CONF (self));
|
|
|
|
g_array_set_size (self->conf_sections, 0);
|
|
g_ptr_array_set_size (self->files, 0);
|
|
}
|
|
|
|
/*!
|
|
* \brief Tests if the configuration files are open
|
|
*
|
|
* \ingroup wpconf
|
|
* \param self the configuration
|
|
* \returns TRUE if the configuration files are open, FALSE otherwise
|
|
*/
|
|
gboolean
|
|
wp_conf_is_open (WpConf * self)
|
|
{
|
|
g_return_val_if_fail (WP_IS_CONF (self), FALSE);
|
|
return self->files->len > 0;
|
|
}
|
|
|
|
/*!
|
|
* \brief Gets the name of the configuration file
|
|
*
|
|
* \ingroup wpconf
|
|
* \param self the configuration
|
|
* \returns the name of the configuration file
|
|
*/
|
|
const gchar *
|
|
wp_conf_get_name (WpConf * self)
|
|
{
|
|
g_return_val_if_fail (WP_IS_CONF (self), NULL);
|
|
return self->name;
|
|
}
|
|
|
|
static WpSpaJson *
|
|
ensure_merged_section (WpConf * self, const gchar *section)
|
|
{
|
|
g_autoptr (WpSpaJson) merged = NULL;
|
|
WpConfSection *merged_section = NULL;
|
|
|
|
/* check if the section is already merged */
|
|
for (guint i = 0; i < self->conf_sections->len; i++) {
|
|
WpConfSection *s = &g_array_index (self->conf_sections, WpConfSection, i);
|
|
if (g_str_equal (s->name, section)) {
|
|
if (!s->location) {
|
|
wp_debug_object (self, "section %s is already merged", section);
|
|
return wp_spa_json_ref (s->value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Iterate over the sections and merge them */
|
|
for (guint i = 0; i < self->conf_sections->len; i++) {
|
|
WpConfSection *s = &g_array_index (self->conf_sections, WpConfSection, i);
|
|
const gchar *s_name = s->name;
|
|
|
|
/* skip the "override." prefix and take a note */
|
|
gboolean override = g_str_has_prefix (s_name, OVERRIDE_SECTION_PREFIX);
|
|
if (override)
|
|
s_name += strlen (OVERRIDE_SECTION_PREFIX);
|
|
|
|
if (g_str_equal (s_name, section)) {
|
|
/* Merge sections if a previous value exists and
|
|
the 'override.' prefix is not present */
|
|
if (!override && merged) {
|
|
g_autoptr (WpSpaJson) new_merged =
|
|
wp_json_utils_merge_containers (merged, s->value);
|
|
if (!merged) {
|
|
wp_warning_object (self,
|
|
"skipping merge of '%s' from '%s' as JSON containers are not compatible",
|
|
section, s->location);
|
|
continue;
|
|
}
|
|
|
|
g_clear_pointer (&merged, wp_spa_json_unref);
|
|
merged = g_steal_pointer (&new_merged);
|
|
merged_section = NULL;
|
|
}
|
|
/* Otherwise always replace */
|
|
else {
|
|
g_clear_pointer (&merged, wp_spa_json_unref);
|
|
merged = wp_spa_json_ref (s->value);
|
|
merged_section = s;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* cache the result */
|
|
if (merged_section) {
|
|
/* if the merged json came from a single location, just clear
|
|
the location from that WpConfSection to mark it as the result */
|
|
wp_info_object (self, "section '%s' is used as-is from '%s'", section,
|
|
merged_section->location);
|
|
g_clear_pointer (&merged_section->location, g_free);
|
|
} else if (merged) {
|
|
/* if the merged json came from multiple locations, create a new
|
|
WpConfSection to store it */
|
|
WpConfSection s = { g_strdup (section), wp_spa_json_ref (merged), NULL };
|
|
g_array_append_val (self->conf_sections, s);
|
|
wp_info_object (self, "section '%s' is merged from multiple locations",
|
|
section);
|
|
} else {
|
|
wp_info_object (self, "section '%s' is not defined", section);
|
|
}
|
|
|
|
return g_steal_pointer (&merged);
|
|
}
|
|
|
|
/*!
|
|
* This method will get the JSON value of a specific section from the
|
|
* configuration. If the same section is defined in multiple locations, the
|
|
* sections with the same name will be either merged in case of arrays and
|
|
* objects, or overridden in case of boolean, int, double and strings.
|
|
*
|
|
* \ingroup wpconf
|
|
* \param self the configuration
|
|
* \param section the section name
|
|
* \returns (transfer full) (nullable): the JSON value of the section or NULL
|
|
* if the section does not exist
|
|
*/
|
|
WpSpaJson *
|
|
wp_conf_get_section (WpConf *self, const gchar *section)
|
|
{
|
|
g_return_val_if_fail (WP_IS_CONF (self), NULL);
|
|
g_return_val_if_fail (section, NULL);
|
|
|
|
return ensure_merged_section (self, section);
|
|
}
|
|
|
|
/*!
|
|
* \brief Updates the given properties with the values of a specific section
|
|
* from the configuration.
|
|
*
|
|
* \ingroup wpconf
|
|
* \param self the configuration
|
|
* \param section the section name
|
|
* \param props the properties to update
|
|
* \returns the number of properties updated
|
|
*/
|
|
gint
|
|
wp_conf_section_update_props (WpConf *self, const gchar *section,
|
|
WpProperties *props)
|
|
{
|
|
g_autoptr (WpSpaJson) json = NULL;
|
|
|
|
g_return_val_if_fail (WP_IS_CONF (self), -1);
|
|
g_return_val_if_fail (section, -1);
|
|
g_return_val_if_fail (props, -1);
|
|
|
|
json = wp_conf_get_section (self, section);
|
|
if (!json)
|
|
return 0;
|
|
return wp_properties_update_from_json (props, json);
|
|
}
|
|
|
|
#include "private/parse-conf-section.c"
|
|
|
|
/*!
|
|
* \brief Parses standard pw_context sections from \a conf
|
|
*
|
|
* \ingroup wpconf
|
|
* \param self the configuration
|
|
* \param context the associated pw_context
|
|
*/
|
|
void
|
|
wp_conf_parse_pw_context_sections (WpConf * self, struct pw_context * context)
|
|
{
|
|
gint res;
|
|
WpProperties *conf_wp;
|
|
struct pw_properties *conf_pw;
|
|
|
|
g_return_if_fail (WP_IS_CONF (self));
|
|
g_return_if_fail (context);
|
|
|
|
/* convert needed sections into a pipewire-style conf dictionary */
|
|
conf_wp = wp_properties_new ("config.path", "wpconf", NULL);
|
|
{
|
|
g_autoptr (WpSpaJson) j = wp_conf_get_section (self, "context.spa-libs");
|
|
if (j) {
|
|
g_autofree gchar *js = wp_spa_json_parse_string (j);
|
|
wp_properties_set (conf_wp, "context.spa-libs", js);
|
|
}
|
|
}
|
|
{
|
|
g_autoptr (WpSpaJson) j = wp_conf_get_section (self, "context.modules");
|
|
if (j) {
|
|
g_autofree gchar *js = wp_spa_json_parse_string (j);
|
|
wp_properties_set (conf_wp, "context.modules", js);
|
|
}
|
|
}
|
|
conf_pw = wp_properties_unref_and_take_pw_properties (conf_wp);
|
|
|
|
/* parse sections */
|
|
if ((res = _pw_context_parse_conf_section (context, conf_pw, "context.spa-libs")) < 0)
|
|
goto error;
|
|
wp_info_object (self, "parsed %d context.spa-libs items", res);
|
|
|
|
if ((res = _pw_context_parse_conf_section (context, conf_pw, "context.modules")) < 0)
|
|
goto error;
|
|
if (res > 0)
|
|
wp_info_object (self, "parsed %d context.modules items", res);
|
|
else
|
|
wp_warning_object (self, "no modules loaded from context.modules");
|
|
|
|
out:
|
|
pw_properties_free (conf_pw);
|
|
return;
|
|
|
|
error:
|
|
wp_critical_object (self, "failed to parse pw_context sections: %s",
|
|
spa_strerror (res));
|
|
goto out;
|
|
}
|