mirror of
https://github.com/alsa-project/alsa-lib.git
synced 2025-10-29 05:40:25 -04:00
ucm: add If condition block
The syntax is simple:
If./any-if-identificator/ {
Condition {
Type /type_here/
/optional defines/
}
True {
/block used when condition is evaluated as true/
}
False {
/block used when condition is evaluated as false/
}
}
The Type "ControlExists" is implemented:
Condition {
Type ControlExists
Device "hw:${CardId}"
Control "iface=CARD,name='Headphone Jack'"
}
Signed-off-by: Jaroslav Kysela <perex@perex.cz>
This commit is contained in:
parent
b9b2247943
commit
8a36e38dc4
8 changed files with 536 additions and 162 deletions
|
|
@ -115,6 +115,7 @@ int snd_config_evaluate(snd_config_t *config, snd_config_t *root,
|
||||||
snd_config_t *private_data, snd_config_t **result);
|
snd_config_t *private_data, snd_config_t **result);
|
||||||
|
|
||||||
int snd_config_add(snd_config_t *config, snd_config_t *leaf);
|
int snd_config_add(snd_config_t *config, snd_config_t *leaf);
|
||||||
|
int snd_config_remove(snd_config_t *config);
|
||||||
int snd_config_delete(snd_config_t *config);
|
int snd_config_delete(snd_config_t *config);
|
||||||
int snd_config_delete_compound_members(const snd_config_t *config);
|
int snd_config_delete_compound_members(const snd_config_t *config);
|
||||||
int snd_config_copy(snd_config_t **dst, snd_config_t *src);
|
int snd_config_copy(snd_config_t **dst, snd_config_t *src);
|
||||||
|
|
@ -179,6 +180,25 @@ snd_config_t *snd_config_iterator_entry(const snd_config_iterator_t iterator);
|
||||||
#define snd_config_for_each(pos, next, node) \
|
#define snd_config_for_each(pos, next, node) \
|
||||||
for (pos = snd_config_iterator_first(node), next = snd_config_iterator_next(pos); pos != snd_config_iterator_end(node); pos = next, next = snd_config_iterator_next(pos))
|
for (pos = snd_config_iterator_first(node), next = snd_config_iterator_next(pos); pos != snd_config_iterator_end(node); pos = next, next = snd_config_iterator_next(pos))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Helper macro to iterate over the children of a compound node.
|
||||||
|
* \param[in,out] pos Iterator variable for the current node.
|
||||||
|
* \param[in] node Handle to the compound configuration node to iterate over.
|
||||||
|
*
|
||||||
|
* Use this macro like a \c for statement, e.g.:
|
||||||
|
* \code
|
||||||
|
* snd_config_iterator_t pos;
|
||||||
|
* snd_config_for_each(pos, node) {
|
||||||
|
* snd_config_t *entry = snd_config_iterator_entry(pos);
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
* \endcode
|
||||||
|
*
|
||||||
|
* This macro does not allow deleting or removing the current node.
|
||||||
|
*/
|
||||||
|
#define snd_config_for_each_unsafe(pos, node) \
|
||||||
|
for (pos = snd_config_iterator_first(node); pos != snd_config_iterator_end(node); pos = snd_config_iterator_next(pos))
|
||||||
|
|
||||||
/* Misc functions */
|
/* Misc functions */
|
||||||
|
|
||||||
int snd_config_get_bool_ascii(const char *ascii);
|
int snd_config_get_bool_ascii(const char *ascii);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
EXTRA_LTLIBRARIES = libucm.la
|
EXTRA_LTLIBRARIES = libucm.la
|
||||||
|
|
||||||
libucm_la_SOURCES = utils.c parser.c main.c
|
libucm_la_SOURCES = utils.c parser.c ucm_cond.c ucm_subs.c main.c
|
||||||
|
|
||||||
noinst_HEADERS = ucm_local.h
|
noinst_HEADERS = ucm_local.h
|
||||||
|
|
||||||
|
|
|
||||||
147
src/ucm/main.c
147
src/ucm/main.c
|
|
@ -1352,151 +1352,6 @@ int snd_use_case_get_list(snd_use_case_mgr_t *uc_mgr,
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
static char *rval_conf_name(snd_use_case_mgr_t *uc_mgr)
|
|
||||||
{
|
|
||||||
if (uc_mgr->conf_file_name[0])
|
|
||||||
return strdup(uc_mgr->conf_file_name);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static char *rval_card_id(snd_use_case_mgr_t *uc_mgr)
|
|
||||||
{
|
|
||||||
struct ctl_list *ctl_list;
|
|
||||||
|
|
||||||
ctl_list = uc_mgr_get_one_ctl(uc_mgr);
|
|
||||||
if (ctl_list == NULL)
|
|
||||||
return NULL;
|
|
||||||
return strdup(snd_ctl_card_info_get_id(ctl_list->ctl_info));
|
|
||||||
}
|
|
||||||
|
|
||||||
static char *rval_card_name(snd_use_case_mgr_t *uc_mgr)
|
|
||||||
{
|
|
||||||
struct ctl_list *ctl_list;
|
|
||||||
|
|
||||||
ctl_list = uc_mgr_get_one_ctl(uc_mgr);
|
|
||||||
if (ctl_list == NULL)
|
|
||||||
return NULL;
|
|
||||||
return strdup(snd_ctl_card_info_get_name(ctl_list->ctl_info));
|
|
||||||
}
|
|
||||||
|
|
||||||
static char *rval_card_longname(snd_use_case_mgr_t *uc_mgr)
|
|
||||||
{
|
|
||||||
struct ctl_list *ctl_list;
|
|
||||||
|
|
||||||
ctl_list = uc_mgr_get_one_ctl(uc_mgr);
|
|
||||||
if (ctl_list == NULL)
|
|
||||||
return NULL;
|
|
||||||
return strdup(snd_ctl_card_info_get_longname(ctl_list->ctl_info));
|
|
||||||
}
|
|
||||||
|
|
||||||
static char *rval_env(snd_use_case_mgr_t *uc_mgr ATTRIBUTE_UNUSED, const char *id)
|
|
||||||
{
|
|
||||||
char *e;
|
|
||||||
|
|
||||||
e = getenv(id);
|
|
||||||
if (e)
|
|
||||||
return strdup(e);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
#define MATCH_VARIABLE(name, id, fcn) \
|
|
||||||
if (strncmp((name), (id), sizeof(id) - 1) == 0) { \
|
|
||||||
rval = fcn(uc_mgr); \
|
|
||||||
idsize = sizeof(id) - 1; \
|
|
||||||
goto __rval; \
|
|
||||||
}
|
|
||||||
|
|
||||||
#define MATCH_VARIABLE2(name, id, fcn) \
|
|
||||||
if (strncmp((name), (id), sizeof(id) - 1) == 0) { \
|
|
||||||
idsize = sizeof(id) - 1; \
|
|
||||||
tmp = strchr(value + idsize, '}'); \
|
|
||||||
if (tmp) { \
|
|
||||||
rvalsize = tmp - (value + idsize); \
|
|
||||||
if (rvalsize > sizeof(v2)) { \
|
|
||||||
err = -ENOMEM; \
|
|
||||||
goto __error; \
|
|
||||||
} \
|
|
||||||
strncpy(v2, value + idsize, rvalsize); \
|
|
||||||
v2[rvalsize] = '\0'; \
|
|
||||||
idsize += rvalsize + 1; \
|
|
||||||
rval = fcn(uc_mgr, v2); \
|
|
||||||
goto __rval; \
|
|
||||||
} \
|
|
||||||
}
|
|
||||||
|
|
||||||
static int get_substituted_value(snd_use_case_mgr_t *uc_mgr,
|
|
||||||
char **_rvalue,
|
|
||||||
const char *value)
|
|
||||||
{
|
|
||||||
size_t size, nsize, idsize, rvalsize, dpos = 0;
|
|
||||||
const char *tmp;
|
|
||||||
char *r, *nr, *rval, v2[32];
|
|
||||||
int err;
|
|
||||||
|
|
||||||
if (value == NULL)
|
|
||||||
return -ENOENT;
|
|
||||||
|
|
||||||
size = strlen(value) + 1;
|
|
||||||
r = malloc(size);
|
|
||||||
if (r == NULL)
|
|
||||||
return -ENOMEM;
|
|
||||||
|
|
||||||
while (*value) {
|
|
||||||
if (*value == '$' && *(value+1) == '{') {
|
|
||||||
MATCH_VARIABLE(value, "${ConfName}", rval_conf_name);
|
|
||||||
MATCH_VARIABLE(value, "${CardId}", rval_card_id);
|
|
||||||
MATCH_VARIABLE(value, "${CardName}", rval_card_name);
|
|
||||||
MATCH_VARIABLE(value, "${CardLongName}", rval_card_longname);
|
|
||||||
MATCH_VARIABLE2(value, "${env:", rval_env);
|
|
||||||
err = -EINVAL;
|
|
||||||
tmp = strchr(value, '}');
|
|
||||||
if (tmp) {
|
|
||||||
strncpy(r, value, tmp + 1 - value);
|
|
||||||
r[tmp + 1 - value] = '\0';
|
|
||||||
uc_error("variable '%s' is not known!", r);
|
|
||||||
} else {
|
|
||||||
uc_error("variable reference '%s' is not complete", value);
|
|
||||||
}
|
|
||||||
goto __error;
|
|
||||||
__rval:
|
|
||||||
if (rval == NULL || rval[0] == '\0') {
|
|
||||||
free(rval);
|
|
||||||
strncpy(r, value, idsize);
|
|
||||||
r[idsize] = '\0';
|
|
||||||
uc_error("variable '%s' is not defined in this context!", r);
|
|
||||||
err = -EINVAL;
|
|
||||||
goto __error;
|
|
||||||
}
|
|
||||||
value += idsize;
|
|
||||||
rvalsize = strlen(rval);
|
|
||||||
nsize = size + rvalsize - idsize;
|
|
||||||
if (nsize > size) {
|
|
||||||
nr = realloc(r, nsize);
|
|
||||||
if (nr == NULL) {
|
|
||||||
err = -ENOMEM;
|
|
||||||
goto __error;
|
|
||||||
}
|
|
||||||
size = nsize;
|
|
||||||
r = nr;
|
|
||||||
}
|
|
||||||
strcpy(r + dpos, rval);
|
|
||||||
dpos += rvalsize;
|
|
||||||
free(rval);
|
|
||||||
} else {
|
|
||||||
r[dpos++] = *value;
|
|
||||||
value++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r[dpos] = '\0';
|
|
||||||
|
|
||||||
*_rvalue = r;
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
__error:
|
|
||||||
free(r);
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int get_value1(snd_use_case_mgr_t *uc_mgr, char **value,
|
static int get_value1(snd_use_case_mgr_t *uc_mgr, char **value,
|
||||||
struct list_head *value_list, const char *identifier)
|
struct list_head *value_list, const char *identifier)
|
||||||
{
|
{
|
||||||
|
|
@ -1515,7 +1370,7 @@ static int get_value1(snd_use_case_mgr_t *uc_mgr, char **value,
|
||||||
return -ENOMEM;
|
return -ENOMEM;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return get_substituted_value(uc_mgr, value, val->data);
|
return uc_mgr_get_substituted_value(uc_mgr, value, val->data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -ENOENT;
|
return -ENOENT;
|
||||||
|
|
|
||||||
|
|
@ -565,7 +565,7 @@ static int parse_value(snd_use_case_mgr_t *uc_mgr ATTRIBUTE_UNUSED,
|
||||||
snd_config_t *cfg)
|
snd_config_t *cfg)
|
||||||
{
|
{
|
||||||
struct ucm_value *curr;
|
struct ucm_value *curr;
|
||||||
snd_config_iterator_t i, next;
|
snd_config_iterator_t i;
|
||||||
snd_config_t *n;
|
snd_config_t *n;
|
||||||
char buf[64];
|
char buf[64];
|
||||||
long l;
|
long l;
|
||||||
|
|
@ -578,13 +578,21 @@ static int parse_value(snd_use_case_mgr_t *uc_mgr ATTRIBUTE_UNUSED,
|
||||||
uc_error("error: compound is expected for value definition");
|
uc_error("error: compound is expected for value definition");
|
||||||
return -EINVAL;
|
return -EINVAL;
|
||||||
}
|
}
|
||||||
snd_config_for_each(i, next, cfg) {
|
snd_config_for_each_unsafe(i, cfg) {
|
||||||
const char *id;
|
const char *id;
|
||||||
n = snd_config_iterator_entry(i);
|
n = snd_config_iterator_entry(i);
|
||||||
err = snd_config_get_id(n, &id);
|
err = snd_config_get_id(n, &id);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
/* in-place condition evaluation */
|
||||||
|
if (strcmp(id, "If") == 0) {
|
||||||
|
err = uc_mgr_evaluate_condition(uc_mgr, cfg, n);
|
||||||
|
if (err < 0)
|
||||||
|
return err;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
/* alloc new value */
|
/* alloc new value */
|
||||||
curr = calloc(1, sizeof(struct ucm_value));
|
curr = calloc(1, sizeof(struct ucm_value));
|
||||||
if (curr == NULL)
|
if (curr == NULL)
|
||||||
|
|
@ -620,7 +628,7 @@ __buf:
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
uc_error("error: invalid type %i in Value compound", type);
|
uc_error("error: invalid type %i in Value compound '%s'", type, id);
|
||||||
return -EINVAL;
|
return -EINVAL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -679,7 +687,7 @@ static int parse_modifier(snd_use_case_mgr_t *uc_mgr,
|
||||||
struct use_case_verb *verb = data1;
|
struct use_case_verb *verb = data1;
|
||||||
struct use_case_modifier *modifier;
|
struct use_case_modifier *modifier;
|
||||||
const char *name;
|
const char *name;
|
||||||
snd_config_iterator_t i, next;
|
snd_config_iterator_t i;
|
||||||
snd_config_t *n;
|
snd_config_t *n;
|
||||||
int err;
|
int err;
|
||||||
|
|
||||||
|
|
@ -705,12 +713,20 @@ static int parse_modifier(snd_use_case_mgr_t *uc_mgr,
|
||||||
list_add_tail(&modifier->list, &verb->modifier_list);
|
list_add_tail(&modifier->list, &verb->modifier_list);
|
||||||
modifier->name = strdup(name);
|
modifier->name = strdup(name);
|
||||||
|
|
||||||
snd_config_for_each(i, next, cfg) {
|
snd_config_for_each_unsafe(i, cfg) {
|
||||||
const char *id;
|
const char *id;
|
||||||
n = snd_config_iterator_entry(i);
|
n = snd_config_iterator_entry(i);
|
||||||
if (snd_config_get_id(n, &id) < 0)
|
if (snd_config_get_id(n, &id) < 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
/* in-place condition evaluation */
|
||||||
|
if (strcmp(id, "If") == 0) {
|
||||||
|
err = uc_mgr_evaluate_condition(uc_mgr, cfg, n);
|
||||||
|
if (err < 0)
|
||||||
|
return err;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (strcmp(id, "Comment") == 0) {
|
if (strcmp(id, "Comment") == 0) {
|
||||||
err = parse_string(n, &modifier->comment);
|
err = parse_string(n, &modifier->comment);
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
|
|
@ -826,7 +842,7 @@ static int parse_device(snd_use_case_mgr_t *uc_mgr,
|
||||||
struct use_case_verb *verb = data1;
|
struct use_case_verb *verb = data1;
|
||||||
const char *name;
|
const char *name;
|
||||||
struct use_case_device *device;
|
struct use_case_device *device;
|
||||||
snd_config_iterator_t i, next;
|
snd_config_iterator_t i;
|
||||||
snd_config_t *n;
|
snd_config_t *n;
|
||||||
int err;
|
int err;
|
||||||
|
|
||||||
|
|
@ -851,12 +867,20 @@ static int parse_device(snd_use_case_mgr_t *uc_mgr,
|
||||||
list_add_tail(&device->list, &verb->device_list);
|
list_add_tail(&device->list, &verb->device_list);
|
||||||
device->name = strdup(name);
|
device->name = strdup(name);
|
||||||
|
|
||||||
snd_config_for_each(i, next, cfg) {
|
snd_config_for_each_unsafe(i, cfg) {
|
||||||
const char *id;
|
const char *id;
|
||||||
n = snd_config_iterator_entry(i);
|
n = snd_config_iterator_entry(i);
|
||||||
if (snd_config_get_id(n, &id) < 0)
|
if (snd_config_get_id(n, &id) < 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
/* in-place condition evaluation */
|
||||||
|
if (strcmp(id, "If") == 0) {
|
||||||
|
err = uc_mgr_evaluate_condition(uc_mgr, cfg, n);
|
||||||
|
if (err < 0)
|
||||||
|
return err;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (strcmp(id, "Comment") == 0) {
|
if (strcmp(id, "Comment") == 0) {
|
||||||
err = parse_string(n, &device->comment);
|
err = parse_string(n, &device->comment);
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
|
|
@ -1034,17 +1058,25 @@ static int parse_verb(snd_use_case_mgr_t *uc_mgr,
|
||||||
struct use_case_verb *verb,
|
struct use_case_verb *verb,
|
||||||
snd_config_t *cfg)
|
snd_config_t *cfg)
|
||||||
{
|
{
|
||||||
snd_config_iterator_t i, next;
|
snd_config_iterator_t i;
|
||||||
snd_config_t *n;
|
snd_config_t *n;
|
||||||
int err;
|
int err;
|
||||||
|
|
||||||
/* parse verb section */
|
/* parse verb section */
|
||||||
snd_config_for_each(i, next, cfg) {
|
snd_config_for_each_unsafe(i, cfg) {
|
||||||
const char *id;
|
const char *id;
|
||||||
n = snd_config_iterator_entry(i);
|
n = snd_config_iterator_entry(i);
|
||||||
if (snd_config_get_id(n, &id) < 0)
|
if (snd_config_get_id(n, &id) < 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
/* in-place condition evaluation */
|
||||||
|
if (strcmp(id, "If") == 0) {
|
||||||
|
err = uc_mgr_evaluate_condition(uc_mgr, cfg, n);
|
||||||
|
if (err < 0)
|
||||||
|
return err;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (strcmp(id, "EnableSequence") == 0) {
|
if (strcmp(id, "EnableSequence") == 0) {
|
||||||
uc_dbg("Parse EnableSequence");
|
uc_dbg("Parse EnableSequence");
|
||||||
err = parse_sequence(uc_mgr, &verb->enable_list, n);
|
err = parse_sequence(uc_mgr, &verb->enable_list, n);
|
||||||
|
|
@ -1103,7 +1135,7 @@ static int parse_verb_file(snd_use_case_mgr_t *uc_mgr,
|
||||||
const char *comment,
|
const char *comment,
|
||||||
const char *file)
|
const char *file)
|
||||||
{
|
{
|
||||||
snd_config_iterator_t i, next;
|
snd_config_iterator_t i;
|
||||||
snd_config_t *n;
|
snd_config_t *n;
|
||||||
struct use_case_verb *verb;
|
struct use_case_verb *verb;
|
||||||
snd_config_t *cfg;
|
snd_config_t *cfg;
|
||||||
|
|
@ -1144,12 +1176,20 @@ static int parse_verb_file(snd_use_case_mgr_t *uc_mgr,
|
||||||
}
|
}
|
||||||
|
|
||||||
/* parse master config sections */
|
/* parse master config sections */
|
||||||
snd_config_for_each(i, next, cfg) {
|
snd_config_for_each_unsafe(i, cfg) {
|
||||||
const char *id;
|
const char *id;
|
||||||
n = snd_config_iterator_entry(i);
|
n = snd_config_iterator_entry(i);
|
||||||
if (snd_config_get_id(n, &id) < 0)
|
if (snd_config_get_id(n, &id) < 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
/* in-place condition evaluation */
|
||||||
|
if (strcmp(id, "If") == 0) {
|
||||||
|
err = uc_mgr_evaluate_condition(uc_mgr, cfg, n);
|
||||||
|
if (err < 0)
|
||||||
|
return err;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
/* find verb section and parse it */
|
/* find verb section and parse it */
|
||||||
if (strcmp(id, "SectionVerb") == 0) {
|
if (strcmp(id, "SectionVerb") == 0) {
|
||||||
err = parse_verb(uc_mgr, verb, n);
|
err = parse_verb(uc_mgr, verb, n);
|
||||||
|
|
@ -1207,7 +1247,7 @@ static int parse_master_section(snd_use_case_mgr_t *uc_mgr, snd_config_t *cfg,
|
||||||
void *data1 ATTRIBUTE_UNUSED,
|
void *data1 ATTRIBUTE_UNUSED,
|
||||||
void *data2 ATTRIBUTE_UNUSED)
|
void *data2 ATTRIBUTE_UNUSED)
|
||||||
{
|
{
|
||||||
snd_config_iterator_t i, next;
|
snd_config_iterator_t i;
|
||||||
snd_config_t *n;
|
snd_config_t *n;
|
||||||
const char *use_case_name, *file = NULL, *comment = NULL;
|
const char *use_case_name, *file = NULL, *comment = NULL;
|
||||||
int err;
|
int err;
|
||||||
|
|
@ -1221,13 +1261,22 @@ static int parse_master_section(snd_use_case_mgr_t *uc_mgr, snd_config_t *cfg,
|
||||||
uc_error("compound type expected for use case section");
|
uc_error("compound type expected for use case section");
|
||||||
return -EINVAL;
|
return -EINVAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* parse master config sections */
|
/* parse master config sections */
|
||||||
snd_config_for_each(i, next, cfg) {
|
snd_config_for_each_unsafe(i, cfg) {
|
||||||
const char *id;
|
const char *id;
|
||||||
n = snd_config_iterator_entry(i);
|
n = snd_config_iterator_entry(i);
|
||||||
if (snd_config_get_id(n, &id) < 0)
|
if (snd_config_get_id(n, &id) < 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
/* in-place condition evaluation */
|
||||||
|
if (strcmp(id, "If") == 0) {
|
||||||
|
err = uc_mgr_evaluate_condition(uc_mgr, cfg, n);
|
||||||
|
if (err < 0)
|
||||||
|
return err;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
/* get use case verb file name */
|
/* get use case verb file name */
|
||||||
if (strcmp(id, "File") == 0) {
|
if (strcmp(id, "File") == 0) {
|
||||||
err = snd_config_get_string(n, &file);
|
err = snd_config_get_string(n, &file);
|
||||||
|
|
@ -1328,7 +1377,7 @@ static int parse_controls(snd_use_case_mgr_t *uc_mgr, snd_config_t *cfg)
|
||||||
*/
|
*/
|
||||||
static int parse_master_file(snd_use_case_mgr_t *uc_mgr, snd_config_t *cfg)
|
static int parse_master_file(snd_use_case_mgr_t *uc_mgr, snd_config_t *cfg)
|
||||||
{
|
{
|
||||||
snd_config_iterator_t i, next;
|
snd_config_iterator_t i;
|
||||||
snd_config_t *n;
|
snd_config_t *n;
|
||||||
const char *id;
|
const char *id;
|
||||||
long l;
|
long l;
|
||||||
|
|
@ -1357,12 +1406,20 @@ static int parse_master_file(snd_use_case_mgr_t *uc_mgr, snd_config_t *cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* parse master config sections */
|
/* parse master config sections */
|
||||||
snd_config_for_each(i, next, cfg) {
|
snd_config_for_each_unsafe(i, cfg) {
|
||||||
|
|
||||||
n = snd_config_iterator_entry(i);
|
n = snd_config_iterator_entry(i);
|
||||||
if (snd_config_get_id(n, &id) < 0)
|
if (snd_config_get_id(n, &id) < 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
/* in-place condition evaluation */
|
||||||
|
if (strcmp(id, "If") == 0) {
|
||||||
|
err = uc_mgr_evaluate_condition(uc_mgr, cfg, n);
|
||||||
|
if (err < 0)
|
||||||
|
return err;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (uc_mgr->conf_format >= 2 && strcmp(id, "Syntax") == 0)
|
if (uc_mgr->conf_format >= 2 && strcmp(id, "Syntax") == 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|
|
||||||
251
src/ucm/ucm_cond.c
Normal file
251
src/ucm/ucm_cond.c
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
/*
|
||||||
|
* 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 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, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*
|
||||||
|
* Support for the verb/device/modifier core logic and API,
|
||||||
|
* command line tool and file parser was kindly sponsored by
|
||||||
|
* Texas Instruments Inc.
|
||||||
|
* Support for multiple active modifiers and devices,
|
||||||
|
* transition sequences, multiple client access and user defined use
|
||||||
|
* cases was kindly sponsored by Wolfson Microelectronics PLC.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 Red Hat Inc.
|
||||||
|
* Authors: Jaroslav Kysela <perex@perex.cz>
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ucm_local.h"
|
||||||
|
|
||||||
|
static int get_string(snd_config_t *compound, const char *key, const char **str)
|
||||||
|
{
|
||||||
|
snd_config_t *node;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
err = snd_config_search(compound, key, &node);
|
||||||
|
if (err < 0)
|
||||||
|
return err;
|
||||||
|
return snd_config_get_string(node, str);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int if_eval_control_exists(snd_use_case_mgr_t *uc_mgr, snd_config_t *eval)
|
||||||
|
{
|
||||||
|
snd_ctl_t *ctl;
|
||||||
|
const char *device = NULL, *ctldef;
|
||||||
|
snd_ctl_elem_id_t *elem_id;
|
||||||
|
snd_ctl_elem_info_t *elem_info;
|
||||||
|
char *s;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
snd_ctl_elem_id_alloca(&elem_id);
|
||||||
|
snd_ctl_elem_info_alloca(&elem_info);
|
||||||
|
|
||||||
|
err = get_string(eval, "Device", &device);
|
||||||
|
if (err < 0 && err != -ENOENT) {
|
||||||
|
uc_error("control device error (If.Condition.Device)");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = get_string(eval, "Control", &ctldef);
|
||||||
|
if (err < 0) {
|
||||||
|
uc_error("control device error (If.Condition.Control)");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = uc_mgr_get_substituted_value(uc_mgr, &s, ctldef);
|
||||||
|
if (err < 0)
|
||||||
|
return err;
|
||||||
|
err = snd_ctl_ascii_elem_id_parse(elem_id, s);
|
||||||
|
free(s);
|
||||||
|
if (err < 0) {
|
||||||
|
uc_error("unable to parse element identificator (%s)", ctldef);
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device == NULL) {
|
||||||
|
ctl = uc_mgr_get_ctl(uc_mgr);
|
||||||
|
if (ctl == NULL) {
|
||||||
|
uc_error("cannot determine control device");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = uc_mgr_get_substituted_value(uc_mgr, &s, device);
|
||||||
|
if (err < 0)
|
||||||
|
return err;
|
||||||
|
err = uc_mgr_open_ctl(uc_mgr, &ctl, s);
|
||||||
|
free(s);
|
||||||
|
if (err < 0)
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
snd_ctl_elem_info_set_id(elem_info, elem_id);
|
||||||
|
err = snd_ctl_elem_info(ctl, elem_info);
|
||||||
|
if (err < 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int if_eval(snd_use_case_mgr_t *uc_mgr, snd_config_t *eval)
|
||||||
|
{
|
||||||
|
const char *type;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
if (snd_config_get_type(eval) != SND_CONFIG_TYPE_COMPOUND) {
|
||||||
|
uc_error("compound type expected for If.Condition");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = get_string(eval, "Type", &type);
|
||||||
|
if (err < 0) {
|
||||||
|
uc_error("type block error (If.Condition)");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(type, "ControlExists") == 0)
|
||||||
|
return if_eval_control_exists(uc_mgr, eval);
|
||||||
|
|
||||||
|
uc_error("unknown If.Condition.Type");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int if_eval_one(snd_use_case_mgr_t *uc_mgr,
|
||||||
|
snd_config_t *cond,
|
||||||
|
snd_config_t **result)
|
||||||
|
{
|
||||||
|
snd_config_t *expr, *_true = NULL, *_false = NULL;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
*result = NULL;
|
||||||
|
|
||||||
|
if (snd_config_get_type(cond) != SND_CONFIG_TYPE_COMPOUND) {
|
||||||
|
uc_error("compound type expected for If.1");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snd_config_search(cond, "Condition", &expr) < 0) {
|
||||||
|
uc_error("condition block expected (If)");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = snd_config_search(cond, "True", &_true);
|
||||||
|
if (err < 0 && err != -ENOENT) {
|
||||||
|
uc_error("true block error (If)");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = snd_config_search(cond, "False", &_false);
|
||||||
|
if (err < 0 && err != -ENOENT) {
|
||||||
|
uc_error("false block error (If)");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = if_eval(uc_mgr, expr);
|
||||||
|
if (err > 0) {
|
||||||
|
*result = _true;
|
||||||
|
return 0;
|
||||||
|
} else if (err == 0) {
|
||||||
|
*result = _false;
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
static void config_dump(snd_config_t *cfg)
|
||||||
|
{
|
||||||
|
snd_output_t *out;
|
||||||
|
snd_output_stdio_attach(&out, stderr, 0);
|
||||||
|
snd_output_printf(out, "-----\n");
|
||||||
|
snd_config_save(cfg, out);
|
||||||
|
snd_output_close(out);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static int compound_merge(snd_config_t *dst, snd_config_t *src)
|
||||||
|
{
|
||||||
|
snd_config_iterator_t i, next;
|
||||||
|
snd_config_t *n;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
if (snd_config_get_type(src) != SND_CONFIG_TYPE_COMPOUND) {
|
||||||
|
uc_error("compound type expected for If True/False block");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
snd_config_for_each(i, next, src) {
|
||||||
|
n = snd_config_iterator_entry(i);
|
||||||
|
err = snd_config_remove(n);
|
||||||
|
if (err < 0)
|
||||||
|
return err;
|
||||||
|
err = snd_config_add(dst, n);
|
||||||
|
if (err < 0) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* put back the result from all conditions to the parent
|
||||||
|
*/
|
||||||
|
int uc_mgr_evaluate_condition(snd_use_case_mgr_t *uc_mgr,
|
||||||
|
snd_config_t *parent,
|
||||||
|
snd_config_t *cond)
|
||||||
|
{
|
||||||
|
snd_config_iterator_t i, i2, next, next2;
|
||||||
|
snd_config_t *a, *n, *n2, *parent2;
|
||||||
|
const char *id;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
if (uc_mgr->conf_format < 2) {
|
||||||
|
uc_error("conditions are not supported for v1 syntax");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snd_config_get_type(cond) != SND_CONFIG_TYPE_COMPOUND) {
|
||||||
|
uc_error("compound type expected for If");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
snd_config_for_each(i, next, cond) {
|
||||||
|
n = snd_config_iterator_entry(i);
|
||||||
|
err = if_eval_one(uc_mgr, n, &a);
|
||||||
|
if (err < 0)
|
||||||
|
return err;
|
||||||
|
snd_config_for_each(i2, next2, a) {
|
||||||
|
n2 = snd_config_iterator_entry(i2);
|
||||||
|
err = snd_config_remove(n2);
|
||||||
|
if (err < 0)
|
||||||
|
return err;
|
||||||
|
err = snd_config_get_id(n2, &id);
|
||||||
|
if (err < 0) {
|
||||||
|
__add:
|
||||||
|
err = snd_config_add(parent, n2);
|
||||||
|
if (err < 0)
|
||||||
|
return err;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
err = snd_config_search(parent, id, &parent2);
|
||||||
|
if (err == -ENOENT)
|
||||||
|
goto __add;
|
||||||
|
err = compound_merge(parent2, n2);
|
||||||
|
if (err < 0)
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
snd_config_delete(n2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -263,8 +263,17 @@ int uc_mgr_open_ctl(snd_use_case_mgr_t *uc_mgr,
|
||||||
const char *device);
|
const char *device);
|
||||||
|
|
||||||
struct ctl_list *uc_mgr_get_one_ctl(snd_use_case_mgr_t *uc_mgr);
|
struct ctl_list *uc_mgr_get_one_ctl(snd_use_case_mgr_t *uc_mgr);
|
||||||
|
snd_ctl_t *uc_mgr_get_ctl(snd_use_case_mgr_t *uc_mgr);
|
||||||
void uc_mgr_free_ctl_list(snd_use_case_mgr_t *uc_mgr);
|
void uc_mgr_free_ctl_list(snd_use_case_mgr_t *uc_mgr);
|
||||||
|
|
||||||
|
int uc_mgr_get_substituted_value(snd_use_case_mgr_t *uc_mgr,
|
||||||
|
char **_rvalue,
|
||||||
|
const char *value);
|
||||||
|
|
||||||
|
int uc_mgr_evaluate_condition(snd_use_case_mgr_t *uc_mgr,
|
||||||
|
snd_config_t *parent,
|
||||||
|
snd_config_t *cond);
|
||||||
|
|
||||||
/** The name of the environment variable containing the UCM directory */
|
/** The name of the environment variable containing the UCM directory */
|
||||||
#define ALSA_CONFIG_UCM_VAR "ALSA_CONFIG_UCM"
|
#define ALSA_CONFIG_UCM_VAR "ALSA_CONFIG_UCM"
|
||||||
|
|
||||||
|
|
|
||||||
172
src/ucm/ucm_subs.c
Normal file
172
src/ucm/ucm_subs.c
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
/*
|
||||||
|
* 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 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, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*
|
||||||
|
* Support for the verb/device/modifier core logic and API,
|
||||||
|
* command line tool and file parser was kindly sponsored by
|
||||||
|
* Texas Instruments Inc.
|
||||||
|
* Support for multiple active modifiers and devices,
|
||||||
|
* transition sequences, multiple client access and user defined use
|
||||||
|
* cases was kindly sponsored by Wolfson Microelectronics PLC.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 Red Hat Inc.
|
||||||
|
* Authors: Jaroslav Kysela <perex@perex.cz>
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ucm_local.h"
|
||||||
|
|
||||||
|
static char *rval_conf_name(snd_use_case_mgr_t *uc_mgr)
|
||||||
|
{
|
||||||
|
if (uc_mgr->conf_file_name[0])
|
||||||
|
return strdup(uc_mgr->conf_file_name);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *rval_card_id(snd_use_case_mgr_t *uc_mgr)
|
||||||
|
{
|
||||||
|
struct ctl_list *ctl_list;
|
||||||
|
|
||||||
|
ctl_list = uc_mgr_get_one_ctl(uc_mgr);
|
||||||
|
if (ctl_list == NULL)
|
||||||
|
return NULL;
|
||||||
|
return strdup(snd_ctl_card_info_get_id(ctl_list->ctl_info));
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *rval_card_name(snd_use_case_mgr_t *uc_mgr)
|
||||||
|
{
|
||||||
|
struct ctl_list *ctl_list;
|
||||||
|
|
||||||
|
ctl_list = uc_mgr_get_one_ctl(uc_mgr);
|
||||||
|
if (ctl_list == NULL)
|
||||||
|
return NULL;
|
||||||
|
return strdup(snd_ctl_card_info_get_name(ctl_list->ctl_info));
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *rval_card_longname(snd_use_case_mgr_t *uc_mgr)
|
||||||
|
{
|
||||||
|
struct ctl_list *ctl_list;
|
||||||
|
|
||||||
|
ctl_list = uc_mgr_get_one_ctl(uc_mgr);
|
||||||
|
if (ctl_list == NULL)
|
||||||
|
return NULL;
|
||||||
|
return strdup(snd_ctl_card_info_get_longname(ctl_list->ctl_info));
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *rval_env(snd_use_case_mgr_t *uc_mgr ATTRIBUTE_UNUSED, const char *id)
|
||||||
|
{
|
||||||
|
char *e;
|
||||||
|
|
||||||
|
e = getenv(id);
|
||||||
|
if (e)
|
||||||
|
return strdup(e);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define MATCH_VARIABLE(name, id, fcn) \
|
||||||
|
if (strncmp((name), (id), sizeof(id) - 1) == 0) { \
|
||||||
|
rval = fcn(uc_mgr); \
|
||||||
|
idsize = sizeof(id) - 1; \
|
||||||
|
goto __rval; \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define MATCH_VARIABLE2(name, id, fcn) \
|
||||||
|
if (strncmp((name), (id), sizeof(id) - 1) == 0) { \
|
||||||
|
idsize = sizeof(id) - 1; \
|
||||||
|
tmp = strchr(value + idsize, '}'); \
|
||||||
|
if (tmp) { \
|
||||||
|
rvalsize = tmp - (value + idsize); \
|
||||||
|
if (rvalsize > sizeof(v2)) { \
|
||||||
|
err = -ENOMEM; \
|
||||||
|
goto __error; \
|
||||||
|
} \
|
||||||
|
strncpy(v2, value + idsize, rvalsize); \
|
||||||
|
v2[rvalsize] = '\0'; \
|
||||||
|
idsize += rvalsize + 1; \
|
||||||
|
rval = fcn(uc_mgr, v2); \
|
||||||
|
goto __rval; \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
int uc_mgr_get_substituted_value(snd_use_case_mgr_t *uc_mgr,
|
||||||
|
char **_rvalue,
|
||||||
|
const char *value)
|
||||||
|
{
|
||||||
|
size_t size, nsize, idsize, rvalsize, dpos = 0;
|
||||||
|
const char *tmp;
|
||||||
|
char *r, *nr, *rval, v2[32];
|
||||||
|
int err;
|
||||||
|
|
||||||
|
if (value == NULL)
|
||||||
|
return -ENOENT;
|
||||||
|
|
||||||
|
size = strlen(value) + 1;
|
||||||
|
r = malloc(size);
|
||||||
|
if (r == NULL)
|
||||||
|
return -ENOMEM;
|
||||||
|
|
||||||
|
while (*value) {
|
||||||
|
if (*value == '$' && *(value+1) == '{') {
|
||||||
|
MATCH_VARIABLE(value, "${ConfName}", rval_conf_name);
|
||||||
|
MATCH_VARIABLE(value, "${CardId}", rval_card_id);
|
||||||
|
MATCH_VARIABLE(value, "${CardName}", rval_card_name);
|
||||||
|
MATCH_VARIABLE(value, "${CardLongName}", rval_card_longname);
|
||||||
|
MATCH_VARIABLE2(value, "${env:", rval_env);
|
||||||
|
err = -EINVAL;
|
||||||
|
tmp = strchr(value, '}');
|
||||||
|
if (tmp) {
|
||||||
|
strncpy(r, value, tmp + 1 - value);
|
||||||
|
r[tmp + 1 - value] = '\0';
|
||||||
|
uc_error("variable '%s' is not known!", r);
|
||||||
|
} else {
|
||||||
|
uc_error("variable reference '%s' is not complete", value);
|
||||||
|
}
|
||||||
|
goto __error;
|
||||||
|
__rval:
|
||||||
|
if (rval == NULL || rval[0] == '\0') {
|
||||||
|
free(rval);
|
||||||
|
strncpy(r, value, idsize);
|
||||||
|
r[idsize] = '\0';
|
||||||
|
uc_error("variable '%s' is not defined in this context!", r);
|
||||||
|
err = -EINVAL;
|
||||||
|
goto __error;
|
||||||
|
}
|
||||||
|
value += idsize;
|
||||||
|
rvalsize = strlen(rval);
|
||||||
|
nsize = size + rvalsize - idsize;
|
||||||
|
if (nsize > size) {
|
||||||
|
nr = realloc(r, nsize);
|
||||||
|
if (nr == NULL) {
|
||||||
|
err = -ENOMEM;
|
||||||
|
goto __error;
|
||||||
|
}
|
||||||
|
size = nsize;
|
||||||
|
r = nr;
|
||||||
|
}
|
||||||
|
strcpy(r + dpos, rval);
|
||||||
|
dpos += rvalsize;
|
||||||
|
free(rval);
|
||||||
|
} else {
|
||||||
|
r[dpos++] = *value;
|
||||||
|
value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r[dpos] = '\0';
|
||||||
|
|
||||||
|
*_rvalue = r;
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
__error:
|
||||||
|
free(r);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
@ -64,6 +64,16 @@ struct ctl_list *uc_mgr_get_one_ctl(snd_use_case_mgr_t *uc_mgr)
|
||||||
return ctl_list;
|
return ctl_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
snd_ctl_t *uc_mgr_get_ctl(snd_use_case_mgr_t *uc_mgr)
|
||||||
|
{
|
||||||
|
struct ctl_list *ctl_list;
|
||||||
|
|
||||||
|
ctl_list = uc_mgr_get_one_ctl(uc_mgr);
|
||||||
|
if (ctl_list)
|
||||||
|
return ctl_list->ctl;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
static void uc_mgr_free_ctl(struct ctl_list *ctl_list)
|
static void uc_mgr_free_ctl(struct ctl_list *ctl_list)
|
||||||
{
|
{
|
||||||
struct list_head *pos, *npos;
|
struct list_head *pos, *npos;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue