Files
finit/libink/match.c
T
Joachim Wiberg 5cc41b3ba4 libink: take the match keys clients send without asking
A rule carrying sender, destination, or eavesdrop was refused whole,
and a peer whose AddMatch fails gets no signals at all.  That is a
poor trade for keys clients attach as a matter of course: better a
filter wider than asked for than a subscription that never happened.

They are accepted and ignored rather than honoured.  Widening costs
nothing here since Finit is the only sender on this bus, and what it
emits through the match table is state any peer that got this far may
already read.

argN and argNpath still take the whole rule down.  Honouring them
means parsing message bodies, and nothing asks for them yet.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-13 10:15:19 +02:00

219 lines
5.0 KiB
C

/* libink — D-Bus AddMatch / RemoveMatch rule parsing and matching.
*
* Subset of the spec: type, interface, member, path. Each entry is
* a key='value' pair with single-quoted value, separated by commas.
* Backslash escapes inside values (\\ and \') are not interpreted —
* a peer needing them will get unexpected literal content. Unknown
* keys cause the whole rule to be rejected so a peer learns its
* filter didn't take, rather than silently receiving everything.
*
* The exceptions are sender, destination, and eavesdrop, which are
* accepted and then ignored. Clients send them as a matter of
* course, and refusing the rule leaves such a peer with no signals at
* all, which serves it far worse than a filter wider than it asked
* for. Widening is safe here: Finit is the only sender on this bus,
* and everything it emits through the match table is state any peer
* that reached the bus may already read.
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
* SPDX-License-Identifier: MIT
*/
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include "internal.h"
static char *dup_range(const char *p, size_t n)
{
char *s = malloc(n + 1);
if (!s)
return NULL;
memcpy(s, p, n);
s[n] = '\0';
return s;
}
/* Parse one key='value' entry starting at *p. On success advances
* *p past the trailing quote and any comma, returns 0. On malformed
* input, returns -1. */
static int parse_kv(const char **p, char **out_key, char **out_value)
{
const char *q = *p;
const char *key_start, *val_start;
while (*q == ' ' || *q == '\t')
q++;
key_start = q;
while ((*q >= 'a' && *q <= 'z') || (*q >= 'A' && *q <= 'Z') || *q == '_')
q++;
if (q == key_start || *q != '=')
return -1;
*out_key = dup_range(key_start, (size_t)(q - key_start));
if (!*out_key)
return -1;
q++;
if (*q != '\'') {
free(*out_key);
return -1;
}
q++;
val_start = q;
while (*q && *q != '\'')
q++;
if (*q != '\'') {
free(*out_key);
return -1;
}
*out_value = dup_range(val_start, (size_t)(q - val_start));
if (!*out_value) {
free(*out_key);
return -1;
}
q++;
while (*q == ' ' || *q == '\t' || *q == ',')
q++;
*p = q;
return 0;
}
struct link_match *__match_parse(const char *rule)
{
struct link_match *m;
const char *p;
if (!rule || strlen(rule) >= LINK_MATCH_RULE_MAX) {
errno = EINVAL;
return NULL;
}
m = calloc(1, sizeof(*m));
if (!m)
return NULL;
m->raw = strdup(rule);
if (!m->raw) {
free(m);
return NULL;
}
for (p = rule; *p; ) {
char *key = NULL, *value = NULL;
char **slot = NULL;
if (parse_kv(&p, &key, &value) < 0)
goto bad;
if (!strcmp(key, "type")) slot = &m->type;
else if (!strcmp(key, "interface")) slot = &m->interface;
else if (!strcmp(key, "member")) slot = &m->member;
else if (!strcmp(key, "path")) slot = &m->path;
else if (!strcmp(key, "sender") ||
!strcmp(key, "destination") ||
!strcmp(key, "eavesdrop")) {
/* Understood well enough to accept, see above. */
free(key);
free(value);
continue;
} else {
/* XXX: argN and argNpath land here, so a rule
* using them takes nothing rather than too
* much. They narrow on body contents, which
* means parsing the body to honour them. */
free(key);
free(value);
goto bad;
}
if (*slot) {
/* Duplicate key. */
free(key);
free(value);
goto bad;
}
*slot = value;
free(key);
}
/* No need to default m->type: when it's NULL the matcher below
* treats it as "match any", and the only thing libink emits via
* the match table is signals, so the effective filter is
* already "signal" without the explicit assignment. */
return m;
bad:
__match_free(m);
errno = EINVAL;
return NULL;
}
void __match_free(struct link_match *m)
{
if (!m)
return;
free(m->raw);
free(m->type);
free(m->interface);
free(m->member);
free(m->path);
free(m);
}
static int field_matches(const char *want, const char *got)
{
if (!want)
return 1; /* no filter on this field */
if (!got)
return 0;
return strcmp(want, got) == 0;
}
int __match_matches(const struct link_match *m,
const char *path, const char *iface,
const char *member)
{
/* Type filter: only signals get delivered through this path. */
if (m->type && strcmp(m->type, "signal") != 0)
return 0;
return field_matches(m->path, path)
&& field_matches(m->interface, iface)
&& field_matches(m->member, member);
}
int __match_add(link_connection_t *conn, const char *rule)
{
struct link_match *m;
if (conn->matches_count >= LINK_MATCH_PEER_CAP) {
errno = ENOSPC;
return -1;
}
m = __match_parse(rule);
if (!m)
return -1;
conn->matches[conn->matches_count++] = m;
return 0;
}
int __match_remove(link_connection_t *conn, const char *rule)
{
size_t i;
for (i = 0; i < conn->matches_count; i++) {
if (strcmp(conn->matches[i]->raw, rule) == 0) {
__match_free(conn->matches[i]);
conn->matches[i] = conn->matches[conn->matches_count - 1];
conn->matches_count--;
return 0;
}
}
errno = ENOENT;
return -1;
}