Commit b2df7de2 authored by Slava Monich's avatar Slava Monich

Merge branch 'auto-select-data' into 'master'

Add AutoSelectDataSim option.

See merge request !245
parents 93ff6448 b56930a8
......@@ -62,6 +62,7 @@ unit/test-sailfish_cell_info_dbus
unit/test-sailfish_manager
unit/test-sailfish_sim_info
unit/test-sailfish_sim_info_dbus
unit/test-config
unit/test-watch
unit/test-sms-filter
unit/test-voicecall-filter
......
......@@ -772,7 +772,7 @@ src_ofonod_SOURCES = $(builtin_sources) $(gatchat_sources) src/ofono.ver \
src/handsfree-audio.c src/bluetooth.h \
src/sim-mnclength.c src/voicecallagent.c \
src/sms-filter.c src/gprs-filter.c \
src/dbus-queue.c src/dbus-access.c \
src/dbus-queue.c src/dbus-access.c src/config.c \
src/voicecall-filter.c src/ril-transport.c \
src/hfp.h src/siri.c src/watchlist.c \
src/netmon.c src/lte.c src/ims.c \
......@@ -968,8 +968,7 @@ if SAILFISH_MANAGER
unit_test_sailfish_cell_info_SOURCES = unit/test-sailfish_cell_info.c \
plugins/sailfish_manager/sailfish_cell_info.c
unit_test_sailfish_cell_info_CFLAGS = $(AM_CFLAGS) $(COVERAGE_OPT) \
-Iplugins/sailfish_manager
unit_test_sailfish_cell_info_CFLAGS = $(AM_CFLAGS) $(COVERAGE_OPT)
unit_test_sailfish_cell_info_LDADD = @GLIB_LIBS@ -ldl
unit_objects += $(unit_test_sailfish_cell_info_OBJECTS)
unit_tests += unit/test-sailfish_cell_info
......@@ -982,7 +981,7 @@ unit_test_sailfish_cell_info_dbus_SOURCES = unit/test-dbus.c \
gdbus/object.c \
src/dbus.c src/log.c
unit_test_sailfish_cell_info_dbus_CFLAGS = $(AM_CFLAGS) $(COVERAGE_OPT) \
@DBUS_GLIB_CFLAGS@ -Iplugins/sailfish_manager
@DBUS_GLIB_CFLAGS@
unit_test_sailfish_cell_info_dbus_LDADD = @DBUS_GLIB_LIBS@ @GLIB_LIBS@ -ldl
unit_objects += $(unit_test_sailfish_cell_info_dbus_OBJECTS)
unit_tests += unit/test-sailfish_cell_info_dbus
......@@ -992,7 +991,7 @@ unit_test_sailfish_sim_info_SOURCES = unit/test-sailfish_sim_info.c \
plugins/sailfish_manager/sailfish_sim_info.c \
src/storage.c src/watchlist.c src/log.c
unit_test_sailfish_sim_info_CFLAGS = $(COVERAGE_OPT) $(AM_CFLAGS) \
-DSTORAGEDIR='"/tmp/ofono"' -Iplugins/sailfish_manager
-DSTORAGEDIR='"/tmp/ofono"'
unit_test_sailfish_sim_info_LDADD = @GLIB_LIBS@ -ldl
unit_objects += $(unit_test_sailfish_sim_info_OBJECTS)
unit_tests += unit/test-sailfish_sim_info
......@@ -1004,8 +1003,7 @@ unit_test_sailfish_sim_info_dbus_SOURCES = unit/test-sailfish_sim_info_dbus.c \
gdbus/object.c \
src/dbus.c src/storage.c src/watchlist.c src/log.c
unit_test_sailfish_sim_info_dbus_CFLAGS = $(COVERAGE_OPT) $(AM_CFLAGS) \
@DBUS_GLIB_CFLAGS@ -DSTORAGEDIR='"/tmp/ofono"' \
-Iplugins/sailfish_manager
@DBUS_GLIB_CFLAGS@ -DSTORAGEDIR='"/tmp/ofono"'
unit_test_sailfish_sim_info_dbus_LDADD = @DBUS_GLIB_LIBS@ @GLIB_LIBS@ -ldl
unit_objects += $(unit_test_sailfish_sim_info_dbus_OBJECTS)
unit_tests += unit/test-sailfish_sim_info_dbus
......@@ -1017,7 +1015,7 @@ unit_test_sailfish_manager_SOURCES = unit/test-sailfish_manager.c \
plugins/sailfish_manager/sailfish_sim_info.c \
src/storage.c src/log.c
unit_test_sailfish_manager_CFLAGS = $(AM_CFLAGS) $(COVERAGE_OPT) \
-DSTORAGEDIR='"/tmp/ofono"' -Iplugins/sailfish_manager
-DSTORAGEDIR='"/tmp/ofono"'
unit_test_sailfish_manager_LDADD = @GLIB_LIBS@ -ldl
unit_objects += $(unit_test_sailfish_manager_OBJECTS)
unit_tests += unit/test-sailfish_manager
......@@ -1025,13 +1023,20 @@ unit_tests += unit/test-sailfish_manager
unit_test_watch_SOURCES = unit/test-watch.c src/watch.c \
src/log.c src/watchlist.c
unit_test_watch_CFLAGS = $(AM_CFLAGS) $(COVERAGE_OPT) \
-DSTORAGEDIR='"/tmp/ofono"' -Iplugins/sailfish_manager
-DSTORAGEDIR='"/tmp/ofono"'
unit_test_watch_LDADD = @GLIB_LIBS@ -ldl
unit_objects += $(unit_test_watch_OBJECTS)
unit_tests += unit/test-watch
endif
unit_test_config_SOURCES = unit/test-config.c drivers/ril/ril_util.c \
src/config.c src/log.c
unit_test_config_CFLAGS = $(COVERAGE_OPT) $(AM_CFLAGS)
unit_test_config_LDADD = @GLIB_LIBS@ -ldl
unit_objects += $(unit_test_config_OBJECTS)
unit_tests += unit/test-config
if SAILFISH_ACCESS
unit_test_sailfish_access_SOURCES = unit/test-sailfish_access.c \
plugins/sailfish_access.c src/dbus-access.c src/log.c
......
/*
* oFono - Open Source Telephony - RIL-based devices
*
* Copyright (C) 2015-2018 Jolla Ltd.
* Copyright (C) 2015-2019 Jolla Ltd.
* Copyright (C) 2019 Open Mobile Platform LLC.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
......@@ -237,337 +238,6 @@ char *ril_config_ints_to_string(GUtilInts *ints, char separator)
return NULL;
}
/**
* The ril_config_merge_files() function does the following:
*
* 1. Loads the specified key file (say, "/etc/foo.conf")
* 2. Scans the subdirectory named after the file (e.g. "/etc/foo.d/")
* for the files with the same suffix as the main file (e.g. "*.conf")
* 3. Sorts the files from the subdirectory (alphabetically)
* 4. Merges the contents of the additional files with the main file
* according to their sort order.
*
* When the entries are merged, keys and groups overwrite the exising
* ones by default. Keys can be suffixed with special characters to
* remove or modify the existing entries instead:
*
* ':' Sets the (default) value if the key is missing
* '+' Appends values to the string list
* '?' Appends only new (non-existent) values to the string list
* '-' Removes the values from the string list
*
* Both keys and groups can be prefixed with '!' to remove the entire key
* or group.
*
* For example if we merge these two files:
*
* /etc/foo.conf:
*
* [foo]
* a=1
* b=2,3
* c=4
* d=5
* [bar]
* e=5
*
* /etc/foo.d/bar.conf:
*
* [foo]
* a+=2
* b-=2
* c=5
* !d
* [!bar]
*
* we end up with this:
*
* [foo]
* a=1
* b=2,3
* c=5
*
* Not that the list separator is assumed to be ',' (rather than default ';').
* The keyfile passed to ril_config_merge_files() should use the same list
* separator, because the default values are copied from the config files
* as is.
*/
static gint ril_config_sort_files(gconstpointer a, gconstpointer b)
{
/* The comparison function for g_ptr_array_sort() doesn't take
* the pointers from the array as arguments, it takes pointers
* to the pointers in the array. */
return strcmp(*(char**)a, *(char**)b);
}
static char **ril_config_collect_files(const char *path, const char *suffix)
{
/* Returns sorted list of regular files in the directory,
* optionally having the specified suffix (e.g. ".conf").
* Returns NULL if nothing appropriate has been found. */
char **files = NULL;
DIR *d = opendir(path);
if (d) {
GPtrArray *list = g_ptr_array_new();
const struct dirent *p;
while ((p = readdir(d)) != NULL) {
/* No need to even stat . and .. */
if (strcmp(p->d_name, ".") &&
strcmp(p->d_name, "..") && (!suffix ||
g_str_has_suffix(p->d_name, suffix))) {
struct stat st;
char *buf = g_strconcat(path, "/", p->d_name,
NULL);
if (!stat(buf, &st) && S_ISREG(st.st_mode)) {
g_ptr_array_add(list, buf);
} else {
g_free(buf);
}
}
}
if (list->len > 0) {
g_ptr_array_sort(list, ril_config_sort_files);
g_ptr_array_add(list, NULL);
files = (char**)g_ptr_array_free(list, FALSE);
} else {
g_ptr_array_free(list, TRUE);
}
closedir(d);
}
return files;
}
static int ril_config_list_find(char **list, gsize len, const char *value)
{
guint i;
for (i = 0; i < len; i++) {
if (!strcmp(list[i], value)) {
return i;
}
}
return -1;
}
static void ril_config_list_append(GKeyFile *conf, GKeyFile *k,
const char *group, const char *key,
char **values, gsize n, gboolean unique)
{
/* Note: will steal strings from values */
if (n > 0) {
int i;
gsize len = 0;
gchar **list = g_key_file_get_string_list(conf, group, key,
&len, NULL);
GPtrArray *newlist = g_ptr_array_new_full(0, g_free);
for (i = 0; i < (int)len; i++) {
g_ptr_array_add(newlist, list[i]);
}
for (i = 0; i < (int)n; i++) {
char *val = values[i];
if (!unique || ril_config_list_find((char**)
newlist->pdata, newlist->len, val) < 0) {
/* Move the string to the new list */
g_ptr_array_add(newlist, val);
memmove(values + i, values + i + 1,
sizeof(char*) * (n - i));
i--;
n--;
}
}
if (newlist->len > len) {
g_key_file_set_string_list(conf, group, key,
(const gchar * const *) newlist->pdata,
newlist->len);
}
/* Strings are deallocated by GPtrArray */
g_ptr_array_free(newlist, TRUE);
g_free(list);
}
}
static void ril_config_list_remove(GKeyFile *conf, GKeyFile *k,
const char *group, const char *key, char **values, gsize n)
{
if (n > 0) {
gsize len = 0;
gchar **list = g_key_file_get_string_list(conf, group, key,
&len, NULL);
if (len > 0) {
gsize i;
const gsize oldlen = len;
for (i = 0; i < n; i++) {
int pos;
/* Remove all matching values */
while ((pos = ril_config_list_find(list, len,
values[i])) >= 0) {
g_free(list[pos]);
memmove(list + pos, list + pos + 1,
sizeof(char*) * (len - pos));
len--;
}
}
if (len < oldlen) {
g_key_file_set_string_list(conf, group, key,
(const gchar * const *) list, len);
}
}
g_strfreev(list);
}
}
static void ril_config_merge_group(GKeyFile *conf, GKeyFile *k,
const char *group)
{
gsize i, n = 0;
char **keys = g_key_file_get_keys(k, group, &n, NULL);
for (i=0; i<n; i++) {
char *key = keys[i];
if (key[0] == '!') {
if (key[1]) {
g_key_file_remove_key(conf, group, key+1, NULL);
}
} else {
const gsize len = strlen(key);
const char last = (len > 0) ? key[len-1] : 0;
if (last == '+' || last == '?') {
gsize count = 0;
gchar **values = g_key_file_get_string_list(k,
group, key, &count, NULL);
key[len-1] = 0;
ril_config_list_append(conf, k, group, key,
values, count, last == '?');
g_strfreev(values);
} else if (last == '-') {
gsize count = 0;
gchar **values = g_key_file_get_string_list(k,
group, key, &count, NULL);
key[len-1] = 0;
ril_config_list_remove(conf, k, group, key,
values, count);
g_strfreev(values);
} else {
/* Overwrite the value (it must exist in k) */
gchar *value = g_key_file_get_value(k, group,
key, NULL);
if (last == ':') {
/* Default value */
key[len-1] = 0;
if (!g_key_file_has_key(conf,
group, key, NULL)) {
g_key_file_set_value(conf,
group, key, value);
}
} else {
g_key_file_set_value(conf, group, key,
value);
}
g_free(value);
}
}
}
g_strfreev(keys);
}
static void ril_config_merge_keyfile(GKeyFile *conf, GKeyFile *k)
{
gsize i, n = 0;
char **groups = g_key_file_get_groups(k, &n);
for (i=0; i<n; i++) {
const char *group = groups[i];
if (group[0] == '!') {
g_key_file_remove_group(conf, group + 1, NULL);
} else {
ril_config_merge_group(conf, k, group);
}
}
g_strfreev(groups);
}
static void ril_config_merge_file(GKeyFile *conf, const char *file)
{
GKeyFile *k = g_key_file_new();
g_key_file_set_list_separator(k, ',');
if (g_key_file_load_from_file(k, file, 0, NULL)) {
ril_config_merge_keyfile(conf, k);
}
g_key_file_unref(k);
}
void ril_config_merge_files(GKeyFile *conf, const char *file)
{
if (conf && file && file[0]) {
char *dot = strrchr(file, '.');
const char *suffix;
char *dir;
char **files;
if (!dot) {
dir = g_strconcat(file, ".d", NULL);
suffix = NULL;
} else if (!dot[1]) {
dir = g_strconcat(file, "d", NULL);
suffix = NULL;
} else {
/* 2 bytes for ".d" and 1 for NULL terminator */
dir = g_malloc(dot - file + 3);
strncpy(dir, file, dot - file);
strcpy(dir + (dot - file), ".d");
suffix = dot + 1;
}
files = ril_config_collect_files(dir, suffix);
g_free(dir);
/* Load the main config */
if (g_file_test(file, G_FILE_TEST_EXISTS)) {
DBG("Loading %s", file);
ril_config_merge_file(conf, file);
}
if (files) {
char **ptr;
for (ptr = files; *ptr; ptr++) {
DBG("Merging %s", *ptr);
ril_config_merge_file(conf, *ptr);
}
g_strfreev(files);
}
}
}
/*
* Local Variables:
* mode: C
......
/*
* oFono - Open Source Telephony - RIL-based devices
*
* Copyright (C) 2015-2018 Jolla Ltd.
* Copyright (C) 2015-2019 Jolla Ltd.
* Copyright (C) 2019 Open Mobile Platform LLC.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
......@@ -22,8 +23,6 @@
#define RILCONF_SETTINGS_GROUP "Settings"
void ril_config_merge_files(GKeyFile *conf, const char *file);
char *ril_config_get_string(GKeyFile *file, const char *group,
const char *key);
char **ril_config_get_strings(GKeyFile *file, const char *group,
......
......@@ -28,6 +28,7 @@
#include "ril_devmon.h"
#include "ril_log.h"
#include "ofono.h"
#include "sailfish_manager.h"
#include <ofono/storage.h>
......@@ -1884,7 +1885,7 @@ static GSList *ril_plugin_load_config(const char *path,
GKeyFile *file = g_key_file_new();
gboolean empty = FALSE;
ril_config_merge_files(file, path);
config_merge_files(file, path);
if (ril_config_get_boolean(file, RILCONF_SETTINGS_GROUP,
RILCONF_SETTINGS_EMPTY, &empty) && empty) {
DBG("Empty config");
......
/*
* oFono - Open Source Telephony
*
* Copyright (C) 2017-2019 Jolla Ltd.
* Copyright (C) 2017-2020 Jolla Ltd.
* Copyright (C) 2019-2020 Open Mobile Platform LLC.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
......@@ -22,6 +23,7 @@
#include <gutil_macros.h>
#include <string.h>
#include <ofono/storage.h>
#include <ofono/watch.h>
#include "src/ofono.h"
......@@ -44,6 +46,12 @@ enum ofono_watch_events {
WATCH_EVENT_COUNT
};
enum sim_auto_select {
SIM_AUTO_SELECT_OFF,
SIM_AUTO_SELECT_ON,
SIM_AUTO_SELECT_ONCE
};
struct sailfish_manager_priv {
struct sailfish_manager pub; /* Public part */
struct sailfish_slot_driver_reg *drivers;
......@@ -52,6 +60,8 @@ struct sailfish_manager_priv {
struct sailfish_slot_priv *data_slot;
struct sailfish_slot_priv *mms_slot;
sailfish_slot_ptr *slots;
enum sim_auto_select auto_data_sim;
gboolean auto_data_sim_done;
int slot_count;
guint init_countdown;
guint init_id;
......@@ -99,6 +109,11 @@ struct sailfish_slot_priv {
int index;
};
/* Read-only config */
#define SF_CONFIG_FILE "main.conf"
#define SF_CONFIG_GROUP "ModemManager"
#define SF_CONFIG_KEY_AUTO_DATA_SIM "AutoSelectDataSim"
/* "ril" is used for historical reasons */
#define SF_STORE "ril"
#define SF_STORE_GROUP "Settings"
......@@ -106,6 +121,7 @@ struct sailfish_slot_priv {
#define SF_STORE_DEFAULT_VOICE_SIM "DefaultVoiceSim"
#define SF_STORE_DEFAULT_DATA_SIM "DefaultDataSim"
#define SF_STORE_SLOTS_SEP ","
#define SF_STORE_AUTO_DATA_SIM_DONE "AutoSelectDataSimDone"
/* The file where error statistics is stored. Again "rilerror" is historical */
#define SF_ERROR_STORAGE "rilerror" /* File name */
......@@ -145,6 +161,50 @@ static inline void sailfish_slot_set_data_role(struct sailfish_slot_priv *s,
}
}
static gboolean sailfish_config_get_enum(GKeyFile *file, const char *group,
const char *key, int *result,
const char *name, int value, ...)
{
char *str = g_key_file_get_string(file, group, key, NULL);
if (str) {
/*
* Some people are thinking that # is a comment
* anywhere on the line, not just at the beginning
*/
char *comment = strchr(str, '#');
if (comment) *comment = 0;
g_strstrip(str);
if (strcasecmp(str, name)) {
va_list args;
va_start(args, value);
while ((name = va_arg(args, char*)) != NULL) {
value = va_arg(args, int);
if (!strcasecmp(str, name)) {
break;
}
}
va_end(args);
}
if (!name) {
ofono_error("Invalid %s config value (%s)", key, str);
}
g_free(str);
if (name) {
if (result) {
*result = value;
}
return TRUE;
}
}
return FALSE;
}
/* Update modem paths and emit D-Bus signal if necessary */
static void sailfish_manager_update_modem_paths_full
(struct sailfish_manager_priv *p)
......@@ -588,6 +648,27 @@ static struct sailfish_slot_priv *sailfish_manager_find_slot_imsi
}
}
static gboolean sailfish_manager_all_sims_are_initialized_proc
(struct sailfish_slot_priv *s, void *user_data)
{
if (s->pub.sim_present && s->pub.enabled && !s->watch->imsi) {
*((gboolean*)user_data) = FALSE;
return SF_LOOP_DONE;
} else {
return SF_LOOP_CONTINUE;
}
}
static gboolean sailfish_manager_all_sims_are_initialized
(struct sailfish_manager_priv *p)
{
gboolean result = TRUE;
sailfish_manager_foreach_slot(p,
sailfish_manager_all_sims_are_initialized_proc, &result);
return result;
}
/* Returns the event mask to be passed to sailfish_manager_dbus_signal.
* The caller has a chance to OR it with other bits */
static int sailfish_manager_update_modem_paths(struct sailfish_manager_priv *p)
......@@ -615,7 +696,7 @@ static int sailfish_manager_update_modem_paths(struct sailfish_manager_priv *p)
* previously selected voice SIM is inserted, we will switch
* back to it.
*
* There is no such fallback for the data.
* A similar behavior can be configured for data SIM too.
*/
if (!slot) {
slot = sailfish_manager_find_slot_imsi(p, NULL);
......@@ -648,11 +729,36 @@ static int sailfish_manager_update_modem_paths(struct sailfish_manager_priv *p)
slot = sailfish_manager_find_slot_imsi(p, NULL);
}
} else {
slot = NULL;
}
/* Check if we need to auto-select data SIM (always or once) */
if (!slot && (p->auto_data_sim == SIM_AUTO_SELECT_ON ||
(p->auto_data_sim == SIM_AUTO_SELECT_ONCE &&
!p->auto_data_sim_done))) {
/*
* Should we automatically select the default data sim
* on a multisim phone that has only one sim inserted?
* To actually make a selection we need all present SIMs
* to be initialized. Otherwise we may end up endlessly
* switching data SIMs back and forth.
*/
slot = NULL;
if (sailfish_manager_all_sims_are_initialized(p)) {
slot = sailfish_manager_find_slot_imsi(p, NULL);
if (slot && slot->watch->online &&
p->auto_data_sim == SIM_AUTO_SELECT_ONCE) {
/*
* Data SIM only needs to be auto-selected
* once and it's done. Write that down.
*/
p->auto_data_sim_done = TRUE;
g_key_file_set_boolean(p->storage,
SF_STORE_GROUP,
SF_STORE_AUTO_DATA_SIM_DONE,
p->auto_data_sim_done);
storage_sync(NULL, SF_STORE, p->storage);
}
} else {
DBG("Skipping auto-selection of data SIM");
}
}
if (slot && !slot->watch->online) {
......@@ -1274,6 +1380,29 @@ static struct sailfish_manager_priv *sailfish_manager_priv_new()
struct sailfish_manager_priv *p =
g_slice_new0(struct sailfish_manager_priv);
GKeyFile *conf = g_key_file_new();
char* fn = g_build_filename(ofono_config_dir(), SF_CONFIG_FILE, NULL);
/* Load config */
if (g_key_file_load_from_file(conf, fn, 0, NULL)) {
int ival;
DBG("Loading configuration file %s", fn);
if (sailfish_config_get_enum(conf, SF_CONFIG_GROUP,
SF_CONFIG_KEY_AUTO_DATA_SIM, &ival,
"off", SIM_AUTO_SELECT_OFF,
"once", SIM_AUTO_SELECT_ONCE,
"always", SIM_AUTO_SELECT_ON,
"on", SIM_AUTO_SELECT_ON, NULL)) {
DBG("Automatic data SIM selection: %s",
ival == SIM_AUTO_SELECT_ONCE ? "once":
ival == SIM_AUTO_SELECT_ON ? "on":
"off");
p->auto_data_sim = ival;
}
}
g_key_file_free(conf);
g_free(fn);
/* Load settings */
p->storage = storage_open(NULL, SF_STORE);
......@@ -1283,6 +1412,8 @@ static struct sailfish_manager_priv *sailfish_manager_priv_new()
p->pub.default_data_imsi = p->default_data_imsi =
g_key_file_get_string(p->storage, SF_STORE_GROUP,
SF_STORE_DEFAULT_DATA_SIM, NULL);
p->auto_data_sim_done = g_key_file_get_boolean(p->storage,
SF_STORE_GROUP, SF_STORE_AUTO_DATA_SIM_DONE, NULL);
DBG("Default voice sim is %s", p->default_voice_imsi ?
p->default_voice_imsi : "(auto)");
......
/*
* oFono - Open Source Telephony
*
* Copyright (C) 2015-2019 Jolla Ltd.
* Copyright (C) 2019 Open Mobile Platform LLC.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program 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 General Public License for more details.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include "ofono.h"
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
/**
* The config_merge_files() function does the following:
*
* 1. Loads the specified key file (say, "/etc/foo.conf")
* 2. Scans the subdirectory named after the file (e.g. "/etc/foo.d/")
* for the files with the same suffix as the main file (e.g. "*.conf")
* 3. Sorts the files from the subdirectory (alphabetically)
* 4. Merges the contents of the additional files with the main file
* according to their sort order.
*
* When the entries are merged, keys and groups overwrite the exising
* ones by default. Keys can be suffixed with special characters to
* remove or modify the existing entries instead:
*
* ':' Sets the (default) value if the key is missing
* '+' Appends values to the string list
* '?' Appends only new (non-existent) values to the string list
* '-' Removes the values from the string list
*
* Both keys and groups can be prefixed with '!' to remove the entire key
* or group.