mirror of
https://github.com/yshui/picom.git
synced 2025-04-14 17:53:25 -04:00

I realized it's probably not a good idea to have a different code path handling window creation etc. just for picom-inspect. I found a not-so-hacky way of doing picom-inspect inside the compositor, also helps with code reuse. Signed-off-by: Yuxuan Shui <yshuiv7@gmail.com>
2229 lines
60 KiB
C
2229 lines
60 KiB
C
// SPDX-License-Identifier: MIT
|
|
// Copyright (c) 2011-2013, Christopher Jeffrey
|
|
// Copyright (c) 2018 Yuxuan Shui <yshuiv7@gmail.com>
|
|
|
|
#include <assert.h>
|
|
#include <ctype.h>
|
|
#include <fnmatch.h>
|
|
#include <inttypes.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <uthash.h>
|
|
|
|
// libpcre
|
|
#ifdef CONFIG_REGEX_PCRE
|
|
#define PCRE2_CODE_UNIT_WIDTH 8
|
|
#include <pcre2.h>
|
|
|
|
#endif
|
|
|
|
#include <X11/Xlib.h>
|
|
#include <xcb/xcb.h>
|
|
|
|
#include "atom.h"
|
|
#include "common.h"
|
|
#include "compiler.h"
|
|
#include "config.h"
|
|
#include "log.h"
|
|
#include "test.h"
|
|
#include "utils/str.h"
|
|
#include "utils/uthash_extra.h"
|
|
#include "wm/win.h"
|
|
#include "x.h"
|
|
|
|
#include "c2.h"
|
|
|
|
#pragma GCC diagnostic error "-Wunused-parameter"
|
|
|
|
#define C2_MAX_LEVELS 10
|
|
|
|
typedef struct _c2_b c2_b_t;
|
|
typedef struct _c2_l c2_l_t;
|
|
|
|
/// Pointer to a condition tree.
|
|
typedef struct {
|
|
bool isbranch : 1;
|
|
bool istrue : 1;
|
|
union {
|
|
c2_b_t *b;
|
|
c2_l_t *l;
|
|
};
|
|
} c2_ptr_t;
|
|
|
|
struct c2_tracked_property_key {
|
|
xcb_atom_t property;
|
|
bool is_on_client;
|
|
char padding[3];
|
|
};
|
|
static_assert(sizeof(struct c2_tracked_property_key) == 8, "Padding bytes in "
|
|
"c2_tracked_property_key");
|
|
|
|
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.
|
|
static const c2_ptr_t C2_PTR_INIT = {
|
|
.isbranch = false,
|
|
.istrue = false,
|
|
.l = NULL,
|
|
};
|
|
|
|
/// Operator of a branch element.
|
|
typedef enum {
|
|
C2_B_OUNDEFINED,
|
|
C2_B_OAND,
|
|
C2_B_OOR,
|
|
C2_B_OXOR,
|
|
} c2_b_op_t;
|
|
|
|
/// Structure for branch element in a window condition
|
|
struct _c2_b {
|
|
bool neg : 1;
|
|
c2_b_op_t op;
|
|
c2_ptr_t opr1;
|
|
c2_ptr_t opr2;
|
|
};
|
|
|
|
/// Initializer for c2_b_t.
|
|
#define C2_B_INIT \
|
|
{.neg = false, .op = C2_B_OUNDEFINED, .opr1 = C2_PTR_INIT, .opr2 = C2_PTR_INIT}
|
|
|
|
/// Structure for leaf element in a window condition
|
|
struct _c2_l {
|
|
bool neg : 1;
|
|
enum {
|
|
C2_L_OEXISTS = 0,
|
|
C2_L_OEQ,
|
|
C2_L_OGT,
|
|
C2_L_OGTEQ,
|
|
C2_L_OLT,
|
|
C2_L_OLTEQ,
|
|
} op : 3;
|
|
enum {
|
|
C2_L_MEXACT,
|
|
C2_L_MSTART,
|
|
C2_L_MCONTAINS,
|
|
C2_L_MWILDCARD,
|
|
C2_L_MPCRE,
|
|
} match : 3;
|
|
bool match_ignorecase : 1;
|
|
char *tgt;
|
|
unsigned int target_id;
|
|
xcb_atom_t tgtatom;
|
|
bool target_on_client;
|
|
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,
|
|
C2_L_PX,
|
|
C2_L_PY,
|
|
C2_L_PX2,
|
|
C2_L_PY2,
|
|
C2_L_PWIDTH,
|
|
C2_L_PHEIGHT,
|
|
C2_L_PWIDTHB,
|
|
C2_L_PHEIGHTB,
|
|
C2_L_PBDW,
|
|
C2_L_PFULLSCREEN,
|
|
C2_L_POVREDIR,
|
|
C2_L_PARGB,
|
|
C2_L_PFOCUSED,
|
|
C2_L_PGROUPFOCUSED,
|
|
C2_L_PWMWIN,
|
|
C2_L_PBSHAPED,
|
|
C2_L_PROUNDED,
|
|
C2_L_PCLIENT,
|
|
C2_L_PWINDOWTYPE,
|
|
C2_L_PLEADER,
|
|
C2_L_PNAME,
|
|
C2_L_PCLASSG,
|
|
C2_L_PCLASSI,
|
|
C2_L_PROLE,
|
|
} predef;
|
|
enum {
|
|
C2_L_PTUNDEFINED,
|
|
C2_L_PTSTRING,
|
|
C2_L_PTINT,
|
|
} ptntype;
|
|
char *ptnstr;
|
|
long ptnint;
|
|
#ifdef CONFIG_REGEX_PCRE
|
|
pcre2_code *regex_pcre;
|
|
pcre2_match_data *regex_pcre_match;
|
|
#endif
|
|
};
|
|
|
|
static const unsigned int C2_L_INVALID_TARGET_ID = UINT_MAX;
|
|
/// Initializer for c2_l_t.
|
|
static const c2_l_t C2_L_INIT = {
|
|
.neg = false,
|
|
.op = C2_L_OEXISTS,
|
|
.match = C2_L_MEXACT,
|
|
.match_ignorecase = false,
|
|
.tgt = NULL,
|
|
.tgtatom = 0,
|
|
.target_on_client = false,
|
|
.predef = C2_L_PUNDEFINED,
|
|
.index = 0,
|
|
.ptntype = C2_L_PTUNDEFINED,
|
|
.ptnstr = NULL,
|
|
.ptnint = 0,
|
|
.target_id = C2_L_INVALID_TARGET_ID,
|
|
};
|
|
|
|
/// Linked list type of conditions.
|
|
struct _c2_lptr {
|
|
c2_ptr_t ptr;
|
|
void *data;
|
|
struct _c2_lptr *next;
|
|
};
|
|
|
|
/// Structure representing a predefined target.
|
|
typedef struct {
|
|
const char *name;
|
|
} c2_predef_t;
|
|
|
|
// clang-format off
|
|
// Predefined targets.
|
|
static struct {
|
|
const char *name;
|
|
bool is_string;
|
|
bool deprecated;
|
|
} C2_PREDEFS[] = {
|
|
[C2_L_PID] = { "id", false, true },
|
|
[C2_L_PX] = { "x", false, },
|
|
[C2_L_PY] = { "y", false, },
|
|
[C2_L_PX2] = { "x2", false, },
|
|
[C2_L_PY2] = { "y2", false, },
|
|
[C2_L_PWIDTH] = { "width", false, },
|
|
[C2_L_PHEIGHT] = { "height", false, },
|
|
[C2_L_PWIDTHB] = { "widthb", false, },
|
|
[C2_L_PHEIGHTB] = { "heightb", false, },
|
|
[C2_L_PBDW] = { "border_width", false, },
|
|
[C2_L_PFULLSCREEN] = { "fullscreen", false, },
|
|
[C2_L_POVREDIR] = { "override_redirect", false, },
|
|
[C2_L_PARGB] = { "argb", false, },
|
|
[C2_L_PFOCUSED] = { "focused", false, },
|
|
[C2_L_PGROUPFOCUSED] = { "group_focused", false, },
|
|
[C2_L_PWMWIN] = { "wmwin", false, },
|
|
[C2_L_PBSHAPED] = { "bounding_shaped", false, },
|
|
[C2_L_PROUNDED] = { "rounded_corners", false, },
|
|
[C2_L_PCLIENT] = { "client", false, true },
|
|
[C2_L_PWINDOWTYPE] = { "window_type", true, },
|
|
[C2_L_PLEADER] = { "leader", false, true },
|
|
[C2_L_PNAME] = { "name", true, },
|
|
[C2_L_PCLASSG] = { "class_g", true, },
|
|
[C2_L_PCLASSI] = { "class_i", true, },
|
|
[C2_L_PROLE] = { "role", true, },
|
|
};
|
|
// clang-format on
|
|
|
|
static const char *const c2_pattern_type_names[] = {
|
|
[C2_L_PTUNDEFINED] = "undefined",
|
|
[C2_L_PTSTRING] = "string",
|
|
[C2_L_PTINT] = "number",
|
|
};
|
|
|
|
/**
|
|
* Compare next word in a string with another string.
|
|
*/
|
|
static inline int strcmp_wd(const char *needle, const char *src) {
|
|
int ret = mstrncmp(needle, src);
|
|
if (ret) {
|
|
return ret;
|
|
}
|
|
|
|
char c = src[strlen(needle)];
|
|
if (isalnum((unsigned char)c) || '_' == c) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Return whether a c2_ptr_t is empty.
|
|
*/
|
|
static inline attr_unused bool c2_ptr_isempty(const c2_ptr_t p) {
|
|
return !(p.isbranch ? (bool)p.b : (bool)p.l);
|
|
}
|
|
|
|
/**
|
|
* Reset a c2_ptr_t.
|
|
*/
|
|
static inline void c2_ptr_reset(c2_ptr_t *pp) {
|
|
if (pp) {
|
|
*pp = C2_PTR_INIT;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Combine two condition trees.
|
|
*/
|
|
static inline c2_ptr_t c2h_comb_tree(c2_b_op_t op, c2_ptr_t p1, c2_ptr_t p2) {
|
|
c2_ptr_t p = {.isbranch = true, .b = NULL};
|
|
p.b = cmalloc(c2_b_t);
|
|
|
|
p.b->neg = false;
|
|
p.b->op = op;
|
|
p.b->opr1 = p1;
|
|
p.b->opr2 = p2;
|
|
|
|
return p;
|
|
}
|
|
|
|
/**
|
|
* Get the precedence value of a condition branch operator.
|
|
*/
|
|
static inline int c2h_b_opp(c2_b_op_t op) {
|
|
switch (op) {
|
|
case C2_B_OAND: return 2;
|
|
case C2_B_OOR:
|
|
case C2_B_OXOR: return 1;
|
|
default: break;
|
|
}
|
|
|
|
assert(0);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Compare precedence of two condition branch operators.
|
|
*
|
|
* Associativity is left-to-right, forever.
|
|
*
|
|
* @return positive number if op1 > op2, 0 if op1 == op2 in precedence,
|
|
* negative number otherwise
|
|
*/
|
|
static inline int c2h_b_opcmp(c2_b_op_t op1, c2_b_op_t op2) {
|
|
return c2h_b_opp(op1) - c2h_b_opp(op2);
|
|
}
|
|
|
|
static int c2_parse_grp(const char *pattern, int offset, c2_ptr_t *presult, int level);
|
|
|
|
static int c2_parse_target(const char *pattern, int offset, c2_ptr_t *presult);
|
|
|
|
static int c2_parse_op(const char *pattern, int offset, c2_ptr_t *presult);
|
|
|
|
static int c2_parse_pattern(const char *pattern, int offset, c2_ptr_t *presult);
|
|
|
|
static int c2_parse_legacy(const char *pattern, int offset, c2_ptr_t *presult);
|
|
|
|
static void c2_free(c2_ptr_t p);
|
|
|
|
static size_t c2_condition_to_str(c2_ptr_t p, char *output, size_t len);
|
|
static const char *c2_condition_to_str2(c2_ptr_t ptr);
|
|
|
|
/**
|
|
* Wrapper of c2_free().
|
|
*/
|
|
static inline void c2_freep(c2_ptr_t *pp) {
|
|
if (pp) {
|
|
c2_free(*pp);
|
|
c2_ptr_reset(pp);
|
|
}
|
|
}
|
|
|
|
static const char *c2h_dump_str_tgt(const c2_l_t *pleaf);
|
|
|
|
/**
|
|
* Parse a condition string.
|
|
*/
|
|
c2_lptr_t *c2_parse(c2_lptr_t **pcondlst, const char *pattern, void *data) {
|
|
if (!pattern) {
|
|
return NULL;
|
|
}
|
|
|
|
// Parse the pattern
|
|
c2_ptr_t result = C2_PTR_INIT;
|
|
int offset = -1;
|
|
|
|
if (strlen(pattern) >= 2 && ':' == pattern[1]) {
|
|
offset = c2_parse_legacy(pattern, 0, &result);
|
|
} else {
|
|
offset = c2_parse_grp(pattern, 0, &result, 0);
|
|
}
|
|
|
|
if (offset < 0) {
|
|
c2_freep(&result);
|
|
return NULL;
|
|
}
|
|
|
|
// Insert to pcondlst
|
|
{
|
|
auto plptr = cmalloc(c2_lptr_t);
|
|
*plptr = (c2_lptr_t){
|
|
.ptr = result,
|
|
.data = data,
|
|
.next = NULL,
|
|
};
|
|
if (pcondlst) {
|
|
plptr->next = *pcondlst;
|
|
*pcondlst = plptr;
|
|
}
|
|
|
|
#ifdef DEBUG_C2
|
|
log_trace("(\"%s\"): ", pattern);
|
|
c2_dump(plptr->ptr);
|
|
putchar('\n');
|
|
#endif
|
|
|
|
return plptr;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse a condition string with a prefix.
|
|
*/
|
|
c2_lptr_t *
|
|
c2_parse_with_prefix(c2_lptr_t **pcondlst, const char *pattern,
|
|
void *(*parse_prefix)(const char *input, const char **end, void *),
|
|
void (*free_value)(void *), void *user_data) {
|
|
char *pattern_start = NULL;
|
|
void *val = parse_prefix(pattern, (const char **)&pattern_start, user_data);
|
|
if (pattern_start == NULL) {
|
|
return NULL;
|
|
}
|
|
auto ret = c2_parse(pcondlst, pattern_start, val);
|
|
if (!ret && free_value) {
|
|
free_value(val);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
TEST_CASE(c2_parse) {
|
|
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);
|
|
TEST_EQUAL(cond->ptr.l->op, C2_L_OEQ);
|
|
TEST_EQUAL(cond->ptr.l->ptntype, C2_L_PTSTRING);
|
|
TEST_STREQUAL(cond->ptr.l->ptnstr, "xterm");
|
|
|
|
size_t len = c2_condition_to_str(cond->ptr, str, sizeof(str));
|
|
TEST_STREQUAL3(str, "name = \"xterm\"", len);
|
|
|
|
struct wm *wm = wm_new();
|
|
struct wm_ref *node = wm_new_mock_window(wm, 1);
|
|
|
|
struct win test_win = {
|
|
.name = "xterm",
|
|
.tree_ref = node,
|
|
};
|
|
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, "argb", NULL);
|
|
TEST_NOTEQUAL(cond, NULL);
|
|
TEST_TRUE(!cond->ptr.isbranch);
|
|
TEST_EQUAL(cond->ptr.l->ptntype, C2_L_PTINT);
|
|
c2_list_free(&cond, NULL);
|
|
|
|
cond = c2_parse(NULL, "argb = 'b'", NULL);
|
|
TEST_EQUAL(cond, NULL);
|
|
|
|
cond = c2_parse(NULL, "_GTK_FRAME_EXTENTS@:c", NULL);
|
|
TEST_NOTEQUAL(cond, NULL);
|
|
TEST_TRUE(!cond->ptr.isbranch);
|
|
TEST_NOTEQUAL(cond->ptr.l, NULL);
|
|
TEST_EQUAL(cond->ptr.l->op, C2_L_OEXISTS);
|
|
TEST_EQUAL(cond->ptr.l->match, C2_L_MEXACT);
|
|
TEST_EQUAL(cond->ptr.l->predef, C2_L_PUNDEFINED);
|
|
TEST_TRUE(cond->ptr.l->target_on_client);
|
|
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);
|
|
|
|
cond = c2_parse(NULL, "name = \"xterm\" && class_g *= \"XTerm\"", NULL);
|
|
TEST_NOTEQUAL(cond, NULL);
|
|
TEST_TRUE(cond->ptr.isbranch);
|
|
TEST_NOTEQUAL(cond->ptr.b, NULL);
|
|
TEST_EQUAL(cond->ptr.b->op, C2_B_OAND);
|
|
TEST_NOTEQUAL(cond->ptr.b->opr1.l, NULL);
|
|
TEST_NOTEQUAL(cond->ptr.b->opr2.l, NULL);
|
|
TEST_EQUAL(cond->ptr.b->opr1.l->op, C2_L_OEQ);
|
|
TEST_EQUAL(cond->ptr.b->opr1.l->match, C2_L_MEXACT);
|
|
TEST_EQUAL(cond->ptr.b->opr1.l->ptntype, C2_L_PTSTRING);
|
|
TEST_EQUAL(cond->ptr.b->opr2.l->op, C2_L_OEQ);
|
|
TEST_EQUAL(cond->ptr.b->opr2.l->match, C2_L_MCONTAINS);
|
|
TEST_EQUAL(cond->ptr.b->opr2.l->ptntype, C2_L_PTSTRING);
|
|
TEST_STREQUAL(cond->ptr.b->opr1.l->tgt, "name");
|
|
TEST_EQUAL(cond->ptr.b->opr1.l->predef, C2_L_PNAME);
|
|
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_win.class_general = "asdf";
|
|
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);
|
|
TEST_STREQUAL(cond->ptr.l->tgt, "_NET_WM_STATE");
|
|
TEST_STREQUAL(cond->ptr.l->ptnstr, "_NET_WM_STATE_HIDDEN");
|
|
|
|
len = c2_condition_to_str(cond->ptr, str, sizeof(str));
|
|
TEST_STREQUAL3(str, "_NET_WM_STATE[1] *= \"_NET_WM_STATE_HIDDEN\"", len);
|
|
c2_list_free(&cond, NULL);
|
|
|
|
cond = c2_parse(NULL, "_NET_WM_STATE[*]:32a*='_NET_WM_STATE_HIDDEN'", NULL);
|
|
TEST_EQUAL(cond->ptr.l->index, -1);
|
|
|
|
len = c2_condition_to_str(cond->ptr, str, sizeof(str));
|
|
TEST_STREQUAL3(str, "_NET_WM_STATE[*] *= \"_NET_WM_STATE_HIDDEN\"", len);
|
|
c2_list_free(&cond, NULL);
|
|
|
|
cond = c2_parse(NULL, "!class_i:0s", NULL);
|
|
TEST_NOTEQUAL(cond, NULL);
|
|
len = c2_condition_to_str(cond->ptr, str, sizeof(str));
|
|
TEST_STREQUAL3(str, "!class_i", len);
|
|
c2_list_free(&cond, NULL);
|
|
|
|
cond = c2_parse(NULL, "_NET_WM_STATE = '_NET_WM_STATE_HIDDEN'", NULL);
|
|
TEST_NOTEQUAL(cond, NULL);
|
|
c2_list_free(&cond, NULL);
|
|
|
|
cond = c2_parse(NULL, "1A:\n1111111111111ar1", NULL);
|
|
TEST_EQUAL(cond, NULL);
|
|
|
|
cond = c2_parse(NULL, "N [4444444444444: \n", NULL);
|
|
TEST_EQUAL(cond, NULL);
|
|
|
|
cond = c2_parse(NULL, " x:a=\"b\377\\xCCCCC", NULL);
|
|
TEST_EQUAL(cond, NULL);
|
|
|
|
cond = c2_parse(NULL, "!!!!!!!((((((!(((((,", NULL);
|
|
TEST_EQUAL(cond, NULL);
|
|
|
|
const char *rule = "(((role = \"\\\\tg^\\n\\n[\\t\" && role ~?= \"\") && "
|
|
"role ~?= \"\\n\\n\\n\\b\\n^\\n*0bon\") && role ~?= "
|
|
"\"\\n\\n\\x8a\\b\\n^\\n*0\\n[\\n[\\n\\n\\b\\n\")";
|
|
cond = c2_parse(NULL, rule, NULL);
|
|
TEST_NOTEQUAL(cond, NULL);
|
|
len = c2_condition_to_str(cond->ptr, str, sizeof(str));
|
|
TEST_STREQUAL3(str, rule, len);
|
|
c2_list_free(&cond, NULL);
|
|
|
|
wm_free_mock_window(wm, test_win.tree_ref);
|
|
wm_free(wm);
|
|
}
|
|
|
|
#define c2_error(format, ...) \
|
|
do { \
|
|
log_error("Pattern \"%s\" pos %d: " format, pattern, offset, ##__VA_ARGS__); \
|
|
goto fail; \
|
|
} while (0)
|
|
|
|
// TODO(yshui) Not a very good macro, should probably be a function
|
|
#define C2H_SKIP_SPACES() \
|
|
{ \
|
|
while (isspace((unsigned char)pattern[offset])) \
|
|
++offset; \
|
|
}
|
|
|
|
/**
|
|
* Parse a group in condition string.
|
|
*
|
|
* @return offset of next character in string
|
|
*/
|
|
static int c2_parse_grp(const char *pattern, int offset, c2_ptr_t *presult, int level) {
|
|
if (!pattern) {
|
|
return -1;
|
|
}
|
|
|
|
// Expected end character
|
|
const char endchar = (offset ? ')' : '\0');
|
|
|
|
// We use a system that a maximum of 2 elements are kept. When we find
|
|
// the third element, we combine the elements according to operator
|
|
// precedence. This design limits operators to have at most two-levels
|
|
// of precedence and fixed left-to-right associativity.
|
|
|
|
// For storing branch operators. ops[0] is actually unused
|
|
c2_b_op_t ops[3] = {};
|
|
// For storing elements
|
|
c2_ptr_t eles[2] = {C2_PTR_INIT, C2_PTR_INIT};
|
|
// Index of next free element slot in eles
|
|
int elei = 0;
|
|
// Pointer to the position of next element
|
|
c2_ptr_t *pele = eles;
|
|
// Negation flag of next operator
|
|
bool neg = false;
|
|
// Whether we are expecting an element immediately, is true at first, or
|
|
// after encountering a logical operator
|
|
bool next_expected = true;
|
|
|
|
// Check for recursion levels
|
|
if (level > C2_MAX_LEVELS) {
|
|
c2_error("Exceeded maximum recursion levels.");
|
|
}
|
|
|
|
// Parse the pattern character-by-character
|
|
for (; pattern[offset]; ++offset) {
|
|
assert(elei <= 2);
|
|
|
|
// Jump over spaces
|
|
if (isspace((unsigned char)pattern[offset])) {
|
|
continue;
|
|
}
|
|
|
|
// Handle end of group
|
|
if (')' == pattern[offset]) {
|
|
break;
|
|
}
|
|
|
|
// Handle "!"
|
|
if ('!' == pattern[offset]) {
|
|
if (!next_expected) {
|
|
c2_error("Unexpected \"!\".");
|
|
}
|
|
|
|
neg = !neg;
|
|
continue;
|
|
}
|
|
|
|
// Handle AND and OR
|
|
if ('&' == pattern[offset] || '|' == pattern[offset]) {
|
|
if (next_expected) {
|
|
c2_error("Unexpected logical operator.");
|
|
}
|
|
|
|
next_expected = true;
|
|
if (!mstrncmp("&&", pattern + offset)) {
|
|
ops[elei] = C2_B_OAND;
|
|
++offset;
|
|
} else if (!mstrncmp("||", pattern + offset)) {
|
|
ops[elei] = C2_B_OOR;
|
|
++offset;
|
|
} else {
|
|
c2_error("Illegal logical operator.");
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Parsing an element
|
|
if (!next_expected) {
|
|
c2_error("Unexpected expression.");
|
|
}
|
|
|
|
assert(!elei || ops[elei]);
|
|
|
|
// If we are out of space
|
|
if (2 == elei) {
|
|
--elei;
|
|
// If the first operator has higher or equal precedence, combine
|
|
// the first two elements
|
|
if (c2h_b_opcmp(ops[1], ops[2]) >= 0) {
|
|
eles[0] = c2h_comb_tree(ops[1], eles[0], eles[1]);
|
|
c2_ptr_reset(&eles[1]);
|
|
pele = &eles[elei];
|
|
ops[1] = ops[2];
|
|
}
|
|
// Otherwise, combine the second and the incoming one
|
|
else {
|
|
eles[1] = c2h_comb_tree(ops[2], eles[1], C2_PTR_INIT);
|
|
assert(eles[1].isbranch);
|
|
pele = &eles[1].b->opr2;
|
|
}
|
|
// The last operator always needs to be reset
|
|
ops[2] = C2_B_OUNDEFINED;
|
|
}
|
|
|
|
// It's a subgroup if it starts with '('
|
|
if ('(' == pattern[offset]) {
|
|
if ((offset = c2_parse_grp(pattern, offset + 1, pele, level + 1)) < 0) {
|
|
goto fail;
|
|
}
|
|
}
|
|
// Otherwise it's a leaf
|
|
else {
|
|
if ((offset = c2_parse_target(pattern, offset, pele)) < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
assert(!pele->isbranch && !c2_ptr_isempty(*pele));
|
|
|
|
if ((offset = c2_parse_op(pattern, offset, pele)) < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
if ((offset = c2_parse_pattern(pattern, offset, pele)) < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
if (pele->l->predef != C2_L_PUNDEFINED) {
|
|
typeof(pele->l->ptntype) predef_type =
|
|
C2_PREDEFS[pele->l->predef].is_string ? C2_L_PTSTRING
|
|
: C2_L_PTINT;
|
|
if (C2_PREDEFS[pele->l->predef].deprecated) {
|
|
log_warn("Predefined target \"%s\" is "
|
|
"deprecated. Matching against it will "
|
|
"always fail. Offending pattern is "
|
|
"\"%s\"",
|
|
pele->l->tgt, pattern);
|
|
}
|
|
if (pele->l->op == C2_L_OEXISTS) {
|
|
pele->l->ptntype = predef_type;
|
|
} else if (pele->l->ptntype != predef_type) {
|
|
c2_error("Predefined target %s is a %s, but you "
|
|
"are comparing it with a %s",
|
|
C2_PREDEFS[pele->l->predef].name,
|
|
c2_pattern_type_names[predef_type],
|
|
c2_pattern_type_names[pele->l->ptntype]);
|
|
}
|
|
}
|
|
}
|
|
// Decrement offset -- we will increment it in loop update
|
|
--offset;
|
|
|
|
// Apply negation
|
|
if (neg) {
|
|
neg = false;
|
|
if (pele->isbranch) {
|
|
pele->b->neg = !pele->b->neg;
|
|
} else {
|
|
pele->l->neg = !pele->l->neg;
|
|
}
|
|
}
|
|
|
|
next_expected = false;
|
|
++elei;
|
|
pele = &eles[elei];
|
|
}
|
|
|
|
// Wrong end character?
|
|
if (pattern[offset] && !endchar) {
|
|
c2_error("Expected end of string but found '%c'.", pattern[offset]);
|
|
}
|
|
if (!pattern[offset] && endchar) {
|
|
c2_error("Expected '%c' but found end of string.", endchar);
|
|
}
|
|
|
|
// Handle end of group
|
|
if (!elei) {
|
|
c2_error("Empty group.");
|
|
} else if (next_expected) {
|
|
c2_error("Missing rule before end of group.");
|
|
} else if (elei > 1) {
|
|
assert(2 == elei);
|
|
assert(ops[1]);
|
|
eles[0] = c2h_comb_tree(ops[1], eles[0], eles[1]);
|
|
c2_ptr_reset(&eles[1]);
|
|
}
|
|
|
|
*presult = eles[0];
|
|
|
|
if (')' == pattern[offset]) {
|
|
++offset;
|
|
}
|
|
|
|
return offset;
|
|
|
|
fail:
|
|
c2_freep(&eles[0]);
|
|
c2_freep(&eles[1]);
|
|
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Parse the target part of a rule.
|
|
*/
|
|
static int c2_parse_target(const char *pattern, int offset, c2_ptr_t *presult) {
|
|
// Initialize leaf
|
|
presult->isbranch = false;
|
|
presult->l = cmalloc(c2_l_t);
|
|
|
|
c2_l_t *const pleaf = presult->l;
|
|
*pleaf = C2_L_INIT;
|
|
|
|
// Parse negation marks
|
|
while ('!' == pattern[offset]) {
|
|
pleaf->neg = !pleaf->neg;
|
|
++offset;
|
|
C2H_SKIP_SPACES();
|
|
}
|
|
|
|
// Copy target name out
|
|
int tgtlen = 0;
|
|
for (; pattern[offset] && (isalnum((unsigned char)pattern[offset]) ||
|
|
'_' == pattern[offset] || '.' == pattern[offset]);
|
|
++offset) {
|
|
++tgtlen;
|
|
}
|
|
if (!tgtlen) {
|
|
c2_error("Empty target.");
|
|
}
|
|
pleaf->tgt = strndup(&pattern[offset - tgtlen], (size_t)tgtlen);
|
|
|
|
// Check for predefined targets
|
|
static const int npredefs = (int)(sizeof(C2_PREDEFS) / sizeof(C2_PREDEFS[0]));
|
|
for (int i = 0; i < npredefs; ++i) {
|
|
if (!strcmp(C2_PREDEFS[i].name, pleaf->tgt)) {
|
|
pleaf->predef = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
C2H_SKIP_SPACES();
|
|
|
|
// Parse target-on-client flag
|
|
if ('@' == pattern[offset]) {
|
|
pleaf->target_on_client = true;
|
|
++offset;
|
|
C2H_SKIP_SPACES();
|
|
}
|
|
|
|
// Parse index
|
|
if ('[' == pattern[offset]) {
|
|
if (pleaf->predef != C2_L_PUNDEFINED) {
|
|
c2_error("Predefined targets can't have index.");
|
|
}
|
|
|
|
offset++;
|
|
|
|
C2H_SKIP_SPACES();
|
|
|
|
long index = -1;
|
|
const char *endptr = NULL;
|
|
|
|
if ('*' == pattern[offset]) {
|
|
index = -1;
|
|
endptr = pattern + offset + 1;
|
|
} else {
|
|
index = strtol(pattern + offset, (char **)&endptr, 0);
|
|
if (index < 0) {
|
|
c2_error("Index number invalid.");
|
|
}
|
|
}
|
|
|
|
if (!endptr || pattern + offset == endptr) {
|
|
c2_error("No index number found after bracket.");
|
|
}
|
|
if (index > INT_MAX) {
|
|
c2_error("Index %ld too large.", index);
|
|
}
|
|
|
|
pleaf->index = (int)index;
|
|
offset = to_int_checked(endptr - pattern);
|
|
|
|
C2H_SKIP_SPACES();
|
|
|
|
if (pattern[offset] != ']') {
|
|
c2_error("Index end marker not found.");
|
|
}
|
|
|
|
++offset;
|
|
|
|
C2H_SKIP_SPACES();
|
|
}
|
|
|
|
// Parse target type and format
|
|
if (':' == pattern[offset]) {
|
|
++offset;
|
|
C2H_SKIP_SPACES();
|
|
|
|
// Look for format
|
|
bool hasformat = false;
|
|
long format = 0;
|
|
char *endptr = NULL;
|
|
format = strtol(pattern + offset, &endptr, 0);
|
|
assert(endptr);
|
|
hasformat = endptr && endptr != pattern + offset;
|
|
if (hasformat) {
|
|
offset = to_int_checked(endptr - pattern);
|
|
}
|
|
C2H_SKIP_SPACES();
|
|
|
|
// Look for type
|
|
switch (pattern[offset]) {
|
|
case 'w':
|
|
case 'd':
|
|
case 'c':
|
|
case 's':
|
|
case 'a': break;
|
|
default: c2_error("Invalid type character.");
|
|
}
|
|
|
|
log_warn("Type specifier is deprecated. Type \"%c\" specified on target "
|
|
"\"%s\" will be ignored, you can remove it.",
|
|
pattern[offset], pleaf->tgt);
|
|
|
|
offset++;
|
|
C2H_SKIP_SPACES();
|
|
|
|
// Write format
|
|
if (hasformat) {
|
|
log_warn("Format specifier is deprecated. Format \"%ld\" "
|
|
"specified on target \"%s\" will be ignored, you can "
|
|
"remove it.",
|
|
format, pleaf->tgt);
|
|
if (format && format != 8 && format != 16 && format != 32) {
|
|
c2_error("Invalid format %ld.", format);
|
|
}
|
|
}
|
|
}
|
|
return offset;
|
|
|
|
fail:
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Parse the operator part of a leaf.
|
|
*/
|
|
static int c2_parse_op(const char *pattern, int offset, c2_ptr_t *presult) {
|
|
c2_l_t *const pleaf = presult->l;
|
|
|
|
// Parse negation marks
|
|
C2H_SKIP_SPACES();
|
|
while ('!' == pattern[offset]) {
|
|
pleaf->neg = !pleaf->neg;
|
|
++offset;
|
|
C2H_SKIP_SPACES();
|
|
}
|
|
|
|
// Parse qualifiers
|
|
if ('*' == pattern[offset] || '^' == pattern[offset] || '%' == pattern[offset] ||
|
|
'~' == pattern[offset]) {
|
|
switch (pattern[offset]) {
|
|
case '*': pleaf->match = C2_L_MCONTAINS; break;
|
|
case '^': pleaf->match = C2_L_MSTART; break;
|
|
case '%': pleaf->match = C2_L_MWILDCARD; break;
|
|
case '~': pleaf->match = C2_L_MPCRE; break;
|
|
default: assert(0);
|
|
}
|
|
++offset;
|
|
C2H_SKIP_SPACES();
|
|
}
|
|
|
|
// Parse flags
|
|
while ('?' == pattern[offset]) {
|
|
pleaf->match_ignorecase = true;
|
|
++offset;
|
|
C2H_SKIP_SPACES();
|
|
}
|
|
|
|
// Parse operator
|
|
while ('=' == pattern[offset] || '>' == pattern[offset] || '<' == pattern[offset]) {
|
|
if ('=' == pattern[offset] && C2_L_OGT == pleaf->op) {
|
|
pleaf->op = C2_L_OGTEQ;
|
|
} else if ('=' == pattern[offset] && C2_L_OLT == pleaf->op) {
|
|
pleaf->op = C2_L_OLTEQ;
|
|
} else if (pleaf->op) {
|
|
c2_error("Duplicate operator.");
|
|
} else {
|
|
switch (pattern[offset]) {
|
|
case '=': pleaf->op = C2_L_OEQ; break;
|
|
case '>': pleaf->op = C2_L_OGT; break;
|
|
case '<': pleaf->op = C2_L_OLT; break;
|
|
default: assert(0);
|
|
}
|
|
}
|
|
++offset;
|
|
C2H_SKIP_SPACES();
|
|
}
|
|
|
|
// Check for problems
|
|
if (C2_L_OEQ != pleaf->op && (pleaf->match || pleaf->match_ignorecase)) {
|
|
c2_error("Exists/greater-than/less-than operators cannot have a "
|
|
"qualifier.");
|
|
}
|
|
|
|
return offset;
|
|
|
|
fail:
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Parse the pattern part of a leaf.
|
|
*/
|
|
static int c2_parse_pattern(const char *pattern, int offset, c2_ptr_t *presult) {
|
|
c2_l_t *const pleaf = presult->l;
|
|
|
|
// Exists operator cannot have pattern
|
|
if (!pleaf->op) {
|
|
return offset;
|
|
}
|
|
|
|
C2H_SKIP_SPACES();
|
|
|
|
char *endptr = NULL;
|
|
if (!strcmp_wd("true", &pattern[offset])) {
|
|
pleaf->ptntype = C2_L_PTINT;
|
|
pleaf->ptnint = true;
|
|
offset += 4; // length of "true";
|
|
} else if (!strcmp_wd("false", &pattern[offset])) {
|
|
pleaf->ptntype = C2_L_PTINT;
|
|
pleaf->ptnint = false;
|
|
offset += 5; // length of "false";
|
|
} else if (pleaf->ptnint = strtol(pattern + offset, &endptr, 0),
|
|
pattern + offset != endptr) {
|
|
pleaf->ptntype = C2_L_PTINT;
|
|
offset = to_int_checked(endptr - pattern);
|
|
// Make sure we are stopping at the end of a word
|
|
if (isalnum((unsigned char)pattern[offset])) {
|
|
c2_error("Trailing characters after a numeric pattern.");
|
|
}
|
|
} else {
|
|
// Parse string patterns
|
|
bool raw = false;
|
|
char delim = '\0';
|
|
|
|
// String flags
|
|
if (tolower((unsigned char)pattern[offset]) == 'r') {
|
|
raw = true;
|
|
++offset;
|
|
C2H_SKIP_SPACES();
|
|
}
|
|
|
|
if (raw == true) {
|
|
log_warn("Raw string patterns has been deprecated. pos %d", offset);
|
|
}
|
|
|
|
// Check for delimiters
|
|
if (pattern[offset] == '\"' || pattern[offset] == '\'') {
|
|
pleaf->ptntype = C2_L_PTSTRING;
|
|
delim = pattern[offset];
|
|
++offset;
|
|
}
|
|
|
|
if (pleaf->ptntype != C2_L_PTSTRING) {
|
|
c2_error("Invalid pattern type.");
|
|
}
|
|
|
|
// Parse the string now
|
|
// We can't determine the length of the pattern, so we use the length
|
|
// to the end of the pattern string -- currently escape sequences
|
|
// cannot be converted to a string longer than itself.
|
|
auto tptnstr = ccalloc((strlen(pattern + offset) + 1), char);
|
|
char *ptptnstr = tptnstr;
|
|
pleaf->ptnstr = tptnstr;
|
|
for (; pattern[offset] && delim != pattern[offset]; ++offset) {
|
|
// Handle escape sequences if it's not a raw string
|
|
if ('\\' == pattern[offset] && !raw) {
|
|
switch (pattern[++offset]) {
|
|
case '\\': *(ptptnstr++) = '\\'; break;
|
|
case '\'': *(ptptnstr++) = '\''; break;
|
|
case '\"': *(ptptnstr++) = '\"'; break;
|
|
case 'a': *(ptptnstr++) = '\a'; break;
|
|
case 'b': *(ptptnstr++) = '\b'; break;
|
|
case 'f': *(ptptnstr++) = '\f'; break;
|
|
case 'n': *(ptptnstr++) = '\n'; break;
|
|
case 'r': *(ptptnstr++) = '\r'; break;
|
|
case 't': *(ptptnstr++) = '\t'; break;
|
|
case 'v': *(ptptnstr++) = '\v'; break;
|
|
case 'o':
|
|
case 'x': {
|
|
scoped_charp tstr = strndup(pattern + offset + 1, 2);
|
|
char *pstr = NULL;
|
|
long val = strtol(
|
|
tstr, &pstr, ('o' == pattern[offset] ? 8 : 16));
|
|
if (pstr != &tstr[2] || val <= 0) {
|
|
c2_error("Invalid octal/hex escape "
|
|
"sequence.");
|
|
}
|
|
if (val > 255) {
|
|
c2_error("Octal/hex escape sequence out "
|
|
"of ASCII range.");
|
|
}
|
|
if (val > 127) {
|
|
// Manual sign extension
|
|
val -= 256;
|
|
}
|
|
*(ptptnstr++) = (char)val;
|
|
offset += 2;
|
|
break;
|
|
}
|
|
default: c2_error("Invalid escape sequence.");
|
|
}
|
|
} else {
|
|
*(ptptnstr++) = pattern[offset];
|
|
}
|
|
}
|
|
if (!pattern[offset]) {
|
|
c2_error("Premature end of pattern string.");
|
|
}
|
|
++offset;
|
|
*ptptnstr = '\0';
|
|
pleaf->ptnstr = strdup(tptnstr);
|
|
free(tptnstr);
|
|
}
|
|
|
|
C2H_SKIP_SPACES();
|
|
|
|
if (!pleaf->ptntype) {
|
|
c2_error("Invalid pattern type.");
|
|
}
|
|
|
|
if (pleaf->ptntype == C2_L_PTINT) {
|
|
if (pleaf->match) {
|
|
c2_error("Integer/boolean pattern cannot have operator "
|
|
"qualifiers.");
|
|
}
|
|
if (pleaf->match_ignorecase) {
|
|
c2_error("Integer/boolean pattern cannot have flags.");
|
|
}
|
|
}
|
|
|
|
if (C2_L_PTSTRING == pleaf->ptntype &&
|
|
(C2_L_OGT == pleaf->op || C2_L_OGTEQ == pleaf->op || C2_L_OLT == pleaf->op ||
|
|
C2_L_OLTEQ == pleaf->op)) {
|
|
c2_error("String pattern cannot have an arithmetic operator.");
|
|
}
|
|
|
|
return offset;
|
|
|
|
fail:
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Parse a condition with legacy syntax.
|
|
*/
|
|
static int c2_parse_legacy(const char *pattern, int offset, c2_ptr_t *presult) {
|
|
if (strlen(pattern + offset) < 4 || pattern[offset + 1] != ':' ||
|
|
!strchr(pattern + offset + 2, ':')) {
|
|
c2_error("Legacy parser: Invalid format.");
|
|
}
|
|
|
|
// Allocate memory for new leaf
|
|
auto pleaf = cmalloc(c2_l_t);
|
|
presult->isbranch = false;
|
|
presult->l = pleaf;
|
|
*pleaf = C2_L_INIT;
|
|
pleaf->op = C2_L_OEQ;
|
|
pleaf->ptntype = C2_L_PTSTRING;
|
|
|
|
// Determine the pattern target
|
|
switch (pattern[offset]) {
|
|
case 'n': pleaf->predef = C2_L_PNAME; break;
|
|
case 'i': pleaf->predef = C2_L_PCLASSI; break;
|
|
case 'g': pleaf->predef = C2_L_PCLASSG; break;
|
|
case 'r': pleaf->predef = C2_L_PROLE; break;
|
|
default: c2_error("Target \"%c\" invalid.\n", pattern[offset]);
|
|
}
|
|
|
|
offset += 2;
|
|
|
|
// Determine the match type
|
|
switch (pattern[offset]) {
|
|
case 'e': pleaf->match = C2_L_MEXACT; break;
|
|
case 'a': pleaf->match = C2_L_MCONTAINS; break;
|
|
case 's': pleaf->match = C2_L_MSTART; break;
|
|
case 'w': pleaf->match = C2_L_MWILDCARD; break;
|
|
case 'p': pleaf->match = C2_L_MPCRE; break;
|
|
default: c2_error("Type \"%c\" invalid.\n", pattern[offset]);
|
|
}
|
|
++offset;
|
|
|
|
// Determine the pattern flags
|
|
while (':' != pattern[offset]) {
|
|
switch (pattern[offset]) {
|
|
case 'i': pleaf->match_ignorecase = true; break;
|
|
default: c2_error("Flag \"%c\" invalid.", pattern[offset]);
|
|
}
|
|
++offset;
|
|
}
|
|
++offset;
|
|
|
|
// Copy the pattern
|
|
pleaf->ptnstr = strdup(pattern + offset);
|
|
|
|
return offset;
|
|
|
|
fail:
|
|
return -1;
|
|
}
|
|
|
|
#undef c2_error
|
|
|
|
/**
|
|
* Do postprocessing on a condition leaf.
|
|
*/
|
|
static bool c2_l_postprocess(struct c2_state *state, xcb_connection_t *c, c2_l_t *pleaf) {
|
|
// Get target atom if it's not a predefined one
|
|
if (pleaf->predef == C2_L_PUNDEFINED) {
|
|
pleaf->tgtatom = get_atom_with_nul(state->atoms, pleaf->tgt, c);
|
|
if (!pleaf->tgtatom) {
|
|
log_error("Failed to get atom for target \"%s\".", pleaf->tgt);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Insert target atom into tracked property name list
|
|
if (pleaf->tgtatom) {
|
|
struct c2_tracked_property *property;
|
|
struct c2_tracked_property_key key = {
|
|
.property = pleaf->tgtatom,
|
|
.is_on_client = pleaf->target_on_client,
|
|
};
|
|
HASH_FIND(hh, state->tracked_properties, &key, sizeof(key), property);
|
|
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;
|
|
}
|
|
pleaf->target_id = property->id;
|
|
}
|
|
|
|
// Warn about lower case characters in target name
|
|
if (pleaf->predef == C2_L_PUNDEFINED) {
|
|
for (const char *pc = pleaf->tgt; *pc; ++pc) {
|
|
if (islower((unsigned char)*pc)) {
|
|
log_warn("Lowercase character in target name \"%s\".",
|
|
pleaf->tgt);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// PCRE patterns
|
|
if (C2_L_PTSTRING == pleaf->ptntype && C2_L_MPCRE == pleaf->match) {
|
|
#ifdef CONFIG_REGEX_PCRE
|
|
int errorcode = 0;
|
|
PCRE2_SIZE erroffset = 0;
|
|
unsigned int options = 0;
|
|
|
|
// Ignore case flag
|
|
if (pleaf->match_ignorecase) {
|
|
options |= PCRE2_CASELESS;
|
|
}
|
|
|
|
// Compile PCRE expression
|
|
pleaf->regex_pcre =
|
|
pcre2_compile((PCRE2_SPTR)pleaf->ptnstr, PCRE2_ZERO_TERMINATED,
|
|
options, &errorcode, &erroffset, NULL);
|
|
if (pleaf->regex_pcre == NULL) {
|
|
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",
|
|
pleaf->ptnstr, erroffset, buffer);
|
|
return false;
|
|
}
|
|
pleaf->regex_pcre_match =
|
|
pcre2_match_data_create_from_pattern(pleaf->regex_pcre, NULL);
|
|
#else
|
|
log_error("PCRE regular expression support not compiled in.");
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool c2_tree_postprocess(struct c2_state *state, xcb_connection_t *c, c2_ptr_t node) {
|
|
if (node.istrue) {
|
|
return true;
|
|
}
|
|
if (!node.isbranch) {
|
|
return c2_l_postprocess(state, c, node.l);
|
|
}
|
|
|
|
return c2_tree_postprocess(state, c, node.b->opr1) &&
|
|
c2_tree_postprocess(state, c, node.b->opr2);
|
|
}
|
|
|
|
bool c2_list_postprocess(struct c2_state *state, xcb_connection_t *c, c2_lptr_t *list) {
|
|
c2_lptr_t *head = list;
|
|
while (head) {
|
|
if (!c2_tree_postprocess(state, c, head->ptr)) {
|
|
return false;
|
|
}
|
|
head = head->next;
|
|
}
|
|
return true;
|
|
}
|
|
/**
|
|
* Free a condition tree.
|
|
*/
|
|
static void c2_free(c2_ptr_t p) {
|
|
// For a branch element
|
|
if (p.isbranch) {
|
|
c2_b_t *const pbranch = p.b;
|
|
|
|
if (!pbranch) {
|
|
return;
|
|
}
|
|
|
|
c2_free(pbranch->opr1);
|
|
c2_free(pbranch->opr2);
|
|
free(pbranch);
|
|
}
|
|
// For a leaf element
|
|
else {
|
|
c2_l_t *const pleaf = p.l;
|
|
|
|
if (!pleaf) {
|
|
return;
|
|
}
|
|
|
|
free(pleaf->tgt);
|
|
free(pleaf->ptnstr);
|
|
#ifdef CONFIG_REGEX_PCRE
|
|
pcre2_code_free(pleaf->regex_pcre);
|
|
pcre2_match_data_free(pleaf->regex_pcre_match);
|
|
#endif
|
|
free(pleaf);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Free a condition tree in c2_lptr_t.
|
|
*/
|
|
c2_lptr_t *c2_free_lptr(c2_lptr_t *lp, c2_userdata_free f) {
|
|
if (!lp) {
|
|
return NULL;
|
|
}
|
|
|
|
c2_lptr_t *pnext = lp->next;
|
|
if (f) {
|
|
f(lp->data);
|
|
}
|
|
lp->data = NULL;
|
|
c2_free(lp->ptr);
|
|
free(lp);
|
|
|
|
return pnext;
|
|
}
|
|
|
|
/**
|
|
* Get a string representation of a rule target.
|
|
*/
|
|
static const char *c2h_dump_str_tgt(const c2_l_t *pleaf) {
|
|
if (pleaf->predef != C2_L_PUNDEFINED) {
|
|
return C2_PREDEFS[pleaf->predef].name;
|
|
}
|
|
return pleaf->tgt;
|
|
}
|
|
|
|
/**
|
|
* Dump a condition tree to string. Return the number of characters that
|
|
* would have been written if the buffer had been large enough, excluding
|
|
* the null terminator.
|
|
* null terminator will not be written to the output.
|
|
*/
|
|
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); \
|
|
offset++
|
|
#define push_str(str) \
|
|
do { \
|
|
if (offset < len) { \
|
|
size_t slen = strlen(str); \
|
|
if (offset + slen > len) \
|
|
slen = len - offset; \
|
|
memcpy(output + offset, str, slen); \
|
|
} \
|
|
offset += strlen(str); \
|
|
} while (false)
|
|
size_t offset = 0;
|
|
if (p.isbranch) {
|
|
// Branch, i.e. logical operators &&, ||, XOR
|
|
const c2_b_t *const pbranch = p.b;
|
|
|
|
if (!pbranch) {
|
|
return 0;
|
|
}
|
|
|
|
if (pbranch->neg) {
|
|
push_char('!');
|
|
}
|
|
|
|
push_char('(');
|
|
if (len > offset) {
|
|
offset += c2_condition_to_str(pbranch->opr1, output + offset,
|
|
len - offset);
|
|
} else {
|
|
offset += c2_condition_to_str(pbranch->opr1, NULL, 0);
|
|
}
|
|
|
|
switch (pbranch->op) {
|
|
case C2_B_OAND: push_str(" && "); break;
|
|
case C2_B_OOR: push_str(" || "); break;
|
|
case C2_B_OXOR: push_str(" XOR "); break;
|
|
default: assert(0); break;
|
|
}
|
|
|
|
if (len > offset) {
|
|
offset += c2_condition_to_str(pbranch->opr2, output + offset,
|
|
len - offset);
|
|
} else {
|
|
offset += c2_condition_to_str(pbranch->opr2, NULL, 0);
|
|
}
|
|
push_str(")");
|
|
} else {
|
|
// Leaf node
|
|
const c2_l_t *const pleaf = p.l;
|
|
char number[128];
|
|
|
|
if (!pleaf) {
|
|
return 0;
|
|
}
|
|
|
|
if (C2_L_OEXISTS == pleaf->op && pleaf->neg) {
|
|
push_char('!');
|
|
}
|
|
|
|
// Print target name, type, and format
|
|
const char *target_str = c2h_dump_str_tgt(pleaf);
|
|
push_str(target_str);
|
|
if (pleaf->target_on_client) {
|
|
push_char('@');
|
|
}
|
|
if (pleaf->predef == C2_L_PUNDEFINED) {
|
|
if (pleaf->index < 0) {
|
|
push_str("[*]");
|
|
} else {
|
|
sprintf(number, "[%d]", pleaf->index);
|
|
push_str(number);
|
|
}
|
|
}
|
|
|
|
if (C2_L_OEXISTS == pleaf->op) {
|
|
return offset;
|
|
}
|
|
|
|
// Print operator
|
|
push_char(' ');
|
|
|
|
if (C2_L_OEXISTS != pleaf->op && pleaf->neg) {
|
|
push_char('!');
|
|
}
|
|
|
|
switch (pleaf->match) {
|
|
case C2_L_MEXACT: break;
|
|
case C2_L_MCONTAINS: push_char('*'); break;
|
|
case C2_L_MSTART: push_char('^'); break;
|
|
case C2_L_MPCRE: push_char('~'); break;
|
|
case C2_L_MWILDCARD: push_char('%'); break;
|
|
}
|
|
|
|
if (pleaf->match_ignorecase) {
|
|
push_char('?');
|
|
}
|
|
|
|
switch (pleaf->op) {
|
|
case C2_L_OEXISTS: break;
|
|
case C2_L_OEQ: push_str("="); break;
|
|
case C2_L_OGT: push_str(">"); break;
|
|
case C2_L_OGTEQ: push_str(">="); break;
|
|
case C2_L_OLT: push_str("<"); break;
|
|
case C2_L_OLTEQ: push_str("<="); break;
|
|
}
|
|
|
|
// Print pattern
|
|
push_char(' ');
|
|
switch (pleaf->ptntype) {
|
|
case C2_L_PTINT:
|
|
sprintf(number, "%ld", pleaf->ptnint);
|
|
push_str(number);
|
|
break;
|
|
case C2_L_PTSTRING:
|
|
// TODO(yshui) Escape string before printing out?
|
|
push_char('"');
|
|
for (int i = 0; pleaf->ptnstr[i]; i++) {
|
|
switch (pleaf->ptnstr[i]) {
|
|
case '\\': push_str("\\\\"); break;
|
|
case '"': push_str("\\\""); break;
|
|
case '\a': push_str("\\a"); break;
|
|
case '\b': push_str("\\b"); break;
|
|
case '\f': push_str("\\f"); break;
|
|
case '\r': push_str("\\r"); break;
|
|
case '\v': push_str("\\v"); break;
|
|
case '\t': push_str("\\t"); break;
|
|
case '\n': push_str("\\n"); break;
|
|
default:
|
|
if (isprint(pleaf->ptnstr[i])) {
|
|
push_char(pleaf->ptnstr[i]);
|
|
} else {
|
|
sprintf(number, "\\x%02x",
|
|
(unsigned char)pleaf->ptnstr[i]);
|
|
push_str(number);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
push_char('"');
|
|
break;
|
|
default: assert(0); break;
|
|
}
|
|
}
|
|
#undef push_char
|
|
#undef push_str
|
|
return offset;
|
|
}
|
|
|
|
/// Wrapper of c2_condition_to_str which uses an internal static buffer, and
|
|
/// returns a nul terminated string. The returned string is only valid until the
|
|
/// next call to this function, and should not be freed.
|
|
static const char *c2_condition_to_str2(c2_ptr_t ptr) {
|
|
static thread_local char buf[4096];
|
|
auto len = c2_condition_to_str(ptr, buf, sizeof(buf));
|
|
if (len >= sizeof(buf)) {
|
|
// Resulting string is too long, clobber the last character with a nul.
|
|
buf[sizeof(buf) - 1] = '\0';
|
|
} else {
|
|
buf[len] = '\0';
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
const char *c2_lptr_to_str(const c2_lptr_t *ptr) {
|
|
return c2_condition_to_str2(ptr->ptr);
|
|
}
|
|
|
|
/// 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 win *w, const c2_l_t *leaf) {
|
|
// Get the value
|
|
if (leaf->predef != C2_L_PUNDEFINED) {
|
|
// A predefined target
|
|
int64_t predef_target = 0;
|
|
if (C2_PREDEFS[leaf->predef].deprecated) {
|
|
return false;
|
|
}
|
|
|
|
switch (leaf->predef) {
|
|
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_PARGB: predef_target = win_has_alpha(w); break;
|
|
case C2_L_PFOCUSED:
|
|
predef_target =
|
|
w->a.map_state == XCB_MAP_STATE_VIEWABLE && w->is_focused;
|
|
break;
|
|
case C2_L_PGROUPFOCUSED:
|
|
predef_target = w->a.map_state == XCB_MAP_STATE_VIEWABLE &&
|
|
w->is_group_focused;
|
|
break;
|
|
case C2_L_PWMWIN: predef_target = win_is_wmwin(w); break;
|
|
case C2_L_PBSHAPED: predef_target = w->bounding_shaped; break;
|
|
case C2_L_PROUNDED: predef_target = w->rounded_corners; break;
|
|
case C2_L_POVREDIR:
|
|
// When user wants to check override-redirect, they almost always
|
|
// want to check the client window, not the frame window. We
|
|
// don't track the override-redirect state of the client window
|
|
// directly, however we can assume if a window has a window
|
|
// manager frame around it, it's not override-redirect.
|
|
predef_target = w->a.override_redirect &&
|
|
wm_ref_client_of(w->tree_ref) == NULL;
|
|
break;
|
|
default: unreachable();
|
|
}
|
|
return c2_int_op(leaf, predef_target);
|
|
}
|
|
|
|
// A raw window property
|
|
if (leaf->target_id == C2_L_INVALID_TARGET_ID) {
|
|
log_debug("Leaf target ID is invalid, skipping. Most likely a list "
|
|
"postprocessing failure.");
|
|
return false;
|
|
}
|
|
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,
|
|
wm_ref_win_id(w->tree_ref), w->name);
|
|
return false;
|
|
}
|
|
|
|
if (values->type == C2_PROPERTY_TYPE_STRING) {
|
|
log_error("Property %s is not an integer", leaf->tgt);
|
|
return false;
|
|
}
|
|
|
|
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) {
|
|
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(struct atom *atoms, const struct win *w, const c2_l_t *leaf) {
|
|
|
|
// A predefined target
|
|
const char *predef_target = NULL;
|
|
if (leaf->predef != C2_L_PUNDEFINED) {
|
|
if (leaf->predef == C2_L_PWINDOWTYPE) {
|
|
for (unsigned i = 0; i < NUM_WINTYPES; i++) {
|
|
if (w->window_types & (1 << i) &&
|
|
c2_string_op(leaf, WINTYPES[i].name)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
switch (leaf->predef) {
|
|
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;
|
|
case C2_L_PWINDOWTYPE:
|
|
default: unreachable();
|
|
}
|
|
if (!predef_target) {
|
|
return false;
|
|
}
|
|
log_verbose("Matching against predefined target %s", predef_target);
|
|
return c2_string_op(leaf, predef_target);
|
|
}
|
|
|
|
if (leaf->target_id == C2_L_INVALID_TARGET_ID) {
|
|
log_debug("Leaf target ID is invalid, skipping. Most likely a list "
|
|
"postprocessing failure.");
|
|
return false;
|
|
}
|
|
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, client %#010x (%s)",
|
|
leaf->tgt, win_id(w), win_client_id(w, false), 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) {
|
|
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;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
return false;
|
|
}
|
|
size_t offset = 0;
|
|
int index = leaf->index;
|
|
while (offset < values->length && index != 0) {
|
|
offset += strlen(values->string + offset) + 1;
|
|
index -= 1;
|
|
}
|
|
if (index != 0 || values->length == 0) {
|
|
// index is out of bounds
|
|
return false;
|
|
}
|
|
return c2_string_op(leaf, values->string + offset);
|
|
}
|
|
|
|
/**
|
|
* Match a window against a single leaf window condition.
|
|
*
|
|
* For internal use.
|
|
*/
|
|
static inline bool
|
|
c2_match_once_leaf(const struct c2_state *state, const struct win *w, const c2_l_t *leaf) {
|
|
assert(leaf);
|
|
|
|
const xcb_window_t wid =
|
|
(leaf->target_on_client ? win_client_id(w, /*fallback_to_self=*/true) : win_id(w));
|
|
|
|
// Return if wid is missing
|
|
if (leaf->predef == C2_L_PUNDEFINED && !wid) {
|
|
log_debug("Window ID missing.");
|
|
return false;
|
|
}
|
|
|
|
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}));
|
|
|
|
unsigned int pattern_type = leaf->ptntype;
|
|
if (pattern_type == C2_L_PTUNDEFINED) {
|
|
if (leaf->target_id == C2_L_INVALID_TARGET_ID) {
|
|
log_debug("Leaf target ID is invalid, skipping. Most likely a "
|
|
"list postprocessing failure.");
|
|
return false;
|
|
}
|
|
auto values = &w->c2_state.values[leaf->target_id];
|
|
if (values->type == C2_PROPERTY_TYPE_STRING) {
|
|
pattern_type = C2_L_PTSTRING;
|
|
} else {
|
|
pattern_type = C2_L_PTINT;
|
|
}
|
|
}
|
|
|
|
switch (pattern_type) {
|
|
// Deal with integer patterns
|
|
case C2_L_PTINT: return c2_match_once_leaf_int(w, leaf);
|
|
// String patterns
|
|
case C2_L_PTSTRING: return c2_match_once_leaf_string(state->atoms, w, leaf);
|
|
default: unreachable();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Match a window against a single window condition.
|
|
*
|
|
* @return true if matched, false otherwise.
|
|
*/
|
|
static bool
|
|
c2_match_once(const struct c2_state *state, const struct win *w, const c2_ptr_t cond) {
|
|
bool result = false;
|
|
|
|
if (cond.isbranch) {
|
|
// Handle a branch (and/or/xor operation)
|
|
const c2_b_t *pb = cond.b;
|
|
|
|
if (!pb) {
|
|
return false;
|
|
}
|
|
|
|
log_verbose("Matching window %#010x (%s) against condition %s", win_id(w),
|
|
w->name, c2_condition_to_str2(cond));
|
|
|
|
switch (pb->op) {
|
|
case C2_B_OAND:
|
|
result = (c2_match_once(state, w, pb->opr1) &&
|
|
c2_match_once(state, w, pb->opr2));
|
|
break;
|
|
case C2_B_OOR:
|
|
result = (c2_match_once(state, w, pb->opr1) ||
|
|
c2_match_once(state, w, pb->opr2));
|
|
break;
|
|
case C2_B_OXOR:
|
|
result = (c2_match_once(state, w, pb->opr1) !=
|
|
c2_match_once(state, w, pb->opr2));
|
|
break;
|
|
default: unreachable();
|
|
}
|
|
|
|
log_debug("(%#010x): branch: result = %d, pattern = %s", win_id(w),
|
|
result, c2_condition_to_str2(cond));
|
|
} else if (cond.istrue) {
|
|
return true;
|
|
} else {
|
|
// A leaf
|
|
const c2_l_t *pleaf = cond.l;
|
|
|
|
if (!pleaf) {
|
|
return false;
|
|
}
|
|
|
|
result = c2_match_once_leaf(state, w, pleaf);
|
|
|
|
log_debug("(%#010x): leaf: result = %d, client = %#010x, pattern = %s",
|
|
win_id(w), result, win_client_id(w, false),
|
|
c2_condition_to_str2(cond));
|
|
}
|
|
|
|
// Postprocess the result
|
|
if (cond.isbranch ? cond.b->neg : cond.l->neg) {
|
|
result = !result;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
c2_lptr_t *c2_new_true(void) {
|
|
auto ret = ccalloc(1, c2_lptr_t);
|
|
ret->ptr = (c2_ptr_t){.istrue = true};
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Match a window against a condition linked list.
|
|
*
|
|
* @param cache a place to cache the last matched condition
|
|
* @param pdata a place to return the data
|
|
* @return true if matched, false otherwise.
|
|
*/
|
|
bool c2_match(struct c2_state *state, const struct 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(state, w, condlst->ptr)) {
|
|
if (pdata) {
|
|
*pdata = condlst->data;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// Match a window against the first condition in a condition linked list.
|
|
bool c2_match_one(const struct c2_state *state, const struct win *w,
|
|
const c2_lptr_t *condlst, void **pdata) {
|
|
if (!condlst) {
|
|
return false;
|
|
}
|
|
if (c2_match_once(state, w, condlst->ptr)) {
|
|
if (pdata) {
|
|
*pdata = condlst->data;
|
|
}
|
|
return true;
|
|
}
|
|
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.
|
|
///
|
|
/// Returns whether the iteration was stopped early.
|
|
bool c2_list_foreach(const c2_lptr_t *condlist, c2_list_foreach_cb_t cb, void *data) {
|
|
for (auto i = condlist; i; i = i->next) {
|
|
if (cb(i, data)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Return user data stored in a condition.
|
|
void *c2_list_get_data(const c2_lptr_t *condlist) {
|
|
return condlist->data;
|
|
}
|
|
|
|
void *c2_list_set_data(c2_lptr_t *condlist, void *data) {
|
|
void *old = condlist->data;
|
|
condlist->data = data;
|
|
return old;
|
|
}
|
|
|
|
struct c2_state *c2_state_new(struct atom *atoms) {
|
|
auto ret = ccalloc(1, struct c2_state);
|
|
ret->atoms = atoms;
|
|
return ret;
|
|
}
|
|
|
|
void c2_state_free(struct c2_state *state) {
|
|
struct c2_tracked_property *property, *tmp;
|
|
HASH_ITER(hh, state->tracked_properties, property, tmp) {
|
|
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_client) {
|
|
struct c2_tracked_property *p;
|
|
struct c2_tracked_property_key key = {
|
|
.property = property,
|
|
.is_on_client = is_on_client,
|
|
};
|
|
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_client ? client_win : frame_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_client ? client_win : frame_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 = {
|
|
.property = property,
|
|
.is_on_client = true,
|
|
};
|
|
HASH_FIND(hh, state->tracked_properties, &key, sizeof(key), p);
|
|
if (p != NULL) {
|
|
return true;
|
|
}
|
|
key.is_on_client = false;
|
|
HASH_FIND(hh, state->tracked_properties, &key, sizeof(key), p);
|
|
return p != NULL;
|
|
}
|
|
|
|
void c2_condlist_insert(c2_lptr_t **pcondlst, c2_lptr_t *pnew) {
|
|
c2_lptr_t *pcur = *pcondlst;
|
|
pnew->next = pcur;
|
|
*pcondlst = pnew;
|
|
}
|