From 94bf6a2dbab04d00651317d1cc48dca8c6c35573 Mon Sep 17 00:00:00 2001 From: Zach DeCook Date: Sat, 29 Jul 2023 07:47:37 -0400 Subject: [PATCH 1/4] Generalize string criteria regex matching Closes #306 --- criteria.c | 177 ++++++++++++++++++++++++++------------------- include/criteria.h | 22 ++++-- include/types.h | 2 - types.c | 2 - 4 files changed, 117 insertions(+), 86 deletions(-) diff --git a/criteria.c b/criteria.c index e435711..838b19d 100644 --- a/criteria.c +++ b/criteria.c @@ -27,18 +27,34 @@ struct mako_criteria *create_criteria(struct mako_config *config) { return criteria; } +void free_cond(struct mako_condition *cond) { + switch(cond->operator) { + case OP_EQUALS: + case OP_NOT_EQUALS: + free(cond->value); + return; + case OP_REGEX_MATCHES: + regfree(&cond->pattern); + return; + case OP_NONE: + case OP_TRUTHY: + case OP_FALSEY: + default: + // Nothing to free. + return; + } +} + void destroy_criteria(struct mako_criteria *criteria) { wl_list_remove(&criteria->link); finish_style(&criteria->style); - free(criteria->app_name); - free(criteria->app_icon); - free(criteria->category); - free(criteria->desktop_entry); - free(criteria->summary); - regfree(&criteria->summary_pattern); - free(criteria->body); - regfree(&criteria->body_pattern); + free_cond(&criteria->app_name); + free_cond(&criteria->app_icon); + free_cond(&criteria->category); + free_cond(&criteria->desktop_entry); + free_cond(&criteria->summary); + free_cond(&criteria->body); free(criteria->raw_string); free(criteria->output); free(criteria->mode); @@ -59,6 +75,24 @@ static bool match_regex_criteria(regex_t *pattern, char *value) { return true; } +bool match_condition(struct mako_condition *cond, char *value) { + switch(cond->operator) { + case OP_EQUALS: + return strcmp(cond->value, value) == 0; + case OP_NOT_EQUALS: + return strcmp(cond->value, value) != 0; + case OP_REGEX_MATCHES: + return match_regex_criteria(&cond->pattern, value); + case OP_TRUTHY: + return strcmp("", value) != 0; + case OP_FALSEY: + return strcmp("", value) == 0; + case OP_NONE: + return true; + } + return true; +} + bool match_criteria(struct mako_criteria *criteria, struct mako_notification *notif) { struct mako_criteria_spec spec = criteria->spec; @@ -74,12 +108,12 @@ bool match_criteria(struct mako_criteria *criteria, } if (spec.app_name && - strcmp(criteria->app_name, notif->app_name) != 0) { + !match_condition(&criteria->app_name, notif->app_name)) { return false; } if (spec.app_icon && - strcmp(criteria->app_icon, notif->app_icon) != 0) { + !match_condition(&criteria->app_icon, notif->app_icon)) { return false; } @@ -99,39 +133,25 @@ bool match_criteria(struct mako_criteria *criteria, } if (spec.category && - strcmp(criteria->category, notif->category) != 0) { + !match_condition(&criteria->category, notif->category)) { return false; } if (spec.desktop_entry && - strcmp(criteria->desktop_entry, notif->desktop_entry) != 0) { + !match_condition(&criteria->desktop_entry, notif->desktop_entry)) { return false; } if (spec.summary && - strcmp(criteria->summary, notif->summary) != 0) { + !match_condition(&criteria->summary, notif->summary)) { return false; } - if (spec.summary_pattern) { - bool ret = match_regex_criteria(&criteria->summary_pattern, notif->summary); - if (!ret) { - return false; - } - } - if (spec.body && - strcmp(criteria->body, notif->body) != 0) { + !match_condition(&criteria->body, notif->body)) { return false; } - if (spec.body_pattern) { - bool ret = match_regex_criteria(&criteria->body_pattern, notif->body); - if (!ret) { - return false; - } - } - if (spec.group_index && criteria->group_index != notif->group_index) { return false; @@ -258,14 +278,38 @@ bool parse_criteria(const char *string, struct mako_criteria *criteria) { return true; } -// Takes a token from the criteria string that looks like "key=value", figures -// out which field of the criteria "key" refers to, and sets it to "value". +bool assign_condition(struct mako_condition *cond, enum operator op, char *value) { + cond->operator = op; + switch (op) { + case OP_REGEX_MATCHES: + if (regcomp(&cond->pattern, value, REG_EXTENDED | REG_NOSUB)) { + fprintf(stderr, "Invalid regex '%s'\n", value); + return false; + } + return true; + case OP_EQUALS: + case OP_NOT_EQUALS: + cond->value = strdup(value); + // fall-thru + case OP_FALSEY: + case OP_TRUTHY: + case OP_NONE: + default: + return true; + } + return true; +} + +// Takes a token from the criteria string that looks like +// "key=value", "key!=value", or "key~=value"; and figures +// out which field of the criteria "key" refers to, and sets it to the condition. // Any further equal signs are assumed to be part of the value. If there is no . // equal sign present, the field is treated as a boolean, with a leading // exclamation point signifying negation. // // Note that the token will be consumed. bool apply_criteria_field(struct mako_criteria *criteria, char *token) { + enum operator op = OP_EQUALS; char *key = token; char *value = strstr(key, "="); bool bare_key = !value; @@ -275,6 +319,15 @@ bool apply_criteria_field(struct mako_criteria *criteria, char *token) { } if (value) { + if(value[-1] == '~') { + op = OP_REGEX_MATCHES; + // shorten the key. + value[-1] = '\0'; + } else if (value[-1] == '!') { + op = OP_NOT_EQUALS; + // shorten the key. + value[-1] = '\0'; + } // Skip past the equal sign to the value itself. *value = '\0'; ++value; @@ -284,8 +337,10 @@ bool apply_criteria_field(struct mako_criteria *criteria, char *token) { if (*key == '!') { // Negated boolean, skip past the exclamation point. ++key; + op = OP_FALSEY; value = "false"; } else { + op = OP_TRUTHY; value = "true"; } } @@ -297,13 +352,11 @@ bool apply_criteria_field(struct mako_criteria *criteria, char *token) { if (!bare_key) { if (strcmp(key, "app-name") == 0) { - criteria->app_name = strdup(value); criteria->spec.app_name = true; - return true; + return assign_condition(&criteria->app_name, op, value); } else if (strcmp(key, "app-icon") == 0) { - criteria->app_icon = strdup(value); criteria->spec.app_icon = true; - return true; + return assign_condition(&criteria->app_icon, op, value); } else if (strcmp(key, "urgency") == 0) { if (!parse_urgency(value, &criteria->urgency)) { fprintf(stderr, "Invalid urgency value '%s'", value); @@ -312,13 +365,11 @@ bool apply_criteria_field(struct mako_criteria *criteria, char *token) { criteria->spec.urgency = true; return true; } else if (strcmp(key, "category") == 0) { - criteria->category = strdup(value); criteria->spec.category = true; - return true; + return assign_condition(&criteria->category, op, value); } else if (strcmp(key, "desktop-entry") == 0) { - criteria->desktop_entry = strdup(value); criteria->spec.desktop_entry = true; - return true; + return assign_condition(&criteria->desktop_entry, op, value); } else if (strcmp(key, "group-index") == 0) { if (!parse_int(value, &criteria->group_index)) { fprintf(stderr, "Invalid group-index value '%s'", value); @@ -327,29 +378,11 @@ bool apply_criteria_field(struct mako_criteria *criteria, char *token) { criteria->spec.group_index = true; return true; } else if (strcmp(key, "summary") == 0) { - criteria->summary = strdup(value); criteria->spec.summary = true; - return true; - } else if (strcmp(key, "summary~") == 0) { - if (regcomp(&criteria->summary_pattern, value, - REG_EXTENDED | REG_NOSUB)) { - fprintf(stderr, "Invalid summary~ regex '%s'\n", value); - return false; - } - criteria->spec.summary_pattern = true; - return true; + return assign_condition(&criteria->summary, op, value); } else if (strcmp(key, "body") == 0) { - criteria->body = strdup(value); criteria->spec.body = true; - return true; - } else if (strcmp(key, "body~") == 0) { - if (regcomp(&criteria->body_pattern, value, - REG_EXTENDED | REG_NOSUB)) { - fprintf(stderr, "Invalid body~ regex '%s'\n", value); - return false; - } - criteria->spec.body_pattern = true; - return true; + return assign_condition(&criteria->body, op, value); } else if (strcmp(key, "anchor") == 0) { return criteria->spec.anchor = parse_anchor(value, &criteria->anchor); @@ -477,15 +510,21 @@ struct mako_criteria *create_criteria_from_notification( // We only really need to copy the ones that are in the spec, but it // doesn't hurt anything to do the rest and it makes this code much nicer // to look at. - criteria->app_name = strdup(notif->app_name); - criteria->app_icon = strdup(notif->app_icon); + criteria->app_name.operator = OP_EQUALS; + criteria->app_name.value = strdup(notif->app_name); + criteria->app_icon.operator = OP_EQUALS; + criteria->app_icon.value = strdup(notif->app_icon); criteria->actionable = !wl_list_empty(¬if->actions); criteria->expiring = (notif->requested_timeout != 0); criteria->urgency = notif->urgency; - criteria->category = strdup(notif->category); - criteria->desktop_entry = strdup(notif->desktop_entry); - criteria->summary = strdup(notif->summary); - criteria->body = strdup(notif->body); + criteria->category.operator = OP_EQUALS; + criteria->category.value = strdup(notif->category); + criteria->desktop_entry.operator = OP_EQUALS; + criteria->desktop_entry.value = strdup(notif->desktop_entry); + criteria->summary.operator = OP_EQUALS; + criteria->summary.value = strdup(notif->summary); + criteria->body.operator = OP_EQUALS; + criteria->body.value = strdup(notif->body); criteria->group_index = notif->group_index; criteria->grouped = (notif->group_index >= 0); criteria->hidden = notif->hidden; @@ -540,16 +579,6 @@ bool validate_criteria(struct mako_criteria *criteria) { return false; } - if (criteria->spec.summary && criteria->spec.summary_pattern) { - fprintf(stderr, "Cannot set both `summary` and `summary~`\n"); - return false; - } - - if (criteria->spec.body && criteria->spec.body_pattern) { - fprintf(stderr, "Cannot set both `body` and `body~`\n"); - return false; - } - if (criteria->style.spec.group_criteria_spec) { struct mako_criteria_spec *spec = &criteria->style.group_criteria_spec; diff --git a/include/criteria.h b/include/criteria.h index 2f03514..dd0646b 100644 --- a/include/criteria.h +++ b/include/criteria.h @@ -11,6 +11,14 @@ struct mako_config; struct mako_notification; +enum operator { OP_NONE, OP_EQUALS, OP_REGEX_MATCHES, OP_NOT_EQUALS, OP_TRUTHY, OP_FALSEY }; + +struct mako_condition { + enum operator operator; + char* value; + regex_t pattern; +}; + struct mako_criteria { struct mako_criteria_spec spec; struct wl_list link; // mako_config::criteria @@ -21,17 +29,15 @@ struct mako_criteria { struct mako_style style; // Fields that can be matched: - char *app_name; - char *app_icon; + struct mako_condition app_name; + struct mako_condition app_icon; bool actionable; // Whether mako_notification.actions is nonempty bool expiring; // Whether mako_notification.requested_timeout is non-zero enum mako_notification_urgency urgency; - char *category; - char *desktop_entry; - char *summary; - regex_t summary_pattern; - char *body; - regex_t body_pattern; + struct mako_condition category; + struct mako_condition desktop_entry; + struct mako_condition summary; + struct mako_condition body; char *mode; diff --git a/include/types.h b/include/types.h index a42d80d..2f77adb 100644 --- a/include/types.h +++ b/include/types.h @@ -51,9 +51,7 @@ struct mako_criteria_spec { bool category; bool desktop_entry; bool summary; - bool summary_pattern; bool body; - bool body_pattern; bool mode; diff --git a/types.c b/types.c index 9612d0f..8c22148 100644 --- a/types.c +++ b/types.c @@ -245,9 +245,7 @@ bool mako_criteria_spec_any(const struct mako_criteria_spec *spec) { spec->category || spec->desktop_entry || spec->summary || - spec->summary_pattern || spec->body || - spec->body_pattern || spec->none || spec->group_index || spec->grouped || From b489396bf9514d92497dd5947433761db6e6fa7a Mon Sep 17 00:00:00 2001 From: Zach DeCook Date: Sat, 29 Jul 2023 07:59:47 -0400 Subject: [PATCH 2/4] Config Parsing: Allow specifying string fields as truthy or falsey --- criteria.c | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/criteria.c b/criteria.c index 838b19d..ec7515c 100644 --- a/criteria.c +++ b/criteria.c @@ -350,26 +350,34 @@ bool apply_criteria_field(struct mako_criteria *criteria, char *token) { // Otherwise, anything is fair game. This helps to return a better error // message. - if (!bare_key) { - if (strcmp(key, "app-name") == 0) { - criteria->spec.app_name = true; - return assign_condition(&criteria->app_name, op, value); - } else if (strcmp(key, "app-icon") == 0) { - criteria->spec.app_icon = true; - return assign_condition(&criteria->app_icon, op, value); - } else if (strcmp(key, "urgency") == 0) { + // String fields can have bare_key, or not bare_key + + if (strcmp(key, "app-name") == 0) { + criteria->spec.app_name = true; + return assign_condition(&criteria->app_name, op, value); + } else if (strcmp(key, "app-icon") == 0) { + criteria->spec.app_icon = true; + return assign_condition(&criteria->app_icon, op, value); + } else if (strcmp(key, "category") == 0) { + criteria->spec.category = true; + return assign_condition(&criteria->category, op, value); + } else if (strcmp(key, "desktop-entry") == 0) { + criteria->spec.desktop_entry = true; + return assign_condition(&criteria->desktop_entry, op, value); + } else if (strcmp(key, "summary") == 0) { + criteria->spec.summary = true; + return assign_condition(&criteria->summary, op, value); + } else if (strcmp(key, "body") == 0) { + criteria->spec.body = true; + return assign_condition(&criteria->body, op, value); + } else if (!bare_key) { + if (strcmp(key, "urgency") == 0) { if (!parse_urgency(value, &criteria->urgency)) { fprintf(stderr, "Invalid urgency value '%s'", value); return false; } criteria->spec.urgency = true; return true; - } else if (strcmp(key, "category") == 0) { - criteria->spec.category = true; - return assign_condition(&criteria->category, op, value); - } else if (strcmp(key, "desktop-entry") == 0) { - criteria->spec.desktop_entry = true; - return assign_condition(&criteria->desktop_entry, op, value); } else if (strcmp(key, "group-index") == 0) { if (!parse_int(value, &criteria->group_index)) { fprintf(stderr, "Invalid group-index value '%s'", value); @@ -377,12 +385,6 @@ bool apply_criteria_field(struct mako_criteria *criteria, char *token) { } criteria->spec.group_index = true; return true; - } else if (strcmp(key, "summary") == 0) { - criteria->spec.summary = true; - return assign_condition(&criteria->summary, op, value); - } else if (strcmp(key, "body") == 0) { - criteria->spec.body = true; - return assign_condition(&criteria->body, op, value); } else if (strcmp(key, "anchor") == 0) { return criteria->spec.anchor = parse_anchor(value, &criteria->anchor); From 1532df3fba8d1887bfd78224579752dc69d64299 Mon Sep 17 00:00:00 2001 From: Zach DeCook Date: Sat, 29 Jul 2023 07:59:34 -0400 Subject: [PATCH 3/4] Update man page for generalized string matching --- mako.5.scd | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/mako.5.scd b/mako.5.scd index b08dc81..4ba9a65 100644 --- a/mako.5.scd +++ b/mako.5.scd @@ -287,19 +287,7 @@ The following fields are available in criteria: - _app-name_ (string) - _app-icon_ (string) - _summary_ (string) - - An exact match on the summary of the notification. - - This field conflicts with _summary~_. -- _summary~_ (string) - - A POSIX extended regular expression match on the summary of the - notification. - - This field conflicts with _summary_. - _body_ (string) - - An exact match on the body of the notification. - - This field conflicts with _body~_. -- _body~_ (string) - - A POSIX extended regular expression match on the body of the - notification. - - This field conflicts with _body_. - _urgency_ (one of "low", "normal", "critical") - _category_ (string) - _desktop-entry_ (string) @@ -324,7 +312,7 @@ where previous style options decided to place each notification: - _output_ (string) - Which output the notification was sorted onto. See the output style option for possible values. -- _anchor_ (string) +- _anchor_ (e.g. "top-center", "center-right", "center"...) - Which position on the output the notification was assigned to. See the anchor style option for possible values. @@ -342,7 +330,19 @@ Quotes within quotes may also be escaped, and a literal backslash may be specified as \\\\. No spaces are allowed around the equal sign. Escaping equal signs within values is unnecessary. -Additionally, boolean values may be specified using any of true/false, 0/1, or +All string fields except mode and output support additional operators != and ~= + +- != indicates a non-match +- ~= indicates a POSIX extended regular expression match + +These string fields can also be specified as a bare word, which is equivalent to +comparisons with the empty string: + + \[summary\] \[summary!=""\] + + \[!summary\] \[summary=""\] + +Boolean values may be specified using any of true/false, 0/1, or as bare words: \[actionable=true\] \[actionable=1\] \[actionable\] From 7a510ed38ebc717f98ef47924943cd5affa8e68e Mon Sep 17 00:00:00 2001 From: Zach DeCook Date: Mon, 31 Jul 2023 12:09:40 -0400 Subject: [PATCH 4/4] Criteria: Only allow != and ~= operators for string fields --- criteria.c | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/criteria.c b/criteria.c index ec7515c..92942bb 100644 --- a/criteria.c +++ b/criteria.c @@ -312,7 +312,6 @@ bool apply_criteria_field(struct mako_criteria *criteria, char *token) { enum operator op = OP_EQUALS; char *key = token; char *value = strstr(key, "="); - bool bare_key = !value; if (*key == '\0') { return true; @@ -350,8 +349,7 @@ bool apply_criteria_field(struct mako_criteria *criteria, char *token) { // Otherwise, anything is fair game. This helps to return a better error // message. - // String fields can have bare_key, or not bare_key - + // String fields that support all operators if (strcmp(key, "app-name") == 0) { criteria->spec.app_name = true; return assign_condition(&criteria->app_name, op, value); @@ -370,7 +368,7 @@ bool apply_criteria_field(struct mako_criteria *criteria, char *token) { } else if (strcmp(key, "body") == 0) { criteria->spec.body = true; return assign_condition(&criteria->body, op, value); - } else if (!bare_key) { + } else if (op == OP_EQUALS) { if (strcmp(key, "urgency") == 0) { if (!parse_urgency(value, &criteria->urgency)) { fprintf(stderr, "Invalid urgency value '%s'", value); @@ -402,6 +400,15 @@ bool apply_criteria_field(struct mako_criteria *criteria, char *token) { } } + if (op == OP_REGEX_MATCHES) { + fprintf(stderr, "Invalid criteria field/operator '%s~='\n", key); + return false; + } else if (op == OP_NOT_EQUALS) { + // TODO: Support != for boolean fields. + fprintf(stderr, "Invalid criteria field/operator '%s!='\n", key); + return false; + } + if (strcmp(key, "actionable") == 0) { if (!parse_boolean(value, &criteria->actionable)) { fprintf(stderr, "Invalid value '%s' for boolean field '%s'\n", @@ -435,11 +442,7 @@ bool apply_criteria_field(struct mako_criteria *criteria, char *token) { criteria->spec.hidden = true; return true; } else { - if (bare_key) { - fprintf(stderr, "Invalid boolean criteria field '%s'\n", key); - } else { - fprintf(stderr, "Invalid criteria field '%s'\n", key); - } + fprintf(stderr, "Invalid criteria field '%s'\n", key); return false; }