~emersion/public-inbox

mako: Generalize string criteria regex matching v1 SUPERSEDED

Zach DeCook: 4
 Generalize string criteria regex matching
 Config Parsing: Allow specifying string fields as truthy or falsey
 Criteria: Only allow != and ~= operators for string fields
 Update man page for generalized string matching

 7 files changed, 165 insertions(+), 129 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~emersion/public-inbox/patches/43528/mbox | git am -3
Learn more about email & git

[PATCH mako 1/4] Generalize string criteria regex matching Export this patch

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(&notif->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 ||
-- 
2.40.1

[PATCH mako 2/4] Config Parsing: Allow specifying string fields as truthy or falsey Export this patch

---
 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);
-- 
2.40.1

[PATCH mako 3/4] Criteria: Only allow != and ~= operators for string fields Export this patch

---
 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;
	}

-- 
2.40.1

[PATCH mako 4/4] Update man page for generalized string matching Export this patch

---
 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\]
-- 
2.40.1