Removed build dependency on xproto.
[platform/upstream/libxkbcommon.git] / src / xkbcomp / rules.c
index ea68752..f5d9c49 100644 (file)
  *
  ********************************************************/
 
-#include <stdio.h>
-#include <ctype.h>
-
-#include "rules.h"
-#include "path.h"
-
-static bool
-input_line_get(struct xkb_context *ctx, FILE *file, darray_char *line)
-{
-    int ch;
-    bool end_of_file = false;
-    bool space_pending;
-    bool slash_pending;
-    bool in_comment;
-
-    while (!end_of_file && darray_empty(*line)) {
-        space_pending = slash_pending = in_comment = false;
-
-        while ((ch = getc(file)) != '\n' && ch != EOF) {
-            if (ch == '\\') {
-                ch = getc(file);
-
-                if (ch == EOF)
-                    break;
-
-                if (ch == '\n') {
-                    in_comment = false;
-                    ch = ' ';
-                }
-            }
+/*
+ * Copyright © 2012 Ran Benita <ran234@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
 
-            if (in_comment)
-                continue;
+#include "config.h"
 
-            if (ch == '/') {
-                if (slash_pending) {
-                    in_comment = true;
-                    slash_pending = false;
-                }
-                else {
-                    slash_pending = true;
-                }
+#include "xkbcomp-priv.h"
+#include "rules.h"
+#include "include.h"
+#include "scanner-utils.h"
 
-                continue;
-            }
+#define MAX_INCLUDE_DEPTH 5
 
-            if (slash_pending) {
-                if (space_pending) {
-                    darray_append(*line, ' ');
-                    space_pending = false;
-                }
+/* Scanner / Lexer */
 
-                darray_append(*line, '/');
-                slash_pending = false;
-            }
+/* Values returned with some tokens, like yylval. */
+union lvalue {
+    struct sval string;
+};
 
-            if (isspace(ch)) {
-                while (isspace(ch) && ch != '\n' && ch != EOF)
-                    ch = getc(file);
+enum rules_token {
+    TOK_END_OF_FILE = 0,
+    TOK_END_OF_LINE,
+    TOK_IDENTIFIER,
+    TOK_GROUP_NAME,
+    TOK_BANG,
+    TOK_EQUALS,
+    TOK_STAR,
+    TOK_INCLUDE,
+    TOK_ERROR
+};
 
-                if (ch == EOF)
-                    break;
+static inline bool
+is_ident(char ch)
+{
+    return is_graph(ch) && ch != '\\';
+}
 
-                if (ch != '\n' && !darray_empty(*line))
-                    space_pending = true;
+static enum rules_token
+lex(struct scanner *s, union lvalue *val)
+{
+skip_more_whitespace_and_comments:
+    /* Skip spaces. */
+    while (scanner_chr(s, ' ') || scanner_chr(s, '\t') || scanner_chr(s, '\r'));
 
-                ungetc(ch, file);
-            }
-            else {
-                if (space_pending) {
-                    darray_append(*line, ' ');
-                    space_pending = false;
-                }
+    /* Skip comments. */
+    if (scanner_lit(s, "//")) {
+        scanner_skip_to_eol(s);
+    }
 
-                if (ch == '!') {
-                    if (!darray_empty(*line)) {
-                        log_warn(ctx,
-                                 "The '!' is legal only at start of line; "
-                                 "Line containing '!' ignored\n");
-                        darray_resize(*line, 0);
-                        break;
-                    }
-                }
+    /* New line. */
+    if (scanner_eol(s)) {
+        while (scanner_eol(s)) scanner_next(s);
+        return TOK_END_OF_LINE;
+    }
 
-                darray_append(*line, ch);
-            }
+    /* Escaped line continuation. */
+    if (scanner_chr(s, '\\')) {
+        /* Optional \r. */
+        scanner_chr(s, '\r');
+        if (!scanner_eol(s)) {
+            scanner_err(s, "illegal new line escape; must appear at end of line");
+            return TOK_ERROR;
         }
+        scanner_next(s);
+        goto skip_more_whitespace_and_comments;
+    }
 
-        if (ch == EOF)
-            end_of_file = true;
+    /* See if we're done. */
+    if (scanner_eof(s)) return TOK_END_OF_FILE;
+
+    /* New token. */
+    s->token_line = s->line;
+    s->token_column = s->column;
+
+    /* Operators and punctuation. */
+    if (scanner_chr(s, '!')) return TOK_BANG;
+    if (scanner_chr(s, '=')) return TOK_EQUALS;
+    if (scanner_chr(s, '*')) return TOK_STAR;
+
+    /* Group name. */
+    if (scanner_chr(s, '$')) {
+        val->string.start = s->s + s->pos;
+        val->string.len = 0;
+        while (is_ident(scanner_peek(s))) {
+            scanner_next(s);
+            val->string.len++;
+        }
+        if (val->string.len == 0) {
+            scanner_err(s, "unexpected character after \'$\'; expected name");
+            return TOK_ERROR;
+        }
+        return TOK_GROUP_NAME;
     }
 
-    if (darray_empty(*line) && end_of_file)
-        return false;
+    /* Include statement. */
+    if (scanner_lit(s, "include"))
+        return TOK_INCLUDE;
+
+    /* Identifier. */
+    if (is_ident(scanner_peek(s))) {
+        val->string.start = s->s + s->pos;
+        val->string.len = 0;
+        while (is_ident(scanner_peek(s))) {
+            scanner_next(s);
+            val->string.len++;
+        }
+        return TOK_IDENTIFIER;
+    }
 
-    darray_append(*line, '\0');
-    return true;
+    scanner_err(s, "unrecognized token");
+    return TOK_ERROR;
 }
 
 /***====================================================================***/
 
-enum {
-    /* "Parts" - the MLVO which rules file maps to components. */
-    MODEL = 0,
-    LAYOUT,
-    VARIANT,
-    OPTION,
-
-#define PART_MASK \
-    ((1 << MODEL) | (1 << LAYOUT) | (1 << VARIANT) | (1 << OPTION))
-
-    /* Components */
-    KEYCODES,
-    SYMBOLS,
-    TYPES,
-    COMPAT,
-    GEOMETRY,
+enum rules_mlvo {
+    MLVO_MODEL,
+    MLVO_LAYOUT,
+    MLVO_VARIANT,
+    MLVO_OPTION,
+    _MLVO_NUM_ENTRIES
+};
 
-#define COMPONENT_MASK \
-    ((1 << KEYCODES) | (1 << SYMBOLS) | (1 << TYPES) | (1 << COMPAT) | \
-     (1 << GEOMETRY))
+#define SVAL_LIT(literal) { literal, sizeof(literal) - 1 }
 
-    MAX_WORDS
+static const struct sval rules_mlvo_svals[_MLVO_NUM_ENTRIES] = {
+    [MLVO_MODEL] = SVAL_LIT("model"),
+    [MLVO_LAYOUT] = SVAL_LIT("layout"),
+    [MLVO_VARIANT] = SVAL_LIT("variant"),
+    [MLVO_OPTION] = SVAL_LIT("option"),
 };
 
-static const char *cname[] = {
-    [MODEL] = "model",
-    [LAYOUT] = "layout",
-    [VARIANT] = "variant",
-    [OPTION] = "option",
-
-    [KEYCODES] = "keycodes",
-    [SYMBOLS] = "symbols",
-    [TYPES] = "types",
-    [COMPAT] = "compat",
-    [GEOMETRY] = "geometry",
+enum rules_kccgst {
+    KCCGST_KEYCODES,
+    KCCGST_TYPES,
+    KCCGST_COMPAT,
+    KCCGST_SYMBOLS,
+    KCCGST_GEOMETRY,
+    _KCCGST_NUM_ENTRIES
 };
 
-struct multi_defs {
-    const char *model;
-    const char *layout[XkbNumKbdGroups + 1];
-    const char *variant[XkbNumKbdGroups + 1];
-    char *options;
+static const struct sval rules_kccgst_svals[_KCCGST_NUM_ENTRIES] = {
+    [KCCGST_KEYCODES] = SVAL_LIT("keycodes"),
+    [KCCGST_TYPES] = SVAL_LIT("types"),
+    [KCCGST_COMPAT] = SVAL_LIT("compat"),
+    [KCCGST_SYMBOLS] = SVAL_LIT("symbols"),
+    [KCCGST_GEOMETRY] = SVAL_LIT("geometry"),
 };
 
-struct mapping {
-    /* Sequential id for the mappings. */
-    int number;
-    size_t num_maps;
-
-    struct {
-        int word;
-        int index;
-    } map[MAX_WORDS];
+/* We use this to keep score whether an mlvo was matched or not; if not,
+ * we warn the user that his preference was ignored. */
+struct matched_sval {
+    struct sval sval;
+    bool matched;
 };
+typedef darray(struct matched_sval) darray_matched_sval;
 
-struct var_desc {
-    char *name;
-    char *desc;
+/*
+ * A broken-down version of xkb_rule_names (without the rules,
+ * obviously).
+ */
+struct rule_names {
+    struct matched_sval model;
+    darray_matched_sval layouts;
+    darray_matched_sval variants;
+    darray_matched_sval options;
 };
 
 struct group {
-    int number;
-    char *name;
-    char *words;
+    struct sval name;
+    darray_sval elements;
 };
 
-enum rule_flag {
-    RULE_FLAG_PENDING_MATCH = (1L << 1),
-    RULE_FLAG_OPTION = (1L << 2),
-    RULE_FLAG_APPEND = (1L << 3),
-    RULE_FLAG_NORMAL = (1L << 4),
+struct mapping {
+    int mlvo_at_pos[_MLVO_NUM_ENTRIES];
+    unsigned int num_mlvo;
+    unsigned int defined_mlvo_mask;
+    xkb_layout_index_t layout_idx, variant_idx;
+    int kccgst_at_pos[_KCCGST_NUM_ENTRIES];
+    unsigned int num_kccgst;
+    unsigned int defined_kccgst_mask;
+    bool skip;
+};
+
+enum mlvo_match_type {
+    MLVO_MATCH_NORMAL = 0,
+    MLVO_MATCH_WILDCARD,
+    MLVO_MATCH_GROUP,
 };
 
 struct rule {
-    int number;
-
-    char *model;
-    char *layout;
-    int layout_num;
-    char *variant;
-    int variant_num;
-    char *option;
-
-    /* yields */
-
-    char *keycodes;
-    char *symbols;
-    char *types;
-    char *compat;
-    unsigned flags;
+    struct sval mlvo_value_at_pos[_MLVO_NUM_ENTRIES];
+    enum mlvo_match_type match_type_at_pos[_MLVO_NUM_ENTRIES];
+    unsigned int num_mlvo_values;
+    struct sval kccgst_value_at_pos[_KCCGST_NUM_ENTRIES];
+    unsigned int num_kccgst_values;
+    bool skip;
 };
 
-struct rules {
-    darray(struct rule) rules;
+/*
+ * This is the main object used to match a given RMLVO against a rules
+ * file and aggragate the results in a KcCGST. It goes through a simple
+ * matching state machine, with tokens as transitions (see
+ * matcher_match()).
+ */
+struct matcher {
+    struct xkb_context *ctx;
+    /* Input.*/
+    struct rule_names rmlvo;
+    union lvalue val;
     darray(struct group) groups;
+    /* Current mapping. */
+    struct mapping mapping;
+    /* Current rule. */
+    struct rule rule;
+    /* Output. */
+    darray_char kccgst[_KCCGST_NUM_ENTRIES];
 };
 
-/***====================================================================***/
+static struct sval
+strip_spaces(struct sval v)
+{
+    while (v.len > 0 && is_space(v.start[0])) { v.len--; v.start++; }
+    while (v.len > 0 && is_space(v.start[v.len - 1])) v.len--;
+    return v;
+}
 
-/*
- * Resolve numeric index, such as "[4]" in layout[4]. Missing index
- * means zero.
- */
-static char *
-get_index(char *str, int *ndx)
+static darray_matched_sval
+split_comma_separated_mlvo(const char *s)
 {
-    int empty = 0, consumed = 0, num;
+    darray_matched_sval arr = darray_new();
 
-    sscanf(str, "[%n%d]%n", &empty, &num, &consumed);
-    if (consumed > 0) {
-        *ndx = num;
-        str += consumed;
-    }
-    else if (empty > 0) {
-        *ndx = -1;
+    /*
+     * Make sure the array returned by this function always includes at
+     * least one value, e.g. "" -> { "" } and "," -> { "", "" }.
+     */
+
+    if (!s) {
+        struct matched_sval val = { .sval = { NULL, 0 } };
+        darray_append(arr, val);
+        return arr;
     }
-    else {
-        *ndx = 0;
+
+    while (true) {
+        struct matched_sval val = { .sval = { s, 0 } };
+        while (*s != '\0' && *s != ',') { s++; val.sval.len++; }
+        val.sval = strip_spaces(val.sval);
+        darray_append(arr, val);
+        if (*s == '\0') break;
+        if (*s == ',') s++;
     }
 
-    return str;
+    return arr;
 }
 
-/*
- * Match a mapping line which opens a rule, e.g:
- * ! model      layout[4]       variant[4]      =       symbols       geometry
- * Which will be followed by lines such as:
- *   *          ben             basic           =       +in(ben):4    nec(pc98)
- * So if the MLVO matches the LHS of some line, we'll get the components
- * on the RHS.
- * In this example, we will get for the second and fourth columns:
- * mapping->map[1] = {.word = LAYOUT, .index = 4}
- * mapping->map[3] = {.word = SYMBOLS, .index = 0}
- */
-static void
-match_mapping_line(struct xkb_context *ctx, darray_char *line,
-                   struct mapping *mapping)
+static struct matcher *
+matcher_new(struct xkb_context *ctx,
+            const struct xkb_rule_names *rmlvo)
 {
-    char *tok;
-    char *str = darray_mem(*line, 1);
-    unsigned present = 0, layout_ndx_present = 0, variant_ndx_present = 0;
-    int i, tmp;
-    size_t len;
-    int ndx;
-    char *strtok_buf;
-    bool found;
+    struct matcher *m = calloc(1, sizeof(*m));
+    if (!m)
+        return NULL;
 
-    /*
-     * Remember the last sequential mapping id (incremented if the match
-     * is successful).
-     */
-    tmp = mapping->number;
-    memset(mapping, 0, sizeof(*mapping));
-    mapping->number = tmp;
+    m->ctx = ctx;
+    m->rmlvo.model.sval.start = rmlvo->model;
+    m->rmlvo.model.sval.len = strlen_safe(rmlvo->model);
+    m->rmlvo.layouts = split_comma_separated_mlvo(rmlvo->layout);
+    m->rmlvo.variants = split_comma_separated_mlvo(rmlvo->variant);
+    m->rmlvo.options = split_comma_separated_mlvo(rmlvo->options);
 
-    while ((tok = strtok_r(str, " ", &strtok_buf)) != NULL) {
-        found = false;
-        str = NULL;
+    return m;
+}
 
-        if (strcmp(tok, "=") == 0)
-            continue;
+static void
+matcher_free(struct matcher *m)
+{
+    struct group *group;
+    if (!m)
+        return;
+    darray_free(m->rmlvo.layouts);
+    darray_free(m->rmlvo.variants);
+    darray_free(m->rmlvo.options);
+    darray_foreach(group, m->groups)
+        darray_free(group->elements);
+    for (int i = 0; i < _KCCGST_NUM_ENTRIES; i++)
+        darray_free(m->kccgst[i]);
+    darray_free(m->groups);
+    free(m);
+}
 
-        for (i = 0; i < MAX_WORDS; i++) {
-            len = strlen(cname[i]);
-
-            if (strncmp(cname[i], tok, len) == 0) {
-                if (strlen(tok) > len) {
-                    char *end = get_index(tok + len, &ndx);
-
-                    if ((i != LAYOUT && i != VARIANT) ||
-                        *end != '\0' || ndx == -1) {
-                        log_warn(ctx,
-                                 "Illegal %s index: %d\n", cname[i], ndx);
-                        log_warn(ctx, "Can only index layout and variant\n");
-                        break;
-                    }
-
-                    if (ndx < 1 || ndx > XkbNumKbdGroups) {
-                        log_warn(ctx, "Illegal %s index: %d\n",
-                                 cname[i], ndx);
-                        log_warn(ctx, "Index must be in range 1..%d\n",
-                                 XkbNumKbdGroups);
-                        break;
-                    }
-                }
-                else {
-                    ndx = 0;
-                }
+static void
+matcher_group_start_new(struct matcher *m, struct sval name)
+{
+    struct group group = { .name = name, .elements = darray_new() };
+    darray_append(m->groups, group);
+}
 
-                found = true;
+static void
+matcher_group_add_element(struct matcher *m, struct scanner *s,
+                          struct sval element)
+{
+    darray_append(darray_item(m->groups, darray_size(m->groups) - 1).elements,
+                  element);
+}
 
-                if (present & (1 << i)) {
-                    if ((i == LAYOUT && layout_ndx_present & (1 << ndx)) ||
-                        (i == VARIANT && variant_ndx_present & (1 << ndx))) {
-                        log_warn(ctx,
-                                 "Component \"%s\" listed twice; "
-                                 "Second definition ignored\n", tok);
-                        break;
-                    }
-                }
+static bool
+read_rules_file(struct xkb_context *ctx,
+                struct matcher *matcher,
+                unsigned include_depth,
+                FILE *file,
+                const char *path);
 
-                present |= (1 << i);
-                if (i == LAYOUT)
-                    layout_ndx_present |= 1 << ndx;
-                if (i == VARIANT)
-                    variant_ndx_present |= 1 << ndx;
+static void
+matcher_include(struct matcher *m, struct scanner *parent_scanner,
+                unsigned include_depth,
+                struct sval inc)
+{
+    struct scanner s; /* parses the !include value */
+    FILE *file;
 
-                mapping->map[mapping->num_maps].word = i;
-                mapping->map[mapping->num_maps].index = ndx;
-                mapping->num_maps++;
-                break;
-            }
-        }
+    scanner_init(&s, m->ctx, inc.start, inc.len,
+                 parent_scanner->file_name, NULL);
+    s.token_line = parent_scanner->token_line;
+    s.token_column = parent_scanner->token_column;
+    s.buf_pos = 0;
 
-        if (!found)
-            log_warn(ctx, "Unknown component \"%s\"; Ignored\n", tok);
+    if (include_depth >= MAX_INCLUDE_DEPTH) {
+        scanner_err(&s, "maximum include depth (%d) exceeded; maybe there is an include loop?",
+                    MAX_INCLUDE_DEPTH);
+        return;
     }
 
-    if ((present & PART_MASK) == 0) {
-        log_warn(ctx,
-                 "Mapping needs at least one MLVO part; "
-                 "Illegal mapping ignored\n");
-        mapping->num_maps = 0;
+    while (!scanner_eof(&s) && !scanner_eol(&s)) {
+        if (scanner_chr(&s, '%')) {
+            if (scanner_chr(&s, '%')) {
+                scanner_buf_append(&s, '%');
+            }
+            else if (scanner_chr(&s, 'H')) {
+                const char *home = xkb_context_getenv(m->ctx, "HOME");
+                if (!home) {
+                    scanner_err(&s, "%%H was used in an include statement, but the HOME environment variable is not set");
+                    return;
+                }
+                if (!scanner_buf_appends(&s, home)) {
+                    scanner_err(&s, "include path after expanding %%H is too long");
+                    return;
+                }
+            }
+            else if (scanner_chr(&s, 'S')) {
+                const char *default_root = xkb_context_include_path_get_system_path(m->ctx);
+                if (!scanner_buf_appends(&s, default_root) || !scanner_buf_appends(&s, "/rules")) {
+                    scanner_err(&s, "include path after expanding %%S is too long");
+                    return;
+                }
+            }
+            else if (scanner_chr(&s, 'E')) {
+                const char *default_root = xkb_context_include_path_get_extra_path(m->ctx);
+                if (!scanner_buf_appends(&s, default_root) || !scanner_buf_appends(&s, "/rules")) {
+                    scanner_err(&s, "include path after expanding %%E is too long");
+                    return;
+                }
+            }
+            else {
+                scanner_err(&s, "unknown %% format (%c) in include statement", scanner_peek(&s));
+                return;
+            }
+        }
+        else {
+            scanner_buf_append(&s, scanner_next(&s));
+        }
+    }
+    if (!scanner_buf_append(&s, '\0')) {
+        scanner_err(&s, "include path is too long");
         return;
     }
 
-    if ((present & COMPONENT_MASK) == 0) {
-        log_warn(ctx,
-                 "Mapping needs at least one component; "
-                 "Illegal mapping ignored\n");
-        mapping->num_maps = 0;
-        return;
+    file = fopen(s.buf, "rb");
+    if (file) {
+        bool ret = read_rules_file(m->ctx, m, include_depth + 1, file, s.buf);
+        if (!ret)
+            log_err(m->ctx, XKB_LOG_MESSAGE_NO_ID,
+                    "No components returned from included XKB rules \"%s\"\n",
+                    s.buf);
+        fclose(file);
+    } else {
+        log_err(m->ctx, XKB_LOG_MESSAGE_NO_ID,
+                "Failed to open included XKB rules \"%s\"\n",
+                s.buf);
     }
+}
 
-    mapping->number++;
+static void
+matcher_mapping_start_new(struct matcher *m)
+{
+    for (unsigned i = 0; i < _MLVO_NUM_ENTRIES; i++)
+        m->mapping.mlvo_at_pos[i] = -1;
+    for (unsigned i = 0; i < _KCCGST_NUM_ENTRIES; i++)
+        m->mapping.kccgst_at_pos[i] = -1;
+    m->mapping.layout_idx = m->mapping.variant_idx = XKB_LAYOUT_INVALID;
+    m->mapping.num_mlvo = m->mapping.num_kccgst = 0;
+    m->mapping.defined_mlvo_mask = 0;
+    m->mapping.defined_kccgst_mask = 0;
+    m->mapping.skip = false;
 }
 
-/*
- * Match a line such as:
- * ! $pcmodels = pc101 pc102 pc104 pc105
- */
-static bool
-match_group_line(darray_char *line, struct group *group)
+static int
+extract_layout_index(const char *s, size_t max_len, xkb_layout_index_t *out)
 {
-    int i;
-    char *name = strchr(darray_mem(*line, 0), '$');
-    char *words = strchr(name, ' ');
+    /* This function is pretty stupid, but works for now. */
+    *out = XKB_LAYOUT_INVALID;
+    if (max_len < 3)
+        return -1;
+    if (s[0] != '[' || !is_digit(s[1]) || s[2] != ']')
+        return -1;
+    if (s[1] - '0' < 1 || s[1] - '0' > XKB_MAX_GROUPS)
+        return -1;
+    /* To zero-based index. */
+    *out = s[1] - '0' - 1;
+    return 3;
+}
 
-    if (!words)
-        return false;
+static void
+matcher_mapping_set_mlvo(struct matcher *m, struct scanner *s,
+                         struct sval ident)
+{
+    enum rules_mlvo mlvo;
+    struct sval mlvo_sval;
 
-    *words++ = '\0';
+    for (mlvo = 0; mlvo < _MLVO_NUM_ENTRIES; mlvo++) {
+        mlvo_sval = rules_mlvo_svals[mlvo];
 
-    for (; *words; words++) {
-        if (*words != '=' && *words != ' ')
+        if (svaleq_prefix(mlvo_sval, ident))
             break;
     }
 
-    if (*words == '\0')
-        return false;
-
-    group->name = strdup(name);
-    group->words = strdup(words);
-
-    words = group->words;
-    for (i = 1; *words; words++) {
-        if (*words == ' ') {
-            *words++ = '\0';
-            i++;
-        }
+    /* Not found. */
+    if (mlvo >= _MLVO_NUM_ENTRIES) {
+        scanner_err(s, "invalid mapping: %.*s is not a valid value here; ignoring rule set",
+                    ident.len, ident.start);
+        m->mapping.skip = true;
+        return;
     }
-    group->number = i;
 
-    return true;
-
-}
-
-/* Match lines following a mapping (see match_mapping_line comment). */
-static bool
-match_rule_line(struct xkb_context *ctx, darray_char *line,
-                struct mapping *mapping, struct rule *rule)
-{
-    char *str, *tok;
-    int nread, i;
-    char *strtok_buf;
-    bool append = false;
-    const char *names[MAX_WORDS] = { NULL };
-
-    if (mapping->num_maps == 0) {
-        log_warn(ctx,
-                 "Must have a mapping before first line of data; "
-                 "Illegal line of data ignored\n");
-        return false;
+    if (m->mapping.defined_mlvo_mask & (1u << mlvo)) {
+        scanner_err(s, "invalid mapping: %.*s appears twice on the same line; ignoring rule set",
+                    mlvo_sval.len, mlvo_sval.start);
+        m->mapping.skip = true;
+        return;
     }
 
-    str = darray_mem(*line, 0);
-
-    for (nread = 0; (tok = strtok_r(str, " ", &strtok_buf)) != NULL;
-         nread++) {
-        str = NULL;
-
-        if (strcmp(tok, "=") == 0) {
-            nread--;
-            continue;
+    /* If there are leftovers still, it must be an index. */
+    if (mlvo_sval.len < ident.len) {
+        xkb_layout_index_t idx;
+        int consumed = extract_layout_index(ident.start + mlvo_sval.len,
+                                            ident.len - mlvo_sval.len, &idx);
+        if ((int) (ident.len - mlvo_sval.len) != consumed) {
+            scanner_err(s, "invalid mapping: \"%.*s\" may only be followed by a valid group index; ignoring rule set",
+                        mlvo_sval.len, mlvo_sval.start);
+            m->mapping.skip = true;
+            return;
         }
 
-        if (nread > mapping->num_maps) {
-            log_warn(ctx,
-                     "Too many words on a line; "
-                     "Extra word \"%s\" ignored\n", tok);
-            continue;
+        if (mlvo == MLVO_LAYOUT) {
+            m->mapping.layout_idx = idx;
         }
-
-        names[mapping->map[nread].word] = tok;
-        if (*tok == '+' || *tok == '|')
-            append = true;
-    }
-
-    if (nread < mapping->num_maps) {
-        log_warn(ctx, "Too few words on a line: %s; Line ignored\n",
-                 darray_mem(*line, 0));
-        return false;
-    }
-
-    rule->flags = 0;
-    rule->number = mapping->number;
-
-    if (names[OPTION])
-        rule->flags |= RULE_FLAG_OPTION;
-    else if (append)
-        rule->flags |= RULE_FLAG_APPEND;
-    else
-        rule->flags |= RULE_FLAG_NORMAL;
-
-    rule->model = uDupString(names[MODEL]);
-    rule->layout = uDupString(names[LAYOUT]);
-    rule->variant = uDupString(names[VARIANT]);
-    rule->option = uDupString(names[OPTION]);
-
-    rule->keycodes = uDupString(names[KEYCODES]);
-    rule->symbols = uDupString(names[SYMBOLS]);
-    rule->types = uDupString(names[TYPES]);
-    rule->compat = uDupString(names[COMPAT]);
-
-    rule->layout_num = rule->variant_num = 0;
-    for (i = 0; i < nread; i++) {
-        if (mapping->map[i].index) {
-            if (mapping->map[i].word == LAYOUT)
-                rule->layout_num = mapping->map[i].index;
-            if (mapping->map[i].word == VARIANT)
-                rule->variant_num = mapping->map[i].index;
+        else if (mlvo == MLVO_VARIANT) {
+            m->mapping.variant_idx = idx;
+        }
+        else {
+            scanner_err(s, "invalid mapping: \"%.*s\" cannot be followed by a group index; ignoring rule set",
+                        mlvo_sval.len, mlvo_sval.start);
+            m->mapping.skip = true;
+            return;
         }
     }
 
-    return true;
+    m->mapping.mlvo_at_pos[m->mapping.num_mlvo] = mlvo;
+    m->mapping.defined_mlvo_mask |= 1u << mlvo;
+    m->mapping.num_mlvo++;
 }
 
-static bool
-match_line(struct xkb_context *ctx, darray_char *line,
-           struct mapping *mapping, struct rule *rule,
-           struct group *group)
+static void
+matcher_mapping_set_kccgst(struct matcher *m, struct scanner *s, struct sval ident)
 {
-    if (darray_item(*line, 0) != '!')
-        return match_rule_line(ctx, line, mapping, rule);
+    enum rules_kccgst kccgst;
+    struct sval kccgst_sval;
 
-    if (darray_item(*line, 1) == '$' ||
-        (darray_item(*line, 1) == ' ' && darray_item(*line, 2) == '$'))
-        return match_group_line(line, group);
+    for (kccgst = 0; kccgst < _KCCGST_NUM_ENTRIES; kccgst++) {
+        kccgst_sval = rules_kccgst_svals[kccgst];
 
-    match_mapping_line(ctx, line, mapping);
-    return false;
-}
+        if (svaleq(rules_kccgst_svals[kccgst], ident))
+            break;
+    }
 
-static void
-squeeze_spaces(char *p1)
-{
-    char *p2;
+    /* Not found. */
+    if (kccgst >= _KCCGST_NUM_ENTRIES) {
+        scanner_err(s, "invalid mapping: %.*s is not a valid value here; ignoring rule set",
+                    ident.len, ident.start);
+        m->mapping.skip = true;
+        return;
+    }
 
-    for (p2 = p1; *p2; p2++) {
-        *p1 = *p2;
-        if (*p1 != ' ')
-            p1++;
+    if (m->mapping.defined_kccgst_mask & (1u << kccgst)) {
+        scanner_err(s, "invalid mapping: %.*s appears twice on the same line; ignoring rule set",
+                    kccgst_sval.len, kccgst_sval.start);
+        m->mapping.skip = true;
+        return;
     }
 
-    *p1 = '\0';
+    m->mapping.kccgst_at_pos[m->mapping.num_kccgst] = kccgst;
+    m->mapping.defined_kccgst_mask |= 1u << kccgst;
+    m->mapping.num_kccgst++;
 }
 
-/*
- * Expand the layout and variant of the rule_names and remove extraneous
- * spaces. If there's one layout/variant, it is kept in
- * .layout[0]/.variant[0], else is kept in [1], [2] and so on, and [0]
- * remains empty. For example, this rule_names:
- *      .model  = "pc105",
- *      .layout = "us,il,ru,ca"
- *      .variant = ",,,multix"
- *      .options = "grp:alts_toggle,   ctrl:nocaps,  compose:rwin"
- * Is expanded into this multi_defs:
- *      .model = "pc105"
- *      .layout = {NULL, "us", "il", "ru", "ca"},
- *      .variant = {NULL, "", "", "", "multix"},
- *      .options = "grp:alts_toggle,ctrl:nocaps,compose:rwin"
- */
-static bool
-make_multi_defs(struct multi_defs *mdefs, const struct xkb_rule_names *mlvo)
+static void
+matcher_mapping_verify(struct matcher *m, struct scanner *s)
 {
-    char *p;
-    int i;
-
-    memset(mdefs, 0, sizeof(*mdefs));
-
-    if (mlvo->model) {
-        mdefs->model = mlvo->model;
+    if (m->mapping.num_mlvo == 0) {
+        scanner_err(s, "invalid mapping: must have at least one value on the left hand side; ignoring rule set");
+        goto skip;
     }
 
-    if (mlvo->options) {
-        mdefs->options = strdup(mlvo->options);
-        if (mdefs->options == NULL)
-            return false;
-
-        squeeze_spaces(mdefs->options);
+    if (m->mapping.num_kccgst == 0) {
+        scanner_err(s, "invalid mapping: must have at least one value on the right hand side; ignoring rule set");
+        goto skip;
     }
 
-    if (mlvo->layout) {
-        if (!strchr(mlvo->layout, ',')) {
-            mdefs->layout[0] = mlvo->layout;
+    /*
+     * This following is very stupid, but this is how it works.
+     * See the "Notes" section in the overview above.
+     */
+
+    if (m->mapping.defined_mlvo_mask & (1u << MLVO_LAYOUT)) {
+        if (m->mapping.layout_idx == XKB_LAYOUT_INVALID) {
+            if (darray_size(m->rmlvo.layouts) > 1)
+                goto skip;
         }
         else {
-            p = strdup(mlvo->layout);
-            if (p == NULL)
-                return false;
-
-            squeeze_spaces(p);
-            mdefs->layout[1] = p;
-
-            for (i = 2; i <= XkbNumKbdGroups; i++) {
-                if ((p = strchr(p, ','))) {
-                    *p++ = '\0';
-                    mdefs->layout[i] = p;
-                }
-                else {
-                    break;
-                }
-            }
-
-            if (p && (p = strchr(p, ',')))
-                *p = '\0';
+            if (darray_size(m->rmlvo.layouts) == 1 ||
+                m->mapping.layout_idx >= darray_size(m->rmlvo.layouts))
+                goto skip;
         }
     }
 
-    if (mlvo->variant) {
-        if (!strchr(mlvo->variant, ',')) {
-            mdefs->variant[0] = mlvo->variant;
+    if (m->mapping.defined_mlvo_mask & (1u << MLVO_VARIANT)) {
+        if (m->mapping.variant_idx == XKB_LAYOUT_INVALID) {
+            if (darray_size(m->rmlvo.variants) > 1)
+                goto skip;
         }
         else {
-            p = strdup(mlvo->variant);
-            if (p == NULL)
-                return false;
-
-            squeeze_spaces(p);
-            mdefs->variant[1] = p;
-
-            for (i = 2; i <= XkbNumKbdGroups; i++) {
-                if ((p = strchr(p, ','))) {
-                    *p++ = '\0';
-                    mdefs->variant[i] = p;
-                }
-                else {
-                    break;
-                }
-            }
-
-            if (p && (p = strchr(p, ',')))
-                *p = '\0';
+            if (darray_size(m->rmlvo.variants) == 1 ||
+                m->mapping.variant_idx >= darray_size(m->rmlvo.variants))
+                goto skip;
         }
     }
 
-    return true;
+    return;
+
+skip:
+    m->mapping.skip = true;
 }
 
 static void
-free_multi_defs(struct multi_defs *defs)
+matcher_rule_start_new(struct matcher *m)
 {
-    free(defs->options);
-    /*
-     * See make_multi_defs comment for the hack; the same strdup'd
-     * string is split among the indexes, but the one in [0] is const.
-     */
-    free(UNCONSTIFY(defs->layout[1]));
-    free(UNCONSTIFY(defs->variant[1]));
+    memset(&m->rule, 0, sizeof(m->rule));
+    m->rule.skip = m->mapping.skip;
 }
 
-/* See apply_rule below. */
 static void
-apply(const char *src, char **dst)
+matcher_rule_set_mlvo_common(struct matcher *m, struct scanner *s,
+                             struct sval ident,
+                             enum mlvo_match_type match_type)
 {
-    int ret;
-    char *tmp;
-
-    if (!src)
+    if (m->rule.num_mlvo_values + 1 > m->mapping.num_mlvo) {
+        scanner_err(s, "invalid rule: has more values than the mapping line; ignoring rule");
+        m->rule.skip = true;
         return;
-
-    if (*src == '+' || *src == '!') {
-        tmp = *dst;
-        ret = asprintf(dst, "%s%s", *dst, src);
-        if (ret < 0)
-            *dst = NULL;
-        free(tmp);
-    }
-    else if (*dst == NULL) {
-        *dst = strdup(src);
     }
+    m->rule.match_type_at_pos[m->rule.num_mlvo_values] = match_type;
+    m->rule.mlvo_value_at_pos[m->rule.num_mlvo_values] = ident;
+    m->rule.num_mlvo_values++;
 }
 
-/*
- * Add the info from the matching rule to the resulting
- * xkb_component_names. If we already had a match for something
- * (e.g. keycodes), and the rule is not an appending one (e.g.
- * +whatever), than we don't override but drop the new one.
- */
 static void
-apply_rule(struct rule *rule, struct xkb_component_names *kccgst)
+matcher_rule_set_mlvo_wildcard(struct matcher *m, struct scanner *s)
 {
-    /* Clear the flag because it's applied. */
-    rule->flags &= ~RULE_FLAG_PENDING_MATCH;
+    struct sval dummy = { NULL, 0 };
+    matcher_rule_set_mlvo_common(m, s, dummy, MLVO_MATCH_WILDCARD);
+}
 
-    apply(rule->keycodes, &kccgst->keycodes);
-    apply(rule->symbols, &kccgst->symbols);
-    apply(rule->types, &kccgst->types);
-    apply(rule->compat, &kccgst->compat);
+static void
+matcher_rule_set_mlvo_group(struct matcher *m, struct scanner *s,
+                            struct sval ident)
+{
+    matcher_rule_set_mlvo_common(m, s, ident, MLVO_MATCH_GROUP);
+}
+
+static void
+matcher_rule_set_mlvo(struct matcher *m, struct scanner *s,
+                      struct sval ident)
+{
+    matcher_rule_set_mlvo_common(m, s, ident, MLVO_MATCH_NORMAL);
+}
+
+static void
+matcher_rule_set_kccgst(struct matcher *m, struct scanner *s,
+                        struct sval ident)
+{
+    if (m->rule.num_kccgst_values + 1 > m->mapping.num_kccgst) {
+        scanner_err(s, "invalid rule: has more values than the mapping line; ignoring rule");
+        m->rule.skip = true;
+        return;
+    }
+    m->rule.kccgst_value_at_pos[m->rule.num_kccgst_values] = ident;
+    m->rule.num_kccgst_values++;
 }
 
-/*
- * Match if name is part of the group, e.g. if the following
- * group is defined:
- *      ! $qwertz = al cz de hr hu ro si sk
- * then
- *      match_group_member(rules, "qwertz", "hr")
- * will return true.
- */
 static bool
-match_group_member(struct rules *rules, const char *group_name,
-                   const char *name)
+match_group(struct matcher *m, struct sval group_name, struct sval to)
 {
-    int i;
-    const char *word;
-    struct group *iter, *group = NULL;
+    struct group *group;
+    struct sval *element;
+    bool found = false;
 
-    darray_foreach(iter, rules->groups) {
-        if (strcmp(iter->name, group_name) == 0) {
-            group = iter;
+    darray_foreach(group, m->groups) {
+        if (svaleq(group->name, group_name)) {
+            found = true;
             break;
         }
     }
 
-    if (!group)
+    if (!found) {
+        /*
+         * rules/evdev intentionally uses some undeclared group names
+         * in rules (e.g. commented group definitions which may be
+         * uncommented if needed). So we continue silently.
+         */
         return false;
+    }
 
-    word = group->words;
-    for (i = 0; i < group->number; i++, word += strlen(word) + 1)
-        if (strcmp(word, name) == 0)
+    darray_foreach(element, group->elements)
+        if (svaleq(to, *element))
             return true;
 
     return false;
 }
 
-/* Match @needle out of @sep-seperated @haystack. */
 static bool
-match_one_of(const char *haystack, const char *needle, char sep)
+match_value(struct matcher *m, struct sval val, struct sval to,
+            enum mlvo_match_type match_type)
 {
-    const char *s = haystack;
-    const size_t len = strlen(needle);
-
-    do {
-        if (strncmp(s, needle, len) == 0 && (s[len] == '\0' || s[len] == sep))
-            return true;
-        s = strchr(s, sep);
-    } while (s++);
-
-    return false;
+    if (match_type == MLVO_MATCH_WILDCARD)
+        return true;
+    if (match_type == MLVO_MATCH_GROUP)
+        return match_group(m, val, to);
+    return svaleq(val, to);
 }
 
-static int
-apply_rule_if_matches(struct rules *rules, struct rule *rule,
-                      struct multi_defs *mdefs,
-                      struct xkb_component_names *kccgst)
+static bool
+match_value_and_mark(struct matcher *m, struct sval val,
+                     struct matched_sval *to, enum mlvo_match_type match_type)
 {
-    bool pending = false;
+    bool matched = match_value(m, val, to->sval, match_type);
+    if (matched)
+        to->matched = true;
+    return matched;
+}
 
-    if (rule->model) {
-        if (mdefs->model == NULL)
-            return 0;
+/*
+ * This function performs %-expansion on @value (see overview above),
+ * and appends the result to @to.
+ */
+static bool
+append_expanded_kccgst_value(struct matcher *m, struct scanner *s,
+                             darray_char *to, struct sval value)
+{
+    const char *str = value.start;
+    darray_char expanded = darray_new();
+    char ch;
+    bool expanded_plus, to_plus;
 
-        if (strcmp(rule->model, "*") == 0) {
-            pending = true;
-        }
-        else if (rule->model[0] == '$') {
-            if (!match_group_member(rules, rule->model, mdefs->model))
-                return 0;
+    /*
+     * Some ugly hand-lexing here, but going through the scanner is more
+     * trouble than it's worth, and the format is ugly on its own merit.
+     */
+    for (unsigned i = 0; i < value.len; ) {
+        enum rules_mlvo mlv;
+        xkb_layout_index_t idx;
+        char pfx, sfx;
+        struct matched_sval *expanded_value;
+
+        /* Check if that's a start of an expansion. */
+        if (str[i] != '%') {
+            /* Just a normal character. */
+            darray_appends_nullterminate(expanded, &str[i++], 1);
+            continue;
         }
-        else if (strcmp(rule->model, mdefs->model) != 0) {
-            return 0;
+        if (++i >= value.len) goto error;
+
+        pfx = sfx = 0;
+
+        /* Check for prefix. */
+        if (str[i] == '(' || str[i] == '+' || str[i] == '|' ||
+            str[i] == '_' || str[i] == '-') {
+            pfx = str[i];
+            if (str[i] == '(') sfx = ')';
+            if (++i >= value.len) goto error;
         }
-    }
 
-    if (rule->option) {
-        if (mdefs->options == NULL)
-            return 0;
+        /* Mandatory model/layout/variant specifier. */
+        switch (str[i++]) {
+        case 'm': mlv = MLVO_MODEL; break;
+        case 'l': mlv = MLVO_LAYOUT; break;
+        case 'v': mlv = MLVO_VARIANT; break;
+        default: goto error;
+        }
 
-        if (!match_one_of(mdefs->options, rule->option, ','))
-            return 0;
-    }
+        /* Check for index. */
+        idx = XKB_LAYOUT_INVALID;
+        if (i < value.len && str[i] == '[') {
+            int consumed;
 
-    if (rule->layout) {
-        if (mdefs->layout[rule->layout_num] == NULL)
-            return 0;
+            if (mlv != MLVO_LAYOUT && mlv != MLVO_VARIANT) {
+                scanner_err(s, "invalid index in %%-expansion; may only index layout or variant");
+                goto error;
+            }
 
-        if (strcmp(rule->layout, "*") == 0) {
-            pending = true;
-        }
-        else if (rule->layout[0] == '$') {
-            if (!match_group_member(rules, rule->layout,
-                                    mdefs->layout[rule->layout_num]))
-                return 0;
+            consumed = extract_layout_index(str + i, value.len - i, &idx);
+            if (consumed == -1) goto error;
+            i += consumed;
         }
-        else if (strcmp(rule->layout,
-                        mdefs->layout[rule->layout_num]) != 0) {
-            return 0;
-        }
-    }
 
-    if (rule->variant) {
-        if (mdefs->variant[rule->variant_num] == NULL)
-            return 0;
+        /* Check for suffix, if there supposed to be one. */
+        if (sfx != 0) {
+            if (i >= value.len) goto error;
+            if (str[i++] != sfx) goto error;
+        }
 
-        if (strcmp(rule->variant, "*") == 0) {
-            pending = true;
+        /* Get the expanded value. */
+        expanded_value = NULL;
+
+        if (mlv == MLVO_LAYOUT) {
+            if (idx != XKB_LAYOUT_INVALID &&
+                idx < darray_size(m->rmlvo.layouts) &&
+                darray_size(m->rmlvo.layouts) > 1)
+                expanded_value = &darray_item(m->rmlvo.layouts, idx);
+            else if (idx == XKB_LAYOUT_INVALID &&
+                     darray_size(m->rmlvo.layouts) == 1)
+                expanded_value = &darray_item(m->rmlvo.layouts, 0);
         }
-        else if (rule->variant[0] == '$') {
-            if (!match_group_member(rules, rule->variant,
-                                    mdefs->variant[rule->variant_num]))
-                return 0;
+        else if (mlv == MLVO_VARIANT) {
+            if (idx != XKB_LAYOUT_INVALID &&
+                idx < darray_size(m->rmlvo.variants) &&
+                darray_size(m->rmlvo.variants) > 1)
+                expanded_value = &darray_item(m->rmlvo.variants, idx);
+            else if (idx == XKB_LAYOUT_INVALID &&
+                     darray_size(m->rmlvo.variants) == 1)
+                expanded_value = &darray_item(m->rmlvo.variants, 0);
         }
-        else if (strcmp(rule->variant,
-                        mdefs->variant[rule->variant_num]) != 0) {
-            return 0;
+        else if (mlv == MLVO_MODEL) {
+            expanded_value = &m->rmlvo.model;
         }
-    }
 
-    if (pending) {
-        rule->flags |= RULE_FLAG_PENDING_MATCH;
-    }
-    else {
-        /* Exact match, apply it now. */
-        apply_rule(rule, kccgst);
+        /* If we didn't get one, skip silently. */
+        if (!expanded_value || expanded_value->sval.len == 0)
+            continue;
+
+        if (pfx != 0)
+            darray_appends_nullterminate(expanded, &pfx, 1);
+        darray_appends_nullterminate(expanded,
+                                     expanded_value->sval.start,
+                                     expanded_value->sval.len);
+        if (sfx != 0)
+            darray_appends_nullterminate(expanded, &sfx, 1);
+        expanded_value->matched = true;
     }
 
-    return rule->number;
-}
+    /*
+     * Appending  bar to  foo ->  foo (not an error if this happens)
+     * Appending +bar to  foo ->  foo+bar
+     * Appending  bar to +foo ->  bar+foo
+     * Appending +bar to +foo -> +foo+bar
+     */
 
-static void
-clear_partial_matches(struct rules *rules)
-{
-    struct rule *rule;
+    ch = (darray_empty(expanded) ? '\0' : darray_item(expanded, 0));
+    expanded_plus = (ch == '+' || ch == '|');
+    ch = (darray_empty(*to) ? '\0' : darray_item(*to, 0));
+    to_plus = (ch == '+' || ch == '|');
 
-    darray_foreach(rule, rules->rules)
-        rule->flags &= ~RULE_FLAG_PENDING_MATCH;
-}
+    if (expanded_plus || darray_empty(*to))
+        darray_appends_nullterminate(*to, expanded.item, expanded.size);
+    else if (to_plus)
+        darray_prepends_nullterminate(*to, expanded.item, expanded.size);
 
-static void
-apply_partial_matches(struct rules *rules, struct xkb_component_names *kccgst)
-{
-    struct rule *rule;
+    darray_free(expanded);
+    return true;
 
-    darray_foreach(rule, rules->rules)
-        if (rule->flags & RULE_FLAG_PENDING_MATCH)
-            apply_rule(rule, kccgst);
+error:
+    darray_free(expanded);
+    scanner_err(s, "invalid %%-expansion in value; not used");
+    return false;
 }
 
 static void
-apply_matching_rules(struct rules *rules, struct multi_defs *mdefs,
-                     struct xkb_component_names *kccgst, unsigned int flags)
+matcher_rule_verify(struct matcher *m, struct scanner *s)
 {
-    int skip = -1;
-    struct rule *rule;
-
-    darray_foreach(rule, rules->rules) {
-        if ((rule->flags & flags) != flags)
-            continue;
-
-        if ((flags & RULE_FLAG_OPTION) == 0 && rule->number == skip)
-            continue;
-
-        skip = apply_rule_if_matches(rules, rule, mdefs, kccgst);
+    if (m->rule.num_mlvo_values != m->mapping.num_mlvo ||
+        m->rule.num_kccgst_values != m->mapping.num_kccgst) {
+        scanner_err(s, "invalid rule: must have same number of values as mapping line; ignoring rule");
+        m->rule.skip = true;
     }
 }
 
-/***====================================================================***/
-
-static char *
-substitute_vars(char *name, struct multi_defs *mdefs)
+static void
+matcher_rule_apply_if_matches(struct matcher *m, struct scanner *s)
 {
-    char *str, *outstr, *var;
-    char *orig = name;
-    size_t len, extra_len;
-    char pfx, sfx;
-    int ndx;
-
-    if (!name)
-        return NULL;
-
-    str = strchr(name, '%');
-    if (str == NULL)
-        return name;
-
-    len = strlen(name);
-
-    while (str != NULL) {
-        pfx = str[1];
-        extra_len = 0;
-
-        if (pfx == '+' || pfx == '|' || pfx == '_' || pfx == '-') {
-            extra_len = 1;
-            str++;
+    for (unsigned i = 0; i < m->mapping.num_mlvo; i++) {
+        enum rules_mlvo mlvo = m->mapping.mlvo_at_pos[i];
+        struct sval value = m->rule.mlvo_value_at_pos[i];
+        enum mlvo_match_type match_type = m->rule.match_type_at_pos[i];
+        struct matched_sval *to;
+        bool matched = false;
+
+        if (mlvo == MLVO_MODEL) {
+            to = &m->rmlvo.model;
+            matched = match_value_and_mark(m, value, to, match_type);
         }
-        else if (pfx == '(') {
-            extra_len = 2;
-            str++;
+        else if (mlvo == MLVO_LAYOUT) {
+            xkb_layout_index_t idx = m->mapping.layout_idx;
+            idx = (idx == XKB_LAYOUT_INVALID ? 0 : idx);
+            to = &darray_item(m->rmlvo.layouts, idx);
+            matched = match_value_and_mark(m, value, to, match_type);
         }
-
-        var = str + 1;
-        str = get_index(var + 1, &ndx);
-        if (ndx == -1) {
-            str = strchr(str, '%');
-            continue;
+        else if (mlvo == MLVO_VARIANT) {
+            xkb_layout_index_t idx = m->mapping.layout_idx;
+            idx = (idx == XKB_LAYOUT_INVALID ? 0 : idx);
+            to = &darray_item(m->rmlvo.variants, idx);
+            matched = match_value_and_mark(m, value, to, match_type);
         }
-
-        if (*var == 'l' && mdefs->layout[ndx] && *mdefs->layout[ndx])
-            len += strlen(mdefs->layout[ndx]) + extra_len;
-        else if (*var == 'm' && mdefs->model)
-            len += strlen(mdefs->model) + extra_len;
-        else if (*var == 'v' && mdefs->variant[ndx] && *mdefs->variant[ndx])
-            len += strlen(mdefs->variant[ndx]) + extra_len;
-
-        if (pfx == '(' && *str == ')')
-            str++;
-
-        str = strchr(&str[0], '%');
-    }
-
-    name = malloc(len + 1);
-    str = orig;
-    outstr = name;
-
-    while (*str != '\0') {
-        if (str[0] == '%') {
-            str++;
-            pfx = str[0];
-            sfx = '\0';
-
-            if (pfx == '+' || pfx == '|' || pfx == '_' || pfx == '-') {
-                str++;
-            }
-            else if (pfx == '(') {
-                sfx = ')';
-                str++;
-            }
-            else {
-                pfx = '\0';
-            }
-
-            var = str;
-            str = get_index(var + 1, &ndx);
-            if (ndx == -1)
-                continue;
-
-            if (*var == 'l' && mdefs->layout[ndx] && *mdefs->layout[ndx]) {
-                if (pfx)
-                    *outstr++ = pfx;
-
-                strcpy(outstr, mdefs->layout[ndx]);
-                outstr += strlen(mdefs->layout[ndx]);
-
-                if (sfx)
-                    *outstr++ = sfx;
-            }
-            else if (*var == 'm' && mdefs->model) {
-                if (pfx)
-                    *outstr++ = pfx;
-
-                strcpy(outstr, mdefs->model);
-                outstr += strlen(mdefs->model);
-
-                if (sfx)
-                    *outstr++ = sfx;
-            }
-            else if (*var == 'v' && mdefs->variant[ndx] &&
-                     *mdefs->variant[ndx]) {
-                if (pfx)
-                    *outstr++ = pfx;
-
-                strcpy(outstr, mdefs->variant[ndx]);
-                outstr += strlen(mdefs->variant[ndx]);
-
-                if (sfx)
-                    *outstr++ = sfx;
+        else if (mlvo == MLVO_OPTION) {
+            darray_foreach(to, m->rmlvo.options) {
+                matched = match_value_and_mark(m, value, to, match_type);
+                if (matched)
+                    break;
             }
-
-            if (pfx == '(' && *str == ')')
-                str++;
-        }
-        else {
-            *outstr++ = *str++;
         }
-    }
 
-    *outstr++ = '\0';
+        if (!matched)
+            return;
+    }
 
-    if (orig != name)
-        free(orig);
+    for (unsigned i = 0; i < m->mapping.num_kccgst; i++) {
+        enum rules_kccgst kccgst = m->mapping.kccgst_at_pos[i];
+        struct sval value = m->rule.kccgst_value_at_pos[i];
+        append_expanded_kccgst_value(m, s, &m->kccgst[kccgst], value);
+    }
 
-    return name;
+    /*
+     * If a rule matches in a rule set, the rest of the set should be
+     * skipped. However, rule sets matching against options may contain
+     * several legitimate rules, so they are processed entirely.
+     */
+    if (!(m->mapping.defined_mlvo_mask & (1 << MLVO_OPTION)))
+        m->mapping.skip = true;
 }
 
-/***====================================================================***/
+static enum rules_token
+gettok(struct matcher *m, struct scanner *s)
+{
+    return lex(s, &m->val);
+}
 
 static bool
-get_components(struct rules *rules, const struct xkb_rule_names *mlvo,
-               struct xkb_component_names *kccgst)
+matcher_match(struct matcher *m, struct scanner *s,
+              unsigned include_depth,
+              const char *string, size_t len,
+              const char *file_name)
 {
-    struct multi_defs mdefs;
+    enum rules_token tok;
 
-    make_multi_defs(&mdefs, mlvo);
+    if (!m)
+        return false;
 
-    clear_partial_matches(rules);
+initial:
+    switch (tok = gettok(m, s)) {
+    case TOK_BANG:
+        goto bang;
+    case TOK_END_OF_LINE:
+        goto initial;
+    case TOK_END_OF_FILE:
+        goto finish;
+    default:
+        goto unexpected;
+    }
 
-    apply_matching_rules(rules, &mdefs, kccgst, RULE_FLAG_NORMAL);
-    apply_partial_matches(rules, kccgst);
+bang:
+    switch (tok = gettok(m, s)) {
+    case TOK_GROUP_NAME:
+        matcher_group_start_new(m, m->val.string);
+        goto group_name;
+    case TOK_INCLUDE:
+        goto include_statement;
+    case TOK_IDENTIFIER:
+        matcher_mapping_start_new(m);
+        matcher_mapping_set_mlvo(m, s, m->val.string);
+        goto mapping_mlvo;
+    default:
+        goto unexpected;
+    }
 
-    apply_matching_rules(rules, &mdefs, kccgst, RULE_FLAG_APPEND);
-    apply_partial_matches(rules, kccgst);
+group_name:
+    switch (tok = gettok(m, s)) {
+    case TOK_EQUALS:
+        goto group_element;
+    default:
+        goto unexpected;
+    }
 
-    apply_matching_rules(rules, &mdefs, kccgst, RULE_FLAG_OPTION);
-    apply_partial_matches(rules, kccgst);
+group_element:
+    switch (tok = gettok(m, s)) {
+    case TOK_IDENTIFIER:
+        matcher_group_add_element(m, s, m->val.string);
+        goto group_element;
+    case TOK_END_OF_LINE:
+        goto initial;
+    default:
+        goto unexpected;
+    }
 
-    kccgst->keycodes = substitute_vars(kccgst->keycodes, &mdefs);
-    kccgst->symbols = substitute_vars(kccgst->symbols, &mdefs);
-    kccgst->types = substitute_vars(kccgst->types, &mdefs);
-    kccgst->compat = substitute_vars(kccgst->compat, &mdefs);
+include_statement:
+    switch (tok = gettok(m, s)) {
+    case TOK_IDENTIFIER:
+        matcher_include(m, s, include_depth, m->val.string);
+        goto initial;
+    default:
+        goto unexpected;
+    }
 
-    free_multi_defs(&mdefs);
+mapping_mlvo:
+    switch (tok = gettok(m, s)) {
+    case TOK_IDENTIFIER:
+        if (!m->mapping.skip)
+            matcher_mapping_set_mlvo(m, s, m->val.string);
+        goto mapping_mlvo;
+    case TOK_EQUALS:
+        goto mapping_kccgst;
+    default:
+        goto unexpected;
+    }
 
-    return kccgst->keycodes && kccgst->symbols && kccgst->types &&
-           kccgst->compat;
-}
+mapping_kccgst:
+    switch (tok = gettok(m, s)) {
+    case TOK_IDENTIFIER:
+        if (!m->mapping.skip)
+            matcher_mapping_set_kccgst(m, s, m->val.string);
+        goto mapping_kccgst;
+    case TOK_END_OF_LINE:
+        if (!m->mapping.skip)
+            matcher_mapping_verify(m, s);
+        goto rule_mlvo_first;
+    default:
+        goto unexpected;
+    }
 
-static struct rules *
-load_rules(struct xkb_context *ctx, FILE *file)
-{
-    darray_char line;
-    struct mapping mapping;
-    struct rule trule;
-    struct group tgroup;
-    struct rules *rules;
+rule_mlvo_first:
+    switch (tok = gettok(m, s)) {
+    case TOK_BANG:
+        goto bang;
+    case TOK_END_OF_LINE:
+        goto rule_mlvo_first;
+    case TOK_END_OF_FILE:
+        goto finish;
+    default:
+        matcher_rule_start_new(m);
+        goto rule_mlvo_no_tok;
+    }
 
-    rules = calloc(1, sizeof(*rules));
-    if (!rules)
-        return NULL;
-    darray_init(rules->rules);
-    darray_growalloc(rules->rules, 16);
-
-    memset(&mapping, 0, sizeof(mapping));
-    memset(&tgroup, 0, sizeof(tgroup));
-    darray_init(line);
-    darray_growalloc(line, 128);
-
-    while (input_line_get(ctx, file, &line)) {
-        if (match_line(ctx, &line, &mapping, &trule, &tgroup)) {
-            if (tgroup.number) {
-                darray_append(rules->groups, tgroup);
-                memset(&tgroup, 0, sizeof(tgroup));
-            }
-            else {
-                darray_append(rules->rules, trule);
-                memset(&trule, 0, sizeof(trule));
-            }
-        }
+rule_mlvo:
+    tok = gettok(m, s);
+rule_mlvo_no_tok:
+    switch (tok) {
+    case TOK_IDENTIFIER:
+        if (!m->rule.skip)
+            matcher_rule_set_mlvo(m, s, m->val.string);
+        goto rule_mlvo;
+    case TOK_STAR:
+        if (!m->rule.skip)
+            matcher_rule_set_mlvo_wildcard(m, s);
+        goto rule_mlvo;
+    case TOK_GROUP_NAME:
+        if (!m->rule.skip)
+            matcher_rule_set_mlvo_group(m, s, m->val.string);
+        goto rule_mlvo;
+    case TOK_EQUALS:
+        goto rule_kccgst;
+    default:
+        goto unexpected;
+    }
 
-        darray_resize(line, 0);
+rule_kccgst:
+    switch (tok = gettok(m, s)) {
+    case TOK_IDENTIFIER:
+        if (!m->rule.skip)
+            matcher_rule_set_kccgst(m, s, m->val.string);
+        goto rule_kccgst;
+    case TOK_END_OF_LINE:
+        if (!m->rule.skip)
+            matcher_rule_verify(m, s);
+        if (!m->rule.skip)
+            matcher_rule_apply_if_matches(m, s);
+        goto rule_mlvo_first;
+    default:
+        goto unexpected;
     }
 
-    darray_free(line);
-    return rules;
-}
+unexpected:
+    switch (tok) {
+    case TOK_ERROR:
+        goto error;
+    default:
+        goto state_error;
+    }
 
-static void
-free_rules(struct rules *rules)
-{
-    struct rule *rule;
-    struct group *group;
+finish:
+    return true;
 
-    if (!rules)
-        return;
+state_error:
+    scanner_err(s, "unexpected token");
+error:
+    return false;
+}
 
-    darray_foreach(rule, rules->rules) {
-        free(rule->model);
-        free(rule->layout);
-        free(rule->variant);
-        free(rule->option);
-        free(rule->keycodes);
-        free(rule->symbols);
-        free(rule->types);
-        free(rule->compat);
+static bool
+read_rules_file(struct xkb_context *ctx,
+                struct matcher *matcher,
+                unsigned include_depth,
+                FILE *file,
+                const char *path)
+{
+    bool ret = false;
+    char *string;
+    size_t size;
+    struct scanner scanner;
+
+    ret = map_file(file, &string, &size);
+    if (!ret) {
+        log_err(ctx, XKB_LOG_MESSAGE_NO_ID,
+                "Couldn't read rules file \"%s\": %s\n",
+                path, strerror(errno));
+        goto out;
     }
-    darray_free(rules->rules);
 
-    darray_foreach(group, rules->groups) {
-        free(group->name);
-        free(group->words);
-    }
-    darray_free(rules->groups);
+    scanner_init(&scanner, matcher->ctx, string, size, path, NULL);
+
+    ret = matcher_match(matcher, &scanner, include_depth, string, size, path);
 
-    free(rules);
+    unmap_file(string, size);
+out:
+    return ret;
 }
 
-struct xkb_component_names *
+bool
 xkb_components_from_rules(struct xkb_context *ctx,
-                          const struct xkb_rule_names *rmlvo)
+                          const struct xkb_rule_names *rmlvo,
+                          struct xkb_component_names *out)
 {
-    int i;
+    bool ret = false;
     FILE *file;
-    char *path;
-    struct rules *rules;
-    struct xkb_component_names *kccgst = NULL;
-
-    file = XkbFindFileInPath(ctx, rmlvo->rules, FILE_TYPE_RULES, &path);
-    if (!file) {
-        log_err(ctx, "could not find \"%s\" rules in XKB path\n",
-                rmlvo->rules);
-        log_err(ctx, "%d include paths searched:\n",
-                xkb_context_num_include_paths(ctx));
-        for (i = 0; i < xkb_context_num_include_paths(ctx); i++)
-            log_err(ctx, "\t%s\n", xkb_context_include_path_get(ctx, i));
-        return NULL;
-    }
-
-    rules = load_rules(ctx, file);
-    if (!rules) {
-        log_err(ctx, "failed to load XKB rules \"%s\"\n", path);
-        goto err;
-    }
-
-    kccgst = calloc(1, sizeof(*kccgst));
-    if (!kccgst) {
-        log_err(ctx, "failed to allocate XKB components\n");
-        goto err;
-    }
-
-    if (!get_components(rules, rmlvo, kccgst)) {
-        free(kccgst->keycodes);
-        free(kccgst->types);
-        free(kccgst->compat);
-        free(kccgst->symbols);
-        free(kccgst);
-        kccgst = NULL;
-        log_err(ctx, "no components returned from XKB rules \"%s\"\n", path);
-        goto err;
+    char *path = NULL;
+    struct matcher *matcher = NULL;
+    struct matched_sval *mval;
+    unsigned int offset = 0;
+
+    file = FindFileInXkbPath(ctx, rmlvo->rules, FILE_TYPE_RULES, &path, &offset);
+    if (!file)
+        goto err_out;
+
+    matcher = matcher_new(ctx, rmlvo);
+
+    ret = read_rules_file(ctx, matcher, 0, file, path);
+    if (!ret ||
+        darray_empty(matcher->kccgst[KCCGST_KEYCODES]) ||
+        darray_empty(matcher->kccgst[KCCGST_TYPES]) ||
+        darray_empty(matcher->kccgst[KCCGST_COMPAT]) ||
+        /* darray_empty(matcher->kccgst[KCCGST_GEOMETRY]) || */
+        darray_empty(matcher->kccgst[KCCGST_SYMBOLS])) {
+        log_err(ctx, XKB_LOG_MESSAGE_NO_ID,
+                "No components returned from XKB rules \"%s\"\n", path);
+        ret = false;
+        goto err_out;
     }
 
-err:
-    free_rules(rules);
+    darray_steal(matcher->kccgst[KCCGST_KEYCODES], &out->keycodes, NULL);
+    darray_steal(matcher->kccgst[KCCGST_TYPES], &out->types, NULL);
+    darray_steal(matcher->kccgst[KCCGST_COMPAT], &out->compat, NULL);
+    darray_steal(matcher->kccgst[KCCGST_SYMBOLS], &out->symbols, NULL);
+    darray_free(matcher->kccgst[KCCGST_GEOMETRY]);
+
+    mval = &matcher->rmlvo.model;
+    if (!mval->matched && mval->sval.len > 0)
+        log_err(matcher->ctx, XKB_LOG_MESSAGE_NO_ID,
+                "Unrecognized RMLVO model \"%.*s\" was ignored\n",
+                mval->sval.len, mval->sval.start);
+    darray_foreach(mval, matcher->rmlvo.layouts)
+        if (!mval->matched && mval->sval.len > 0)
+            log_err(matcher->ctx, XKB_LOG_MESSAGE_NO_ID,
+                    "Unrecognized RMLVO layout \"%.*s\" was ignored\n",
+                    mval->sval.len, mval->sval.start);
+    darray_foreach(mval, matcher->rmlvo.variants)
+        if (!mval->matched && mval->sval.len > 0)
+            log_err(matcher->ctx, XKB_LOG_MESSAGE_NO_ID,
+                    "Unrecognized RMLVO variant \"%.*s\" was ignored\n",
+                    mval->sval.len, mval->sval.start);
+    darray_foreach(mval, matcher->rmlvo.options)
+        if (!mval->matched && mval->sval.len > 0)
+            log_err(matcher->ctx, XKB_LOG_MESSAGE_NO_ID,
+                    "Unrecognized RMLVO option \"%.*s\" was ignored\n",
+                    mval->sval.len, mval->sval.start);
+
+err_out:
     if (file)
         fclose(file);
+    matcher_free(matcher);
     free(path);
-    return kccgst;
+    return ret;
 }