From 6aae8f0d3c20aa35f8827ef355bf13a13187abf3 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Thu, 15 Feb 2024 13:57:06 +0000 Subject: [PATCH 1/9] c2: split up c2_match_once_leaf Split string and integer cases into 2 separate functions. Signed-off-by: Yuxuan Shui --- src/c2.c | 532 +++++++++++++++++++++++++++---------------------------- 1 file changed, 258 insertions(+), 274 deletions(-) diff --git a/src/c2.c b/src/c2.c index 21d0c962..bba2e528 100644 --- a/src/c2.c +++ b/src/c2.c @@ -10,6 +10,7 @@ * */ +#include #include #include #include @@ -1428,282 +1429,277 @@ static const char *c2_condition_to_str2(c2_ptr_t ptr) { return buf; } +static bool +c2_match_once_leaf_int(session_t *ps, const struct managed_win *w, const c2_l_t *leaf) { + long long *targets = NULL; + long long *targets_free = NULL; + size_t ntargets = 0; + const xcb_window_t wid = (leaf->tgt_onframe ? w->client_win : w->base.id); + + // Get the value + // A predefined target + long long predef_target = 0; + if (leaf->predef != C2_L_PUNDEFINED) { + switch (leaf->predef) { + case C2_L_PID: predef_target = wid; break; + case C2_L_PX: predef_target = w->g.x; break; + case C2_L_PY: predef_target = w->g.y; break; + case C2_L_PX2: predef_target = w->g.x + w->widthb; break; + case C2_L_PY2: predef_target = w->g.y + w->heightb; break; + case C2_L_PWIDTH: predef_target = w->g.width; break; + case C2_L_PHEIGHT: predef_target = w->g.height; break; + case C2_L_PWIDTHB: predef_target = w->widthb; break; + case C2_L_PHEIGHTB: predef_target = w->heightb; break; + case C2_L_PBDW: predef_target = w->g.border_width; break; + case C2_L_PFULLSCREEN: predef_target = w->is_fullscreen; break; + case C2_L_POVREDIR: predef_target = w->a.override_redirect; break; + case C2_L_PARGB: predef_target = win_has_alpha(w); break; + case C2_L_PFOCUSED: predef_target = win_is_focused_raw(w); break; + case C2_L_PWMWIN: predef_target = w->wmwin; break; + case C2_L_PBSHAPED: predef_target = w->bounding_shaped; break; + case C2_L_PROUNDED: predef_target = w->rounded_corners; break; + case C2_L_PCLIENT: predef_target = w->client_win; break; + case C2_L_PLEADER: predef_target = w->leader; break; + default: unreachable(); + } + ntargets = 1; + targets = &predef_target; + } else { + // A raw window property + int word_count = 1; + int offset = leaf->index; + if (leaf->index < 0) { + // index < 0 means match any index + // Get length of property in 32-bit multiples + auto prop_info = x_get_prop_info(&ps->c, wid, leaf->tgtatom); + word_count = to_int_checked((prop_info.length + 4 - 1) / 4); + offset = 0; + } + winprop_t prop = x_get_prop_with_offset(&ps->c, wid, leaf->tgtatom, + offset, word_count, 0, 0); + + ntargets = (leaf->index < 0 ? prop.nitems : min2(prop.nitems, 1)); + if (ntargets > 0) { + targets = targets_free = ccalloc(ntargets, long long); + for (size_t i = 0; i < ntargets; ++i) { + targets[i] = winprop_get_int(prop, i); + } + } + free_winprop(&prop); + } + + // Do comparison + bool matches = false; + for (size_t i = 0; i < ntargets; ++i) { + long long tgt = targets[i]; + switch (leaf->op) { + case C2_L_OEXISTS: + matches = (leaf->predef != C2_L_PUNDEFINED ? tgt : true); + break; + case C2_L_OEQ: matches = (tgt == leaf->ptnint); break; + case C2_L_OGT: matches = (tgt > leaf->ptnint); break; + case C2_L_OGTEQ: matches = (tgt >= leaf->ptnint); break; + case C2_L_OLT: matches = (tgt < leaf->ptnint); break; + case C2_L_OLTEQ: matches = (tgt <= leaf->ptnint); break; + default: unreachable(); + } + if (matches) { + break; + } + } + + // Free property values after usage, if necessary + if (targets_free) { + free(targets_free); + } + return matches; +} + +static bool c2_string_op(const c2_l_t *leaf, const char *target) { + if (leaf->op == C2_L_OEXISTS) { + return true; + } + if (leaf->op != C2_L_OEQ) { + log_error("Unsupported operator %d for string comparison.", leaf->op); + assert(leaf->op == C2_L_OEQ); + return false; + } + if (leaf->match == C2_L_MPCRE) { +#ifdef CONFIG_REGEX_PCRE + assert(strlen(target) <= INT_MAX); + assert(leaf->regex_pcre); + return (pcre2_match(leaf->regex_pcre, (PCRE2_SPTR)target, strlen(target), + 0, 0, leaf->regex_pcre_match, NULL) > 0); +#else + log_error("PCRE regular expression support not compiled in."); + assert(leaf->match != C2_L_MPCRE); + return false; +#endif + } + if (leaf->match_ignorecase) { + switch (leaf->match) { + case C2_L_MEXACT: return !strcasecmp(target, leaf->ptnstr); + case C2_L_MCONTAINS: return strcasestr(target, leaf->ptnstr); + case C2_L_MSTART: + return !strncasecmp(target, leaf->ptnstr, strlen(leaf->ptnstr)); + case C2_L_MWILDCARD: return !fnmatch(leaf->ptnstr, target, FNM_CASEFOLD); + default: unreachable(); + } + } else { + switch (leaf->match) { + case C2_L_MEXACT: return !strcmp(target, leaf->ptnstr); + case C2_L_MCONTAINS: return strstr(target, leaf->ptnstr); + case C2_L_MSTART: + return !strncmp(target, leaf->ptnstr, strlen(leaf->ptnstr)); + case C2_L_MWILDCARD: return !fnmatch(leaf->ptnstr, target, 0); + default: unreachable(); + } + } + unreachable(); +} + +static bool +c2_match_once_leaf_string(session_t *ps, const struct managed_win *w, const c2_l_t *leaf) { + + const char **targets = NULL; + const char **targets_free = NULL; + const char **targets_free_inner = NULL; + size_t ntargets = 0; + const xcb_window_t wid = (leaf->tgt_onframe ? w->client_win : w->base.id); + + winprop_info_t prop_info = {0}; + if (leaf->predef == C2_L_PUNDEFINED) { + prop_info = x_get_prop_info(&ps->c, wid, leaf->tgtatom); + } + + // A predefined target + const char *predef_target = NULL; + if (leaf->predef != C2_L_PUNDEFINED) { + switch (leaf->predef) { + case C2_L_PWINDOWTYPE: predef_target = WINTYPES[w->window_type]; break; + case C2_L_PNAME: predef_target = w->name; break; + case C2_L_PCLASSG: predef_target = w->class_general; break; + case C2_L_PCLASSI: predef_target = w->class_instance; break; + case C2_L_PROLE: predef_target = w->role; break; + default: unreachable(); + } + ntargets = 1; + targets = &predef_target; + } else if (prop_info.type == XCB_ATOM_ATOM) { + // An atom type property, convert it to string + int word_count = 1; + int offset = leaf->index; + if (leaf->index < 0) { + // index < 0 means match any index + // Get length of property in 32-bit multiples + word_count = to_int_checked((prop_info.length + 4 - 1) / 4); + offset = 0; + } + winprop_t prop = x_get_prop_with_offset( + &ps->c, wid, leaf->tgtatom, offset, word_count, XCB_ATOM_ATOM, 0); + + ntargets = (leaf->index < 0 ? prop.nitems : min2(prop.nitems, 1)); + targets = targets_free = (const char **)ccalloc(2 * ntargets, char *); + targets_free_inner = targets + ntargets; + + for (size_t i = 0; i < ntargets; ++i) { + xcb_atom_t atom = (xcb_atom_t)winprop_get_int(prop, i); + if (atom) { + xcb_get_atom_name_reply_t *reply = xcb_get_atom_name_reply( + ps->c.c, xcb_get_atom_name(ps->c.c, atom), NULL); + if (reply) { + targets[i] = targets_free_inner[i] = strndup( + xcb_get_atom_name_name(reply), + (size_t)xcb_get_atom_name_name_length(reply)); + free(reply); + } + } + } + free_winprop(&prop); + } else { + // Not an atom type, just fetch the string list + char **strlst = NULL; + int nstr = 0; + int index = max2(0, leaf->index); + if (wid_get_text_prop(&ps->c, ps->atoms, wid, leaf->tgtatom, &strlst, &nstr)) { + if (leaf->index < 0 && nstr > 0 && strlen(strlst[0]) > 0) { + ntargets = to_u32_checked(nstr); + targets = (const char **)strlst; + } else if (nstr > index) { + ntargets = 1; + targets = (const char **)strlst + index; + } + } + if (strlst) { + targets_free = (const char **)strlst; + } + } + + bool matches = false; + if (ntargets == 0) { + goto fail; + } + for (size_t i = 0; i < ntargets; ++i) { + if (!targets[i]) { + goto fail; + } + } + + // Actual matching + for (size_t i = 0; i < ntargets; ++i) { + if (c2_string_op(leaf, targets[i])) { + matches = true; + break; + } + } + +fail: + // Free the string after usage, if necessary + if (targets_free_inner) { + for (size_t i = 0; i < ntargets; ++i) { + if (targets_free_inner[i]) { + free((void *)targets_free_inner[i]); + } + } + } + // Free property values after usage, if necessary + if (targets_free) { + free(targets_free); + } + return matches; +} + /** * Match a window against a single leaf window condition. * * For internal use. */ -static inline void c2_match_once_leaf(session_t *ps, const struct managed_win *w, - const c2_l_t *pleaf, bool *pres, bool *perr) { - assert(pleaf); +static inline bool +c2_match_once_leaf(session_t *ps, const struct managed_win *w, const c2_l_t *leaf) { + assert(leaf); - const xcb_window_t wid = (pleaf->tgt_onframe ? w->client_win : w->base.id); + const xcb_window_t wid = (leaf->tgt_onframe ? w->client_win : w->base.id); // Return if wid is missing - if (pleaf->predef == C2_L_PUNDEFINED && !wid) { - return; + if (leaf->predef == C2_L_PUNDEFINED && !wid) { + log_debug("Window ID missing."); + return false; } - const int idx = (pleaf->index < 0 ? 0 : pleaf->index); - auto type = pleaf->ptntype; + auto type = leaf->ptntype; if (type == C2_L_PTUNDEFINED) { - auto prop_info = x_get_prop_info(&ps->c, wid, pleaf->tgtatom); + auto prop_info = x_get_prop_info(&ps->c, wid, leaf->tgtatom); if (x_is_type_string(ps->atoms, prop_info.type)) { type = C2_L_PTSTRING; } else { type = C2_L_PTINT; } } - switch (type) { // Deal with integer patterns - case C2_L_PTINT: { - long long *targets = NULL; - long long *targets_free = NULL; - size_t ntargets = 0; - - // Get the value - // A predefined target - long long predef_target = 0; - if (pleaf->predef != C2_L_PUNDEFINED) { - *perr = false; - switch (pleaf->predef) { - case C2_L_PID: predef_target = wid; break; - case C2_L_PX: predef_target = w->g.x; break; - case C2_L_PY: predef_target = w->g.y; break; - case C2_L_PX2: predef_target = w->g.x + w->widthb; break; - case C2_L_PY2: predef_target = w->g.y + w->heightb; break; - case C2_L_PWIDTH: predef_target = w->g.width; break; - case C2_L_PHEIGHT: predef_target = w->g.height; break; - case C2_L_PWIDTHB: predef_target = w->widthb; break; - case C2_L_PHEIGHTB: predef_target = w->heightb; break; - case C2_L_PBDW: predef_target = w->g.border_width; break; - case C2_L_PFULLSCREEN: predef_target = w->is_fullscreen; break; - case C2_L_POVREDIR: predef_target = w->a.override_redirect; break; - case C2_L_PARGB: predef_target = win_has_alpha(w); break; - case C2_L_PFOCUSED: predef_target = win_is_focused_raw(w); break; - case C2_L_PWMWIN: predef_target = w->wmwin; break; - case C2_L_PBSHAPED: predef_target = w->bounding_shaped; break; - case C2_L_PROUNDED: predef_target = w->rounded_corners; break; - case C2_L_PCLIENT: predef_target = w->client_win; break; - case C2_L_PLEADER: predef_target = w->leader; break; - default: - *perr = true; - assert(0); - break; - } - ntargets = 1; - targets = &predef_target; - } - // A raw window property - else { - int word_count = 1; - if (pleaf->index < 0) { - // Get length of property in 32-bit multiples - auto prop_info = x_get_prop_info(&ps->c, wid, pleaf->tgtatom); - word_count = to_int_checked((prop_info.length + 4 - 1) / 4); - } - winprop_t prop = x_get_prop_with_offset( - &ps->c, wid, pleaf->tgtatom, idx, word_count, 0, 0); - - ntargets = (pleaf->index < 0 ? prop.nitems : min2(prop.nitems, 1)); - if (ntargets > 0) { - targets = targets_free = ccalloc(ntargets, long long); - *perr = false; - for (size_t i = 0; i < ntargets; ++i) { - targets[i] = winprop_get_int(prop, i); - } - } - free_winprop(&prop); - } - - if (*perr) { - goto fail_int; - } - - // Do comparison - bool res = false; - for (size_t i = 0; i < ntargets; ++i) { - long long tgt = targets[i]; - switch (pleaf->op) { - case C2_L_OEXISTS: - res = (pleaf->predef != C2_L_PUNDEFINED ? tgt : true); - break; - case C2_L_OEQ: res = (tgt == pleaf->ptnint); break; - case C2_L_OGT: res = (tgt > pleaf->ptnint); break; - case C2_L_OGTEQ: res = (tgt >= pleaf->ptnint); break; - case C2_L_OLT: res = (tgt < pleaf->ptnint); break; - case C2_L_OLTEQ: res = (tgt <= pleaf->ptnint); break; - default: *perr = true; assert(0); - } - if (res) { - break; - } - } - *pres = res; - - fail_int: - // Free property values after usage, if necessary - if (targets_free) { - free(targets_free); - } - } break; + case C2_L_PTINT: return c2_match_once_leaf_int(ps, w, leaf); // String patterns - case C2_L_PTSTRING: { - const char **targets = NULL; - const char **targets_free = NULL; - const char **targets_free_inner = NULL; - size_t ntargets = 0; - winprop_info_t prop_info = {0}; - if (pleaf->predef == C2_L_PUNDEFINED) { - prop_info = x_get_prop_info(&ps->c, wid, pleaf->tgtatom); - } - - // A predefined target - const char *predef_target = NULL; - if (pleaf->predef != C2_L_PUNDEFINED) { - switch (pleaf->predef) { - case C2_L_PWINDOWTYPE: - predef_target = WINTYPES[w->window_type]; - break; - case C2_L_PNAME: predef_target = w->name; break; - case C2_L_PCLASSG: predef_target = w->class_general; break; - case C2_L_PCLASSI: predef_target = w->class_instance; break; - case C2_L_PROLE: predef_target = w->role; break; - default: assert(0); break; - } - ntargets = 1; - targets = &predef_target; - } else if (prop_info.type == XCB_ATOM_ATOM) { - // An atom type property, convert it to string - int word_count = 1; - if (pleaf->index < 0) { - // Get length of property in 32-bit multiples - word_count = to_int_checked((prop_info.length + 4 - 1) / 4); - } - winprop_t prop = x_get_prop_with_offset( - &ps->c, wid, pleaf->tgtatom, idx, word_count, XCB_ATOM_ATOM, 0); - - ntargets = (pleaf->index < 0 ? prop.nitems : min2(prop.nitems, 1)); - targets = targets_free = (const char **)ccalloc(2 * ntargets, char *); - targets_free_inner = targets + ntargets; - - for (size_t i = 0; i < ntargets; ++i) { - xcb_atom_t atom = (xcb_atom_t)winprop_get_int(prop, i); - if (atom) { - xcb_get_atom_name_reply_t *reply = xcb_get_atom_name_reply( - ps->c.c, xcb_get_atom_name(ps->c.c, atom), NULL); - if (reply) { - targets[i] = targets_free_inner[i] = strndup( - xcb_get_atom_name_name(reply), - (size_t)xcb_get_atom_name_name_length(reply)); - free(reply); - } - } - } - free_winprop(&prop); - } else { - // Not an atom type, just fetch the string list - char **strlst = NULL; - int nstr = 0; - if (wid_get_text_prop(&ps->c, ps->atoms, wid, pleaf->tgtatom, - &strlst, &nstr)) { - if (pleaf->index < 0 && nstr > 0 && strlen(strlst[0]) > 0) { - ntargets = to_u32_checked(nstr); - targets = (const char **)strlst; - } else if (nstr > idx) { - ntargets = 1; - targets = (const char **)strlst + idx; - } - } - if (strlst) { - targets_free = (const char **)strlst; - } - } - - if (ntargets == 0) { - goto fail_str; - } - for (size_t i = 0; i < ntargets; ++i) { - if (!targets[i]) { - goto fail_str; - } - } - *perr = false; - - // Actual matching - bool res = false; - for (size_t i = 0; i < ntargets; ++i) { - const char *tgt = targets[i]; - switch (pleaf->op) { - case C2_L_OEXISTS: res = true; break; - case C2_L_OEQ: - switch (pleaf->match) { - case C2_L_MEXACT: - if (pleaf->match_ignorecase) { - res = !strcasecmp(tgt, pleaf->ptnstr); - } else { - res = !strcmp(tgt, pleaf->ptnstr); - } - break; - case C2_L_MCONTAINS: - if (pleaf->match_ignorecase) { - res = strcasestr(tgt, pleaf->ptnstr); - } else { - res = strstr(tgt, pleaf->ptnstr); - } - break; - case C2_L_MSTART: - if (pleaf->match_ignorecase) { - res = !strncasecmp(tgt, pleaf->ptnstr, - strlen(pleaf->ptnstr)); - } else { - res = !strncmp(tgt, pleaf->ptnstr, - strlen(pleaf->ptnstr)); - } - break; - case C2_L_MWILDCARD: { - int flags = 0; - if (pleaf->match_ignorecase) { - flags |= FNM_CASEFOLD; - } - res = !fnmatch(pleaf->ptnstr, tgt, flags); - } break; - case C2_L_MPCRE: -#ifdef CONFIG_REGEX_PCRE - assert(strlen(tgt) <= INT_MAX); - assert(pleaf->regex_pcre); - res = (pcre2_match(pleaf->regex_pcre, (PCRE2_SPTR)tgt, - strlen(tgt), 0, 0, - pleaf->regex_pcre_match, NULL) > 0); -#else - assert(0); -#endif - break; - } - break; - default: *perr = true; assert(0); - } - if (res) { - break; - } - } - *pres = res; - - fail_str: - // Free the string after usage, if necessary - if (targets_free_inner) { - for (size_t i = 0; i < ntargets; ++i) { - if (targets_free_inner[i]) { - free((void *)targets_free_inner[i]); - } - } - } - // Free property values after usage, if necessary - if (targets_free) { - free(targets_free); - } - } break; + case C2_L_PTSTRING: return c2_match_once_leaf_string(ps, w, leaf); default: unreachable(); } } @@ -1715,18 +1711,15 @@ static inline void c2_match_once_leaf(session_t *ps, const struct managed_win *w */ static bool c2_match_once(session_t *ps, const struct managed_win *w, const c2_ptr_t cond) { bool result = false; - bool error = true; - // Handle a branch if (cond.isbranch) { + // Handle a branch (and/or/xor operation) const c2_b_t *pb = cond.b; if (!pb) { return false; } - error = false; - switch (pb->op) { case C2_B_OAND: result = (c2_match_once(ps, w, pb->opr1) && @@ -1740,7 +1733,7 @@ static bool c2_match_once(session_t *ps, const struct managed_win *w, const c2_p result = (c2_match_once(ps, w, pb->opr1) != c2_match_once(ps, w, pb->opr2)); break; - default: error = true; assert(0); + default: unreachable(); } log_debug("(%#010x): branch: result = %d, pattern = %s", w->base.id, @@ -1753,13 +1746,7 @@ static bool c2_match_once(session_t *ps, const struct managed_win *w, const c2_p return false; } - c2_match_once_leaf(ps, w, pleaf, &result, &error); - - // For EXISTS operator, no errors are fatal - if (C2_L_OEXISTS == pleaf->op && error) { - result = false; - error = false; - } + result = c2_match_once_leaf(ps, w, pleaf); log_debug("(%#010x): leaf: result = %d, client = %#010x, " "pattern = %s", @@ -1767,10 +1754,6 @@ static bool c2_match_once(session_t *ps, const struct managed_win *w, const c2_p } // Postprocess the result - if (error) { - result = false; - } - if (cond.isbranch ? cond.b->neg : cond.l->neg) { result = !result; } @@ -1801,8 +1784,9 @@ bool c2_match(session_t *ps, const struct managed_win *w, const c2_lptr_t *condl return false; } -/// Iterate over all conditions in a condition linked list. Call the callback for each of -/// the conditions. If the callback returns true, the iteration stops early. +/// Iterate over all conditions in a condition linked list. Call the callback for +/// each of the conditions. If the callback returns true, the iteration stops +/// early. /// /// Returns whether the iteration was stopped early. bool c2_list_foreach(const c2_lptr_t *condlist, c2_list_foreach_cb_t cb, void *data) { From 5159f4da378826b4ed044567f6ce02d9bafa16a9 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Fri, 16 Feb 2024 01:03:24 +0000 Subject: [PATCH 2/9] atom: add two-way mapping between atom and its name Signed-off-by: Yuxuan Shui --- src/atom.c | 55 +++++++++++++++++++++++++++++++++++++++++++++++++----- src/atom.h | 3 +++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/atom.c b/src/atom.c index 202a2195..f4883982 100644 --- a/src/atom.c +++ b/src/atom.c @@ -1,4 +1,5 @@ #include +#include #include #include "atom.h" @@ -10,12 +11,14 @@ struct atom_entry { struct cache_handle entry; + UT_hash_handle hh; xcb_atom_t atom; }; -static inline int atom_getter(struct cache *cache attr_unused, const char *atom_name, - size_t keylen, struct cache_handle **value, void *user_data) { +static inline int atom_getter(struct cache *cache, const char *atom_name, size_t keylen, + struct cache_handle **value, void *user_data) { xcb_connection_t *c = user_data; + auto atoms = container_of(cache, struct atom, c); xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply( c, xcb_intern_atom(c, 0, to_u16_checked(keylen), atom_name), NULL); @@ -31,13 +34,25 @@ static inline int atom_getter(struct cache *cache attr_unused, const char *atom_ struct atom_entry *entry = ccalloc(1, struct atom_entry); entry->atom = atom; + HASH_ADD_INT(atoms->atom_to_name, atom, entry); *value = &entry->entry; return 0; } -static inline void -atom_entry_free(struct cache *cache attr_unused, struct cache_handle *handle) { - struct atom_entry *entry = cache_entry(handle, struct atom_entry, entry); +static inline int +known_atom_getter(struct cache *cache attr_unused, const char *atom_name attr_unused, + size_t keylen attr_unused, struct cache_handle **value, void *user_data) { + auto atom = *(xcb_atom_t *)user_data; + struct atom_entry *entry = ccalloc(1, struct atom_entry); + entry->atom = atom; + *value = &entry->entry; + return 0; +} + +static inline void atom_entry_free(struct cache *cache, struct cache_handle *handle) { + auto entry = cache_entry(handle, struct atom_entry, entry); + auto atoms = container_of(cache, struct atom, c); + HASH_DEL(atoms->atom_to_name, entry); free(entry); } @@ -54,6 +69,35 @@ xcb_atom_t get_atom_cached(struct atom *a, const char *key, size_t keylen) { return cache_entry(cache_get(&a->c, key, keylen), struct atom_entry, entry)->atom; } +const char *get_atom_name(struct atom *a, xcb_atom_t atom, xcb_connection_t *c) { + struct atom_entry *entry = NULL; + HASH_FIND(hh, a->atom_to_name, &atom, sizeof(xcb_atom_t), entry); + if (!entry) { + auto r = xcb_get_atom_name_reply(c, xcb_get_atom_name(c, atom), NULL); + if (!r) { + log_error("Failed to get atom name"); + return NULL; + } + char *atom_name = xcb_get_atom_name_name(r); + auto len = (size_t)xcb_get_atom_name_name_length(r); + struct cache_handle *handle = NULL; + cache_get_or_fetch(&a->c, atom_name, len, &handle, &atom, known_atom_getter); + entry = cache_entry(handle, struct atom_entry, entry); + HASH_ADD_INT(a->atom_to_name, atom, entry); + free(r); + } + return entry->entry.key; +} + +const char *get_atom_name_cached(struct atom *a, xcb_atom_t atom) { + struct atom_entry *entry = NULL; + HASH_FIND(hh, a->atom_to_name, &atom, sizeof(xcb_atom_t), entry); + if (!entry) { + return NULL; + } + return entry->entry.key; +} + /** * Create a new atom structure and fetch all predefined atoms */ @@ -69,5 +113,6 @@ struct atom *init_atoms(xcb_connection_t *c) { void destroy_atoms(struct atom *a) { cache_invalidate_all(&a->c, atom_entry_free); + assert(a->atom_to_name == NULL); free(a); } diff --git a/src/atom.h b/src/atom.h index a0473914..dea4155f 100644 --- a/src/atom.h +++ b/src/atom.h @@ -54,6 +54,7 @@ struct atom_entry; struct atom { struct cache c; + struct atom_entry *atom_to_name; LIST_APPLY(ATOM_DEF, SEP_COLON, ATOM_LIST1); LIST_APPLY(ATOM_DEF, SEP_COLON, ATOM_LIST2); }; @@ -71,5 +72,7 @@ xcb_atom_t get_atom_cached(struct atom *a, const char *key, size_t keylen); static inline xcb_atom_t get_atom_cached_with_nul(struct atom *a, const char *key) { return get_atom_cached(a, key, strlen(key)); } +const char *get_atom_name(struct atom *a, xcb_atom_t, xcb_connection_t *c); +const char *get_atom_name_cached(struct atom *a, xcb_atom_t atom); void destroy_atoms(struct atom *a); From f2626cc930234f1293a9e2413af34fb98b3dddeb Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Fri, 16 Feb 2024 12:41:16 +0000 Subject: [PATCH 3/9] c2: store tracked property values per-window Store properties used in c2 conditions in the window state, so we don't need to re-query them every time we evaluate the rules. First step to remove the dependency on X connection from c2_match. Signed-off-by: Yuxuan Shui --- src/c2.c | 261 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/c2.h | 14 +++ src/event.c | 4 + src/win.c | 3 + src/win.h | 2 + 5 files changed, 284 insertions(+) diff --git a/src/c2.c b/src/c2.c index bba2e528..38378fca 100644 --- a/src/c2.c +++ b/src/c2.c @@ -13,6 +13,8 @@ #include #include #include +#include +#include #include #include #include @@ -67,11 +69,43 @@ static_assert(sizeof(struct c2_tracked_property_key) == 8, "Padding bytes in " struct c2_tracked_property { UT_hash_handle hh; struct c2_tracked_property_key key; + unsigned int id; + /// Highest indices of this property that + /// are tracked. -1 mean all indices are tracked. + int max_indices; }; struct c2_state { struct c2_tracked_property *tracked_properties; struct atom *atoms; + xcb_get_property_cookie_t *cookies; + uint32_t *propert_lengths; +}; + +// TODO(yshui) this has some overlap with winprop_t, consider merging them. +struct c2_property_value { + union { + struct { + char *string; + }; + struct { + int64_t numbers[4]; + }; + struct { + int64_t *array; + unsigned int capacity; + }; + }; + /// Number of items if the property is a number type, + /// or number of bytes in the string if the property is a string type. + uint32_t length; + enum { + C2_PROPERTY_TYPE_STRING, + C2_PROPERTY_TYPE_NUMBER, + C2_PROPERTY_TYPE_ATOM, + } type; + bool valid; + bool needs_update; }; /// Initializer for c2_ptr_t. @@ -1121,8 +1155,14 @@ static bool c2_l_postprocess(struct c2_state *state, xcb_connection_t *c, c2_l_t if (property == NULL) { property = cmalloc(struct c2_tracked_property); property->key = key; + property->id = HASH_COUNT(state->tracked_properties); HASH_ADD_KEYPTR(hh, state->tracked_properties, &property->key, sizeof(property->key), property); + property->max_indices = pleaf->index; + } else if (pleaf->index == -1) { + property->max_indices = -1; + } else if (property->max_indices >= 0 && pleaf->index > property->max_indices) { + property->max_indices = pleaf->index; } } @@ -1815,9 +1855,230 @@ void c2_state_free(struct c2_state *state) { HASH_DEL(state->tracked_properties, property); free(property); } + free(state->propert_lengths); + free(state->cookies); free(state); } +void c2_window_state_init(const struct c2_state *state, struct c2_window_state *window_state) { + auto property_count = HASH_COUNT(state->tracked_properties); + window_state->values = ccalloc(property_count, struct c2_property_value); + for (size_t i = 0; i < property_count; i++) { + window_state->values[i].needs_update = true; + window_state->values[i].valid = false; + } +} + +void c2_window_state_destroy(const struct c2_state *state, + struct c2_window_state *window_state) { + size_t property_count = HASH_COUNT(state->tracked_properties); + for (size_t i = 0; i < property_count; i++) { + auto values = &window_state->values[i]; + if (values->type == C2_PROPERTY_TYPE_STRING) { + free(window_state->values[i].string); + } else if (values->length > ARR_SIZE(values->numbers)) { + free(window_state->values[i].array); + } + } + free(window_state->values); +} + +void c2_window_state_mark_dirty(const struct c2_state *state, + struct c2_window_state *window_state, xcb_atom_t property, + bool is_on_frame) { + struct c2_tracked_property *p; + struct c2_tracked_property_key key = { + .property = property, + .is_on_frame = is_on_frame, + }; + HASH_FIND(hh, state->tracked_properties, &key, sizeof(key), p); + if (p) { + window_state->values[p->id].needs_update = true; + } +} + +static void +c2_window_state_update_one_from_reply(struct c2_state *state, + struct c2_property_value *value, xcb_atom_t property, + xcb_get_property_reply_t *reply, xcb_connection_t *c) { + auto len = to_u32_checked(xcb_get_property_value_length(reply)); + void *data = xcb_get_property_value(reply); + bool property_is_string = x_is_type_string(state->atoms, reply->type); + value->needs_update = false; + value->valid = false; + if (reply->type == XCB_ATOM_NONE) { + // Property doesn't exist on this window + log_verbose("Property %s doesn't exist on this window", + get_atom_name_cached(state->atoms, property)); + return; + } + if ((property_is_string && reply->format != 8) || + (reply->format != 8 && reply->format != 16 && reply->format != 32)) { + log_error("Invalid property type and format combination: property %s, " + "type: %s, format: %d.", + get_atom_name_cached(state->atoms, property), + get_atom_name_cached(state->atoms, reply->type), reply->format); + return; + } + + log_verbose("Updating property %s, length = %u, format = %d", + get_atom_name_cached(state->atoms, property), len, reply->format); + value->valid = true; + if (len == 0) { + value->length = 0; + return; + } + if (property_is_string) { + bool nul_terminated = ((char *)data)[len - 1] == '\0'; + value->length = len; + value->type = C2_PROPERTY_TYPE_STRING; + if (!nul_terminated) { + value->length += 1; + } + value->string = crealloc(value->string, value->length); + memcpy(value->string, data, len); + if (!nul_terminated) { + value->string[len] = '\0'; + } + } else { + size_t step = reply->format / 8; + bool is_signed = reply->type == XCB_ATOM_INTEGER; + value->length = len * 8 / reply->format; + if (reply->type == XCB_ATOM_ATOM) { + value->type = C2_PROPERTY_TYPE_ATOM; + } else { + value->type = C2_PROPERTY_TYPE_NUMBER; + } + + int64_t *storage = value->numbers; + if (value->length > ARR_SIZE(value->numbers)) { + if (value->capacity < value->length) { + value->array = crealloc(value->array, value->length); + value->capacity = value->length; + } + storage = value->array; + } + for (uint32_t i = 0; i < value->length; i++) { + auto item = (char *)data + i * step; + if (is_signed) { + switch (reply->format) { + case 8: storage[i] = *(int8_t *)item; break; + case 16: storage[i] = *(int16_t *)item; break; + case 32: storage[i] = *(int32_t *)item; break; + default: unreachable(); + } + } else { + switch (reply->format) { + case 8: storage[i] = *(uint8_t *)item; break; + case 16: storage[i] = *(uint16_t *)item; break; + case 32: storage[i] = *(uint32_t *)item; break; + default: unreachable(); + } + } + log_verbose("Property %s[%d] = %" PRId64, + get_atom_name_cached(state->atoms, property), i, + storage[i]); + if (reply->type == XCB_ATOM_ATOM) { + // Prefetch the atom name so it will be available + // during `c2_match`. We don't need the return value here. + get_atom_name(state->atoms, (xcb_atom_t)storage[i], c); + } + } + } +} + +static void c2_window_state_update_from_replies(struct c2_state *state, + struct c2_window_state *window_state, + xcb_connection_t *c, xcb_window_t client_win, + xcb_window_t frame_win, bool refetch) { + HASH_ITER2(state->tracked_properties, p) { + if (!window_state->values[p->id].needs_update) { + continue; + } + xcb_window_t window = p->key.is_on_frame ? frame_win : client_win; + xcb_get_property_reply_t *reply = + xcb_get_property_reply(c, state->cookies[p->id], NULL); + if (!reply) { + log_warn("Failed to get property %d for window %#010x, some " + "window rules might not work.", + p->id, window); + window_state->values[p->id].valid = false; + window_state->values[p->id].needs_update = false; + continue; + } + bool property_is_string = x_is_type_string(state->atoms, reply->type); + if (reply->bytes_after > 0 && (property_is_string || p->max_indices < 0)) { + if (!refetch) { + log_warn("Did property %d for window %#010x change while " + "we were fetching it? some window rules might " + "not work.", + p->id, window); + window_state->values[p->id].valid = false; + window_state->values[p->id].needs_update = false; + } else { + state->propert_lengths[p->id] += reply->bytes_after; + state->cookies[p->id] = xcb_get_property( + c, 0, window, p->key.property, XCB_GET_PROPERTY_TYPE_ANY, + 0, (state->propert_lengths[p->id] + 3) / 4); + } + } else { + c2_window_state_update_one_from_reply( + state, &window_state->values[p->id], p->key.property, reply, c); + } + free(reply); + } +} + +void c2_window_state_update(struct c2_state *state, struct c2_window_state *window_state, + xcb_connection_t *c, xcb_window_t client_win, + xcb_window_t frame_win) { + size_t property_count = HASH_COUNT(state->tracked_properties); + if (!state->cookies) { + state->cookies = ccalloc(property_count, xcb_get_property_cookie_t); + } + if (!state->propert_lengths) { + state->propert_lengths = ccalloc(property_count, uint32_t); + } + memset(state->cookies, 0, property_count * sizeof(xcb_get_property_cookie_t)); + + log_verbose("Updating c2 window state for window %#010x (frame %#010x)", + client_win, frame_win); + + // Because we don't know the length of all properties (i.e. if they are string + // properties, or for properties matched with `[*]`). We do this in 3 steps: + // 1. Send requests to all properties we need. Use `max_indices` to determine + // the length, or use 0 if it's unknown. + // 2. From the replies to (1), for properties we know the length of, we update + // the values. For those we don't, use the length information from the + // replies to send a new request with the correct length. + // 3. Update the rest of the properties. + + // Step 1 + HASH_ITER2(state->tracked_properties, p) { + if (!window_state->values[p->id].needs_update) { + continue; + } + uint32_t length = 0; + if (p->max_indices >= 0) { + // length is in 4 bytes units + length = (uint32_t)p->max_indices + 1; + } + + xcb_window_t window = p->key.is_on_frame ? frame_win : client_win; + // xcb_get_property long_length is in units of 4-byte, + // so use `ceil(length / 4)`. same below. + state->cookies[p->id] = xcb_get_property( + c, 0, window, p->key.property, XCB_GET_PROPERTY_TYPE_ANY, 0, length); + state->propert_lengths[p->id] = length * 4; + } + + // Step 2 + c2_window_state_update_from_replies(state, window_state, c, client_win, frame_win, true); + // Step 3 + c2_window_state_update_from_replies(state, window_state, c, client_win, frame_win, + false); +} + bool c2_state_is_property_tracked(struct c2_state *state, xcb_atom_t property) { struct c2_tracked_property *p; struct c2_tracked_property_key key = { diff --git a/src/c2.h b/src/c2.h index dc75ec6a..f73848dd 100644 --- a/src/c2.h +++ b/src/c2.h @@ -18,6 +18,12 @@ typedef struct _c2_lptr c2_lptr_t; typedef struct session session_t; struct c2_state; +/// Per-window state used for c2 condition matching. +struct c2_window_state { + /// An array of window properties. Exact how many + /// properties there are is stored inside `struct c2_state`. + struct c2_property_value *values; +}; struct atom; struct managed_win; @@ -34,6 +40,14 @@ struct c2_state *c2_state_new(struct atom *atoms); void c2_state_free(struct c2_state *state); /// Returns true if value of the property is used in any condition. bool c2_state_is_property_tracked(struct c2_state *state, xcb_atom_t property); +void c2_window_state_init(const struct c2_state *state, struct c2_window_state *window_state); +void c2_window_state_destroy(const struct c2_state *state, struct c2_window_state *window_state); +void c2_window_state_mark_dirty(const struct c2_state *state, + struct c2_window_state *window_state, xcb_atom_t property, + bool is_on_frame); +void c2_window_state_update(struct c2_state *state, struct c2_window_state *window_state, + xcb_connection_t *c, xcb_window_t client_win, + xcb_window_t frame_win); bool c2_match(session_t *ps, const struct managed_win *w, const c2_lptr_t *condlst, void **pdata); diff --git a/src/event.c b/src/event.c index bbb5c276..9734c88e 100644 --- a/src/event.c +++ b/src/event.c @@ -575,11 +575,15 @@ static inline void ev_property_notify(session_t *ps, xcb_property_notify_event_t // Check for other atoms we are tracking if (c2_state_is_property_tracked(ps->c2_state, ev->atom)) { + bool change_is_on_frame = true; auto w = find_managed_win(ps, ev->window); if (!w) { w = find_toplevel(ps, ev->window); + change_is_on_frame = false; } if (w) { + c2_window_state_mark_dirty(ps->c2_state, &w->c2_state, ev->atom, + change_is_on_frame); // Set FACTOR_CHANGED so rules based on properties will be // re-evaluated. // Don't need to set property stale here, since that only diff --git a/src/win.c b/src/win.c index e79cce5d..4b0ff03a 100644 --- a/src/win.c +++ b/src/win.c @@ -1220,6 +1220,7 @@ void win_update_opacity_rule(session_t *ps, struct managed_win *w) { */ void win_on_factor_change(session_t *ps, struct managed_win *w) { log_debug("Window %#010x (%s) factor change", w->base.id, w->name); + c2_window_state_update(ps->c2_state, &w->c2_state, ps->c.c, w->client_win, w->base.id); // Focus and is_fullscreen needs to be updated first, as other rules might depend // on the focused state of the window win_update_focused(ps, w); @@ -1472,6 +1473,7 @@ void free_win_res(session_t *ps, struct managed_win *w) { free(w->stale_props); w->stale_props = NULL; w->stale_props_capacity = 0; + c2_window_state_destroy(ps->c2_state, &w->c2_state); } /// Insert a new window after list_node `prev` @@ -1729,6 +1731,7 @@ struct win *fill_win(session_t *ps, struct win *w) { ps->atoms->a_NET_WM_STATE, }; win_set_properties_stale(new, init_stale_props, ARR_SIZE(init_stale_props)); + c2_window_state_init(ps->c2_state, &new->c2_state); #ifdef CONFIG_DBUS // Send D-Bus signal diff --git a/src/win.h b/src/win.h index bd328806..82b2e9bd 100644 --- a/src/win.h +++ b/src/win.h @@ -284,6 +284,8 @@ struct managed_win { /// The custom window shader to use when rendering. struct shader_info *fg_shader; + struct c2_window_state c2_state; + #ifdef CONFIG_OPENGL /// Textures and FBO background blur use. glx_blur_cache_t glx_blur_cache; From 06134406450744434b9fb8f03ca4cfed72768aae Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Fri, 16 Feb 2024 15:19:36 +0000 Subject: [PATCH 4/9] c2: match using stored window properties And removing the session_t parameter from c2_match. As a side-effect, rules with mismatched types will stop working from this commit onward. As was warned about in a previous commit. Signed-off-by: Yuxuan Shui --- src/c2.c | 323 +++++++++++++++++++++++------------------------------- src/c2.h | 4 +- src/win.c | 29 ++--- 3 files changed, 153 insertions(+), 203 deletions(-) diff --git a/src/c2.c b/src/c2.c index 38378fca..30e62117 100644 --- a/src/c2.c +++ b/src/c2.c @@ -154,6 +154,7 @@ struct _c2_l { } match : 3; bool match_ignorecase : 1; char *tgt; + unsigned int target_id; xcb_atom_t tgtatom; bool tgt_onframe; int index; @@ -252,26 +253,6 @@ static const char *C2_PREDEFS[] = { [C2_L_PROLE] = "role", }; -/** - * Get the numeric property value from a win_prop_t. - */ -static inline long winprop_get_int(winprop_t prop, size_t index) { - long tgt = 0; - - if (!prop.nitems || index >= prop.nitems) { - return 0; - } - - switch (prop.format) { - case 8: tgt = *(prop.p8 + index); break; - case 16: tgt = *(prop.p16 + index); break; - case 32: tgt = *(prop.p32 + index); break; - default: assert(0); break; - } - - return tgt; -} - /** * Compare next word in a string with another string. */ @@ -374,8 +355,6 @@ static inline void c2_freep(c2_ptr_t *pp) { static const char *c2h_dump_str_tgt(const c2_l_t *pleaf); -static bool c2_match_once(session_t *ps, const struct managed_win *w, c2_ptr_t cond); - /** * Parse a condition string. */ @@ -1164,6 +1143,7 @@ static bool c2_l_postprocess(struct c2_state *state, xcb_connection_t *c, c2_l_t } else if (property->max_indices >= 0 && pleaf->index > property->max_indices) { property->max_indices = pleaf->index; } + pleaf->target_id = property->id; } // Warn about lower case characters in target name @@ -1197,9 +1177,7 @@ static bool c2_l_postprocess(struct c2_state *state, xcb_connection_t *c, c2_l_t PCRE2_UCHAR buffer[256]; pcre2_get_error_message(errorcode, buffer, sizeof(buffer)); log_error("Pattern \"%s\": PCRE regular expression " - "parsing " - "failed on " - "offset %zu: %s", + "parsing failed on offset %zu: %s", pleaf->ptnstr, erroffset, buffer); return false; } @@ -1302,7 +1280,7 @@ static const char *c2h_dump_str_tgt(const c2_l_t *pleaf) { * the null terminator. * null terminator will not be written to the output. */ -static size_t c2_condition_to_str(c2_ptr_t p, char *output, size_t len) { +static size_t c2_condition_to_str(const c2_ptr_t p, char *output, size_t len) { #define push_char(c) \ if (offset < len) \ output[offset] = (c); \ @@ -1469,17 +1447,44 @@ static const char *c2_condition_to_str2(c2_ptr_t ptr) { return buf; } -static bool -c2_match_once_leaf_int(session_t *ps, const struct managed_win *w, const c2_l_t *leaf) { - long long *targets = NULL; - long long *targets_free = NULL; - size_t ntargets = 0; +/// Get the list of target number values from a struct c2_property_value +static inline const int64_t * +c2_values_get_number_targets(const struct c2_property_value *values, int index, size_t *n) { + auto storage = values->numbers; + if (values->length > ARR_SIZE(values->numbers)) { + storage = values->array; + } + if (index < 0) { + *n = values->length; + return storage; + } + if ((size_t)index < values->length) { + *n = 1; + return &storage[index]; + } + *n = 0; + return NULL; +} + +static inline bool c2_int_op(const c2_l_t *leaf, int64_t target) { + switch (leaf->op) { + case C2_L_OEXISTS: return leaf->predef != C2_L_PUNDEFINED ? target : true; + case C2_L_OEQ: return target == leaf->ptnint; + case C2_L_OGT: return target > leaf->ptnint; + case C2_L_OGTEQ: return target >= leaf->ptnint; + case C2_L_OLT: return target < leaf->ptnint; + case C2_L_OLTEQ: return target <= leaf->ptnint; + } + unreachable(); +} + +static bool c2_match_once_leaf_int(const struct managed_win *w, const c2_l_t *leaf) { const xcb_window_t wid = (leaf->tgt_onframe ? w->client_win : w->base.id); // Get the value - // A predefined target - long long predef_target = 0; if (leaf->predef != C2_L_PUNDEFINED) { + // A predefined target + int64_t predef_target = 0; switch (leaf->predef) { case C2_L_PID: predef_target = wid; break; case C2_L_PX: predef_target = w->g.x; break; @@ -1502,57 +1507,31 @@ c2_match_once_leaf_int(session_t *ps, const struct managed_win *w, const c2_l_t case C2_L_PLEADER: predef_target = w->leader; break; default: unreachable(); } - ntargets = 1; - targets = &predef_target; - } else { - // A raw window property - int word_count = 1; - int offset = leaf->index; - if (leaf->index < 0) { - // index < 0 means match any index - // Get length of property in 32-bit multiples - auto prop_info = x_get_prop_info(&ps->c, wid, leaf->tgtatom); - word_count = to_int_checked((prop_info.length + 4 - 1) / 4); - offset = 0; - } - winprop_t prop = x_get_prop_with_offset(&ps->c, wid, leaf->tgtatom, - offset, word_count, 0, 0); - - ntargets = (leaf->index < 0 ? prop.nitems : min2(prop.nitems, 1)); - if (ntargets > 0) { - targets = targets_free = ccalloc(ntargets, long long); - for (size_t i = 0; i < ntargets; ++i) { - targets[i] = winprop_get_int(prop, i); - } - } - free_winprop(&prop); + return c2_int_op(leaf, predef_target); } - // Do comparison - bool matches = false; - for (size_t i = 0; i < ntargets; ++i) { - long long tgt = targets[i]; - switch (leaf->op) { - case C2_L_OEXISTS: - matches = (leaf->predef != C2_L_PUNDEFINED ? tgt : true); - break; - case C2_L_OEQ: matches = (tgt == leaf->ptnint); break; - case C2_L_OGT: matches = (tgt > leaf->ptnint); break; - case C2_L_OGTEQ: matches = (tgt >= leaf->ptnint); break; - case C2_L_OLT: matches = (tgt < leaf->ptnint); break; - case C2_L_OLTEQ: matches = (tgt <= leaf->ptnint); break; - default: unreachable(); - } - if (matches) { - break; - } + // A raw window property + auto values = &w->c2_state.values[leaf->target_id]; + assert(!values->needs_update); + if (!values->valid) { + log_verbose("Property %s not found on window %#010x (%s)", leaf->tgt, + w->client_win, w->name); + return false; } - // Free property values after usage, if necessary - if (targets_free) { - free(targets_free); + if (values->type == C2_PROPERTY_TYPE_STRING) { + log_error("Property %s is not an integer", leaf->tgt); + return false; } - return matches; + + size_t ntargets = 0; + auto targets = c2_values_get_number_targets(values, leaf->index, &ntargets); + for (size_t i = 0; i < ntargets; i++) { + if (c2_int_op(leaf, targets[i])) { + return true; + } + } + return false; } static bool c2_string_op(const c2_l_t *leaf, const char *target) { @@ -1598,19 +1577,8 @@ static bool c2_string_op(const c2_l_t *leaf, const char *target) { unreachable(); } -static bool -c2_match_once_leaf_string(session_t *ps, const struct managed_win *w, const c2_l_t *leaf) { - - const char **targets = NULL; - const char **targets_free = NULL; - const char **targets_free_inner = NULL; - size_t ntargets = 0; - const xcb_window_t wid = (leaf->tgt_onframe ? w->client_win : w->base.id); - - winprop_info_t prop_info = {0}; - if (leaf->predef == C2_L_PUNDEFINED) { - prop_info = x_get_prop_info(&ps->c, wid, leaf->tgtatom); - } +static bool c2_match_once_leaf_string(struct atom *atoms, const struct managed_win *w, + const c2_l_t *leaf) { // A predefined target const char *predef_target = NULL; @@ -1623,90 +1591,64 @@ c2_match_once_leaf_string(session_t *ps, const struct managed_win *w, const c2_l case C2_L_PROLE: predef_target = w->role; break; default: unreachable(); } - ntargets = 1; - targets = &predef_target; - } else if (prop_info.type == XCB_ATOM_ATOM) { - // An atom type property, convert it to string - int word_count = 1; - int offset = leaf->index; - if (leaf->index < 0) { - // index < 0 means match any index - // Get length of property in 32-bit multiples - word_count = to_int_checked((prop_info.length + 4 - 1) / 4); - offset = 0; + if (!predef_target) { + return false; } - winprop_t prop = x_get_prop_with_offset( - &ps->c, wid, leaf->tgtatom, offset, word_count, XCB_ATOM_ATOM, 0); + log_verbose("Matching against predefined target %s", predef_target); + return c2_string_op(leaf, predef_target); + } - ntargets = (leaf->index < 0 ? prop.nitems : min2(prop.nitems, 1)); - targets = targets_free = (const char **)ccalloc(2 * ntargets, char *); - targets_free_inner = targets + ntargets; + auto values = &w->c2_state.values[leaf->target_id]; + assert(!values->needs_update); + if (!values->valid) { + log_verbose("Property %s not found on window %#010x (%s)", leaf->tgt, + w->client_win, w->name); + return false; + } + + if (values->type == C2_PROPERTY_TYPE_ATOM) { + size_t ntargets = 0; + auto targets = c2_values_get_number_targets(values, leaf->index, &ntargets); for (size_t i = 0; i < ntargets; ++i) { - xcb_atom_t atom = (xcb_atom_t)winprop_get_int(prop, i); - if (atom) { - xcb_get_atom_name_reply_t *reply = xcb_get_atom_name_reply( - ps->c.c, xcb_get_atom_name(ps->c.c, atom), NULL); - if (reply) { - targets[i] = targets_free_inner[i] = strndup( - xcb_get_atom_name_name(reply), - (size_t)xcb_get_atom_name_name_length(reply)); - free(reply); - } + auto atom = (xcb_atom_t)targets[i]; + const char *atom_name = get_atom_name_cached(atoms, atom); + log_verbose("(%zu/%zu) Atom %u is %s", i, ntargets, atom, atom_name); + assert(atom_name != NULL); + if (atom_name && c2_string_op(leaf, atom_name)) { + return true; } } - free_winprop(&prop); - } else { - // Not an atom type, just fetch the string list - char **strlst = NULL; - int nstr = 0; - int index = max2(0, leaf->index); - if (wid_get_text_prop(&ps->c, ps->atoms, wid, leaf->tgtatom, &strlst, &nstr)) { - if (leaf->index < 0 && nstr > 0 && strlen(strlst[0]) > 0) { - ntargets = to_u32_checked(nstr); - targets = (const char **)strlst; - } else if (nstr > index) { - ntargets = 1; - targets = (const char **)strlst + index; + return false; + } + + if (values->type != C2_PROPERTY_TYPE_STRING) { + log_verbose("Property %s is not a string", leaf->tgt); + return false; + } + + // Not an atom type, value is a list of nul separated strings + if (leaf->index < 0) { + size_t offset = 0; + while (offset < values->length) { + if (c2_string_op(leaf, values->string + offset)) { + return true; } + offset += strlen(values->string + offset) + 1; } - if (strlst) { - targets_free = (const char **)strlst; - } + return false; } - - bool matches = false; - if (ntargets == 0) { - goto fail; + size_t offset = 0; + int index = leaf->index; + while (offset < values->length && index != 0) { + offset += strlen(values->string + offset) + 1; + index -= 1; } - for (size_t i = 0; i < ntargets; ++i) { - if (!targets[i]) { - goto fail; - } + if (index != 0 || values->length == 0) { + // index is out of bounds + return false; } - - // Actual matching - for (size_t i = 0; i < ntargets; ++i) { - if (c2_string_op(leaf, targets[i])) { - matches = true; - break; - } - } - -fail: - // Free the string after usage, if necessary - if (targets_free_inner) { - for (size_t i = 0; i < ntargets; ++i) { - if (targets_free_inner[i]) { - free((void *)targets_free_inner[i]); - } - } - } - // Free property values after usage, if necessary - if (targets_free) { - free(targets_free); - } - return matches; + return c2_string_op(leaf, values->string + offset); } /** @@ -1715,7 +1657,7 @@ fail: * For internal use. */ static inline bool -c2_match_once_leaf(session_t *ps, const struct managed_win *w, const c2_l_t *leaf) { +c2_match_once_leaf(struct c2_state *state, const struct managed_win *w, const c2_l_t *leaf) { assert(leaf); const xcb_window_t wid = (leaf->tgt_onframe ? w->client_win : w->base.id); @@ -1726,20 +1668,24 @@ c2_match_once_leaf(session_t *ps, const struct managed_win *w, const c2_l_t *lea return false; } - auto type = leaf->ptntype; - if (type == C2_L_PTUNDEFINED) { - auto prop_info = x_get_prop_info(&ps->c, wid, leaf->tgtatom); - if (x_is_type_string(ps->atoms, prop_info.type)) { - type = C2_L_PTSTRING; + log_verbose("Matching window %#010x (%s) against condition %s", wid, w->name, + c2_condition_to_str2((c2_ptr_t){.l = (c2_l_t *)leaf, .isbranch = false})); + + auto pattern_type = leaf->ptntype; + if (pattern_type == C2_L_PTUNDEFINED) { + auto values = &w->c2_state.values[leaf->target_id]; + if (values->type == C2_PROPERTY_TYPE_STRING) { + pattern_type = C2_L_PTSTRING; } else { - type = C2_L_PTINT; + pattern_type = C2_L_PTINT; } } - switch (type) { + + switch (pattern_type) { // Deal with integer patterns - case C2_L_PTINT: return c2_match_once_leaf_int(ps, w, leaf); + case C2_L_PTINT: return c2_match_once_leaf_int(w, leaf); // String patterns - case C2_L_PTSTRING: return c2_match_once_leaf_string(ps, w, leaf); + case C2_L_PTSTRING: return c2_match_once_leaf_string(state->atoms, w, leaf); default: unreachable(); } } @@ -1749,7 +1695,8 @@ c2_match_once_leaf(session_t *ps, const struct managed_win *w, const c2_l_t *lea * * @return true if matched, false otherwise. */ -static bool c2_match_once(session_t *ps, const struct managed_win *w, const c2_ptr_t cond) { +static bool +c2_match_once(struct c2_state *state, const struct managed_win *w, const c2_ptr_t cond) { bool result = false; if (cond.isbranch) { @@ -1760,18 +1707,21 @@ static bool c2_match_once(session_t *ps, const struct managed_win *w, const c2_p return false; } + log_verbose("Matching window %#010x (%s) against condition %s", + w->base.id, w->name, c2_condition_to_str2(cond)); + switch (pb->op) { case C2_B_OAND: - result = (c2_match_once(ps, w, pb->opr1) && - c2_match_once(ps, w, pb->opr2)); + result = (c2_match_once(state, w, pb->opr1) && + c2_match_once(state, w, pb->opr2)); break; case C2_B_OOR: - result = (c2_match_once(ps, w, pb->opr1) || - c2_match_once(ps, w, pb->opr2)); + result = (c2_match_once(state, w, pb->opr1) || + c2_match_once(state, w, pb->opr2)); break; case C2_B_OXOR: - result = (c2_match_once(ps, w, pb->opr1) != - c2_match_once(ps, w, pb->opr2)); + result = (c2_match_once(state, w, pb->opr1) != + c2_match_once(state, w, pb->opr2)); break; default: unreachable(); } @@ -1786,7 +1736,7 @@ static bool c2_match_once(session_t *ps, const struct managed_win *w, const c2_p return false; } - result = c2_match_once_leaf(ps, w, pleaf); + result = c2_match_once_leaf(state, w, pleaf); log_debug("(%#010x): leaf: result = %d, client = %#010x, " "pattern = %s", @@ -1808,12 +1758,11 @@ static bool c2_match_once(session_t *ps, const struct managed_win *w, const c2_p * @param pdata a place to return the data * @return true if matched, false otherwise. */ -bool c2_match(session_t *ps, const struct managed_win *w, const c2_lptr_t *condlst, - void **pdata) { - assert(ps->server_grabbed); +bool c2_match(struct c2_state *state, const struct managed_win *w, + const c2_lptr_t *condlst, void **pdata) { // Then go through the whole linked list for (; condlst; condlst = condlst->next) { - if (c2_match_once(ps, w, condlst->ptr)) { + if (c2_match_once(state, w, condlst->ptr)) { if (pdata) { *pdata = condlst->data; } diff --git a/src/c2.h b/src/c2.h index f73848dd..90a3dca1 100644 --- a/src/c2.h +++ b/src/c2.h @@ -49,8 +49,8 @@ void c2_window_state_update(struct c2_state *state, struct c2_window_state *wind xcb_connection_t *c, xcb_window_t client_win, xcb_window_t frame_win); -bool c2_match(session_t *ps, const struct managed_win *w, const c2_lptr_t *condlst, - void **pdata); +bool c2_match(struct c2_state *state, const struct managed_win *w, + const c2_lptr_t *condlst, void **pdata); bool c2_list_postprocess(struct c2_state *state, xcb_connection_t *c, c2_lptr_t *list); typedef bool (*c2_list_foreach_cb_t)(const c2_lptr_t *cond, void *data); diff --git a/src/win.c b/src/win.c index 4b0ff03a..f260cea9 100644 --- a/src/win.c +++ b/src/win.c @@ -150,7 +150,7 @@ static void win_update_focused(session_t *ps, struct managed_win *w) { (ps->o.mark_wmwin_focused && w->wmwin) || (ps->o.mark_ovredir_focused && w->base.id == w->client_win && !w->wmwin) || (w->a.map_state == XCB_MAP_STATE_VIEWABLE && - c2_match(ps, w, ps->o.focus_blacklist, NULL))) { + c2_match(ps->c2_state, w, ps->o.focus_blacklist, NULL))) { w->focused = true; } @@ -959,7 +959,7 @@ static void win_determine_shadow(session_t *ps, struct managed_win *w) { if (!ps->o.wintype_option[w->window_type].shadow) { log_debug("Shadow disabled by wintypes"); shadow_new = false; - } else if (c2_match(ps, w, ps->o.shadow_blacklist, NULL)) { + } else if (c2_match(ps->c2_state, w, ps->o.shadow_blacklist, NULL)) { log_debug("Shadow disabled by shadow-exclude"); shadow_new = false; } else if (ps->o.shadow_ignore_shaped && w->bounding_shaped && @@ -1015,7 +1015,7 @@ bool win_update_prop_fullscreen(struct x_connection *c, const struct atom *atoms static void win_determine_clip_shadow_above(session_t *ps, struct managed_win *w) { bool should_crop = (ps->o.wintype_option[w->window_type].clip_shadow_above || - c2_match(ps, w, ps->o.shadow_clip_list, NULL)); + c2_match(ps->c2_state, w, ps->o.shadow_clip_list, NULL)); w->clip_shadow_above = should_crop; } @@ -1038,7 +1038,7 @@ static void win_determine_invert_color(session_t *ps, struct managed_win *w) { if (UNSET != w->invert_color_force) { invert_color_new = w->invert_color_force; } else if (w->a.map_state == XCB_MAP_STATE_VIEWABLE) { - invert_color_new = c2_match(ps, w, ps->o.invert_color_list, NULL); + invert_color_new = c2_match(ps->c2_state, w, ps->o.invert_color_list, NULL); } win_set_invert_color(ps, w, invert_color_new); @@ -1127,7 +1127,7 @@ static void win_determine_blur_background(session_t *ps, struct managed_win *w) if (!ps->o.wintype_option[w->window_type].blur_background) { log_debug("Blur background disabled by wintypes"); blur_background_new = false; - } else if (c2_match(ps, w, ps->o.blur_background_blacklist, NULL)) { + } else if (c2_match(ps->c2_state, w, ps->o.blur_background_blacklist, NULL)) { log_debug("Blur background disabled by " "blur-background-exclude"); blur_background_new = false; @@ -1142,7 +1142,7 @@ static void win_determine_blur_background(session_t *ps, struct managed_win *w) */ static void win_determine_rounded_corners(session_t *ps, struct managed_win *w) { void *radius_override = NULL; - if (c2_match(ps, w, ps->o.corner_radius_rules, &radius_override)) { + if (c2_match(ps->c2_state, w, ps->o.corner_radius_rules, &radius_override)) { log_debug("Matched corner rule! %d", w->corner_radius); } @@ -1153,8 +1153,9 @@ static void win_determine_rounded_corners(session_t *ps, struct managed_win *w) // Don't round full screen windows & excluded windows, // unless we find a corner override in corner_radius_rules - if (!radius_override && ((w && w->is_fullscreen) || - c2_match(ps, w, ps->o.rounded_corners_blacklist, NULL))) { + if (!radius_override && + ((w && w->is_fullscreen) || + c2_match(ps->c2_state, w, ps->o.rounded_corners_blacklist, NULL))) { w->corner_radius = 0; log_debug("Not rounding corners for window %#010x", w->base.id); } else { @@ -1181,7 +1182,7 @@ static void win_determine_fg_shader(session_t *ps, struct managed_win *w) { auto shader_new = ps->o.window_shader_fg; void *val = NULL; - if (c2_match(ps, w, ps->o.window_shader_fg_rules, &val)) { + if (c2_match(ps->c2_state, w, ps->o.window_shader_fg_rules, &val)) { shader_new = val; } @@ -1204,7 +1205,7 @@ void win_update_opacity_rule(session_t *ps, struct managed_win *w) { double opacity = 1.0; bool is_set = false; void *val = NULL; - if (c2_match(ps, w, ps->o.opacity_rules, &val)) { + if (c2_match(ps->c2_state, w, ps->o.opacity_rules, &val)) { opacity = ((double)(long)val) / 100.0; is_set = true; } @@ -1236,17 +1237,17 @@ void win_on_factor_change(session_t *ps, struct managed_win *w) { log_debug("Window mode changed to %d", w->mode); win_update_opacity_rule(ps, w); if (w->a.map_state == XCB_MAP_STATE_VIEWABLE) { - w->paint_excluded = c2_match(ps, w, ps->o.paint_blacklist, NULL); + w->paint_excluded = c2_match(ps->c2_state, w, ps->o.paint_blacklist, NULL); } if (w->a.map_state == XCB_MAP_STATE_VIEWABLE) { w->unredir_if_possible_excluded = - c2_match(ps, w, ps->o.unredir_if_possible_blacklist, NULL); + c2_match(ps->c2_state, w, ps->o.unredir_if_possible_blacklist, NULL); } - w->fade_excluded = c2_match(ps, w, ps->o.fade_blacklist, NULL); + w->fade_excluded = c2_match(ps->c2_state, w, ps->o.fade_blacklist, NULL); w->transparent_clipping_excluded = - c2_match(ps, w, ps->o.transparent_clipping_blacklist, NULL); + c2_match(ps->c2_state, w, ps->o.transparent_clipping_blacklist, NULL); win_update_opacity_target(ps, w); From ad250341dbe626aec675a737d58a46cd45b52d87 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Fri, 16 Feb 2024 16:41:12 +0000 Subject: [PATCH 5/9] c2: add a TODO Signed-off-by: Yuxuan Shui --- src/c2.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/c2.c b/src/c2.c index 30e62117..468e1f12 100644 --- a/src/c2.c +++ b/src/c2.c @@ -158,6 +158,10 @@ struct _c2_l { xcb_atom_t tgtatom; bool tgt_onframe; int index; + // TODO(yshui) translate some of the pre-defined targets to + // generic window properties. e.g. `name = "xterm"` + // should be translated to: + // "WM_NAME = 'xterm' || _NET_WM_NAME = 'xterm'" enum { C2_L_PUNDEFINED = -1, C2_L_PID = 0, From c0e2e79293ef6e0793b54a92b05629ad0b25e580 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Sun, 18 Feb 2024 18:59:58 +0000 Subject: [PATCH 6/9] c2: add simple unit tests for c2_match Signed-off-by: Yuxuan Shui --- src/c2.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/c2.c b/src/c2.c index 468e1f12..dfb4c1d7 100644 --- a/src/c2.c +++ b/src/c2.c @@ -418,6 +418,14 @@ TEST_CASE(c2_parse) { size_t len = c2_condition_to_str(cond->ptr, str, sizeof(str)); TEST_STREQUAL3(str, "name = \"xterm\"", len); + + struct c2_state state = { + .atoms = NULL, + }; + struct managed_win test_win = { + .name = "xterm", + }; + TEST_TRUE(c2_match(&state, &test_win, cond, NULL)); c2_list_free(&cond, NULL); cond = c2_parse(NULL, "_GTK_FRAME_EXTENTS@:c", NULL); @@ -455,6 +463,10 @@ TEST_CASE(c2_parse) { len = c2_condition_to_str(cond->ptr, str, sizeof(str)); TEST_STREQUAL3(str, "(name = \"xterm\" && class_g *= \"XTerm\")", len); + test_win.class_general = "XTerm"; + TEST_TRUE(c2_match(&state, &test_win, cond, NULL)); + test_win.class_general = "asdf"; + TEST_TRUE(!c2_match(&state, &test_win, cond, NULL)); c2_list_free(&cond, NULL); cond = c2_parse(NULL, "_NET_WM_STATE[1]:32a *='_NET_WM_STATE_HIDDEN'", NULL); From ae88e83aa175908300edc0a765619e2f0261505a Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Sat, 17 Feb 2024 01:55:21 +0000 Subject: [PATCH 7/9] atom: make the internal implementations replaceable Signed-off-by: Yuxuan Shui --- src/atom.c | 52 +++++++++++++++++++++++++++++++++++----------------- src/atom.h | 2 -- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/atom.c b/src/atom.c index f4883982..cfc4938c 100644 --- a/src/atom.c +++ b/src/atom.c @@ -15,10 +15,17 @@ struct atom_entry { xcb_atom_t atom; }; +struct atom_impl { + struct atom base; + struct cache c; + struct atom_entry *atom_to_name; + cache_getter_t getter; +}; + static inline int atom_getter(struct cache *cache, const char *atom_name, size_t keylen, struct cache_handle **value, void *user_data) { xcb_connection_t *c = user_data; - auto atoms = container_of(cache, struct atom, c); + auto atoms = container_of(cache, struct atom_impl, c); xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply( c, xcb_intern_atom(c, 0, to_u16_checked(keylen), atom_name), NULL); @@ -51,14 +58,15 @@ known_atom_getter(struct cache *cache attr_unused, const char *atom_name attr_un static inline void atom_entry_free(struct cache *cache, struct cache_handle *handle) { auto entry = cache_entry(handle, struct atom_entry, entry); - auto atoms = container_of(cache, struct atom, c); + auto atoms = container_of(cache, struct atom_impl, c); HASH_DEL(atoms->atom_to_name, entry); free(entry); } xcb_atom_t get_atom(struct atom *a, const char *key, size_t keylen, xcb_connection_t *c) { struct cache_handle *entry = NULL; - if (cache_get_or_fetch(&a->c, key, keylen, &entry, c, atom_getter) < 0) { + auto atoms = container_of(a, struct atom_impl, base); + if (cache_get_or_fetch(&atoms->c, key, keylen, &entry, c, atoms->getter) < 0) { log_error("Failed to get atom %s", key); return XCB_NONE; } @@ -66,13 +74,16 @@ xcb_atom_t get_atom(struct atom *a, const char *key, size_t keylen, xcb_connecti } xcb_atom_t get_atom_cached(struct atom *a, const char *key, size_t keylen) { - return cache_entry(cache_get(&a->c, key, keylen), struct atom_entry, entry)->atom; + auto atoms = container_of(a, struct atom_impl, base); + return cache_entry(cache_get(&atoms->c, key, keylen), struct atom_entry, entry)->atom; } const char *get_atom_name(struct atom *a, xcb_atom_t atom, xcb_connection_t *c) { struct atom_entry *entry = NULL; - HASH_FIND(hh, a->atom_to_name, &atom, sizeof(xcb_atom_t), entry); + auto atoms = container_of(a, struct atom_impl, base); + HASH_FIND(hh, atoms->atom_to_name, &atom, sizeof(xcb_atom_t), entry); if (!entry) { + BUG_ON(c == NULL); auto r = xcb_get_atom_name_reply(c, xcb_get_atom_name(c, atom), NULL); if (!r) { log_error("Failed to get atom name"); @@ -81,9 +92,9 @@ const char *get_atom_name(struct atom *a, xcb_atom_t atom, xcb_connection_t *c) char *atom_name = xcb_get_atom_name_name(r); auto len = (size_t)xcb_get_atom_name_name_length(r); struct cache_handle *handle = NULL; - cache_get_or_fetch(&a->c, atom_name, len, &handle, &atom, known_atom_getter); + cache_get_or_fetch(&atoms->c, atom_name, len, &handle, &atom, known_atom_getter); entry = cache_entry(handle, struct atom_entry, entry); - HASH_ADD_INT(a->atom_to_name, atom, entry); + HASH_ADD_INT(atoms->atom_to_name, atom, entry); free(r); } return entry->entry.key; @@ -91,28 +102,35 @@ const char *get_atom_name(struct atom *a, xcb_atom_t atom, xcb_connection_t *c) const char *get_atom_name_cached(struct atom *a, xcb_atom_t atom) { struct atom_entry *entry = NULL; - HASH_FIND(hh, a->atom_to_name, &atom, sizeof(xcb_atom_t), entry); + auto atoms = container_of(a, struct atom_impl, base); + HASH_FIND(hh, atoms->atom_to_name, &atom, sizeof(xcb_atom_t), entry); if (!entry) { return NULL; } return entry->entry.key; } +static inline struct atom *init_atoms_impl(xcb_connection_t *c, cache_getter_t getter) { + auto atoms = ccalloc(1, struct atom_impl); + atoms->c = CACHE_INIT; + atoms->getter = getter; +#define ATOM_GET(x) atoms->base.a##x = get_atom(&atoms->base, #x, sizeof(#x) - 1, c) + LIST_APPLY(ATOM_GET, SEP_COLON, ATOM_LIST1); + LIST_APPLY(ATOM_GET, SEP_COLON, ATOM_LIST2); +#undef ATOM_GET + return &atoms->base; +} + /** * Create a new atom structure and fetch all predefined atoms */ struct atom *init_atoms(xcb_connection_t *c) { - auto atoms = ccalloc(1, struct atom); - atoms->c = CACHE_INIT; -#define ATOM_GET(x) atoms->a##x = get_atom(atoms, #x, sizeof(#x) - 1, c) - LIST_APPLY(ATOM_GET, SEP_COLON, ATOM_LIST1); - LIST_APPLY(ATOM_GET, SEP_COLON, ATOM_LIST2); -#undef ATOM_GET - return atoms; + return init_atoms_impl(c, atom_getter); } void destroy_atoms(struct atom *a) { - cache_invalidate_all(&a->c, atom_entry_free); - assert(a->atom_to_name == NULL); + auto atoms = container_of(a, struct atom_impl, base); + cache_invalidate_all(&atoms->c, atom_entry_free); + assert(atoms->atom_to_name == NULL); free(a); } diff --git a/src/atom.h b/src/atom.h index dea4155f..561bf98e 100644 --- a/src/atom.h +++ b/src/atom.h @@ -53,8 +53,6 @@ struct atom_entry; struct atom { - struct cache c; - struct atom_entry *atom_to_name; LIST_APPLY(ATOM_DEF, SEP_COLON, ATOM_LIST1); LIST_APPLY(ATOM_DEF, SEP_COLON, ATOM_LIST2); }; From 8f8d42f0c691addefe3992d794eafc61f2859a9a Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Sun, 18 Feb 2024 18:25:21 +0000 Subject: [PATCH 8/9] atom: add a mock for struct atom And define a macro specifically for fuzzer so we know when to build the mock. Signed-off-by: Yuxuan Shui --- src/atom.c | 26 ++++++++++++++++++++++++++ src/atom.h | 6 ++++++ src/meson.build | 2 +- src/picom.c | 8 +++++++- 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/atom.c b/src/atom.c index cfc4938c..33321a1c 100644 --- a/src/atom.c +++ b/src/atom.c @@ -134,3 +134,29 @@ void destroy_atoms(struct atom *a) { assert(atoms->atom_to_name == NULL); free(a); } + +#if defined(UNIT_TEST) || defined(CONFIG_FUZZER) + +static inline int mock_atom_getter(struct cache *cache, const char *atom_name attr_unused, + size_t atom_len attr_unused, struct cache_handle **value, + void *user_data attr_unused) { + auto atoms = container_of(cache, struct atom_impl, c); + xcb_atom_t atom = (xcb_atom_t)HASH_COUNT(atoms->atom_to_name) + 1; + struct atom_entry *entry = ccalloc(1, struct atom_entry); + entry->atom = atom; + HASH_ADD_INT(atoms->atom_to_name, atom, entry); + *value = &entry->entry; + return 0; +} + +struct atom *init_mock_atoms(void) { + return init_atoms_impl(NULL, mock_atom_getter); +} + +#else + +struct atom *init_mock_atoms(void) { + abort(); +} + +#endif \ No newline at end of file diff --git a/src/atom.h b/src/atom.h index 561bf98e..a24dcd44 100644 --- a/src/atom.h +++ b/src/atom.h @@ -74,3 +74,9 @@ const char *get_atom_name(struct atom *a, xcb_atom_t, xcb_connection_t *c); const char *get_atom_name_cached(struct atom *a, xcb_atom_t atom); void destroy_atoms(struct atom *a); + +/// A mock atom object for unit testing. Successive calls to get_atom will return +/// secutive integers as atoms, starting from 1. Calling get_atom_name with atoms +/// previously seen will result in the string that was used to create the atom; if +/// the atom was never returned by get_atom, it will abort. +struct atom *init_mock_atoms(void); \ No newline at end of file diff --git a/src/meson.build b/src/meson.build index 9b06ffae..ae4f05d7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -98,7 +98,7 @@ endif if cc.has_argument('-fsanitize=fuzzer') c2_fuzz = executable('c2_fuzz', srcs + ['fuzzer/c2.c'], - c_args: cflags + ['-fsanitize=fuzzer', '-Dmain=__main__'], + c_args: cflags + ['-fsanitize=fuzzer', '-DCONFIG_FUZZER'], link_args: ['-fsanitize=fuzzer'], dependencies: [ base_deps, deps, test_h_dep ], build_by_default: false, diff --git a/src/picom.c b/src/picom.c index 0ff437ef..ed302d2f 100644 --- a/src/picom.c +++ b/src/picom.c @@ -2791,10 +2791,16 @@ static void session_run(session_t *ps) { ev_run(ps->loop, 0); } +#ifdef CONFIG_FUZZER +#define PICOM_MAIN(...) no_main(__VA_ARGS__) +#else +#define PICOM_MAIN(...) main(__VA_ARGS__) +#endif + /** * The function that everybody knows. */ -int main(int argc, char **argv) { +int PICOM_MAIN(int argc, char **argv) { // Set locale so window names with special characters are interpreted // correctly setlocale(LC_ALL, ""); From 22116ec2b737dd46cdaf7afa6f4fec965d84e5f7 Mon Sep 17 00:00:00 2001 From: Yuxuan Shui Date: Sat, 17 Feb 2024 02:22:39 +0000 Subject: [PATCH 9/9] c2: add unit tests for c2_list_postprocess Signed-off-by: Yuxuan Shui --- src/c2.c | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/c2.c b/src/c2.c index dfb4c1d7..56aaef1e 100644 --- a/src/c2.c +++ b/src/c2.c @@ -406,9 +406,12 @@ c2_lptr_t *c2_parse(c2_lptr_t **pcondlst, const char *pattern, void *data) { TEST_CASE(c2_parse) { log_init_tls(); + // log_add_target_tls(stderr_logger_new()); char str[1024]; c2_lptr_t *cond = c2_parse(NULL, "name = \"xterm\"", NULL); + struct atom *atoms = init_mock_atoms(); + struct c2_state *state = c2_state_new(atoms); TEST_NOTEQUAL(cond, NULL); TEST_TRUE(!cond->ptr.isbranch); TEST_NOTEQUAL(cond->ptr.l, NULL); @@ -419,13 +422,14 @@ TEST_CASE(c2_parse) { size_t len = c2_condition_to_str(cond->ptr, str, sizeof(str)); TEST_STREQUAL3(str, "name = \"xterm\"", len); - struct c2_state state = { - .atoms = NULL, - }; struct managed_win test_win = { .name = "xterm", }; - TEST_TRUE(c2_match(&state, &test_win, cond, NULL)); + TEST_TRUE(c2_match(state, &test_win, cond, NULL)); + c2_list_postprocess(state, NULL, cond); + TEST_EQUAL(HASH_COUNT(state->tracked_properties), 0); + c2_state_free(state); + destroy_atoms(atoms); c2_list_free(&cond, NULL); cond = c2_parse(NULL, "_GTK_FRAME_EXTENTS@:c", NULL); @@ -439,6 +443,18 @@ TEST_CASE(c2_parse) { TEST_NOTEQUAL(cond->ptr.l->tgt, NULL); TEST_STREQUAL(cond->ptr.l->tgt, "_GTK_FRAME_EXTENTS"); + atoms = init_mock_atoms(); + state = c2_state_new(atoms); + c2_list_postprocess(state, NULL, cond); + TEST_EQUAL(HASH_COUNT(state->tracked_properties), 1); + HASH_ITER2(state->tracked_properties, prop) { + TEST_EQUAL(prop->key.property, + get_atom_with_nul(state->atoms, "_GTK_FRAME_EXTENTS", NULL)); + TEST_EQUAL(prop->max_indices, 0); + } + c2_state_free(state); + destroy_atoms(atoms); + len = c2_condition_to_str(cond->ptr, str, sizeof(str)); TEST_STREQUAL3(str, "_GTK_FRAME_EXTENTS@[0]", len); c2_list_free(&cond, NULL); @@ -461,13 +477,17 @@ TEST_CASE(c2_parse) { TEST_STREQUAL(cond->ptr.b->opr2.l->tgt, "class_g"); TEST_EQUAL(cond->ptr.b->opr2.l->predef, C2_L_PCLASSG); + atoms = init_mock_atoms(); + state = c2_state_new(atoms); len = c2_condition_to_str(cond->ptr, str, sizeof(str)); TEST_STREQUAL3(str, "(name = \"xterm\" && class_g *= \"XTerm\")", len); test_win.class_general = "XTerm"; - TEST_TRUE(c2_match(&state, &test_win, cond, NULL)); + TEST_TRUE(c2_match(state, &test_win, cond, NULL)); test_win.class_general = "asdf"; - TEST_TRUE(!c2_match(&state, &test_win, cond, NULL)); + TEST_TRUE(!c2_match(state, &test_win, cond, NULL)); c2_list_free(&cond, NULL); + c2_state_free(state); + destroy_atoms(atoms); cond = c2_parse(NULL, "_NET_WM_STATE[1]:32a *='_NET_WM_STATE_HIDDEN'", NULL); TEST_EQUAL(cond->ptr.l->index, 1);