Removed build dependency on xproto.
[platform/upstream/libxkbcommon.git] / src / xkbcomp / rules.c
index 244701d..f5d9c49 100644 (file)
 /************************************************************
- Copyright (c) 1996 by Silicon Graphics Computer Systems, Inc.
-
- Permission to use, copy, modify, and distribute this
- software and its documentation for any purpose and without
- fee is hereby granted, provided that the above copyright
- notice appear in all copies and that both that copyright
- notice and this permission notice appear in supporting
- documentation, and that the name of Silicon Graphics not be
- used in advertising or publicity pertaining to distribution
- of the software without specific prior written permission.
- Silicon Graphics makes no representation about the suitability
- of this software for any purpose. It is provided "as is"
- without any express or implied warranty.
-
- SILICON GRAPHICS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
- SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
- AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT SHALL SILICON
- GRAPHICS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL
- DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
- DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
- OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION  WITH
- THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
Copyright (c) 1996 by Silicon Graphics Computer Systems, Inc.
+ *
Permission to use, copy, modify, and distribute this
software and its documentation for any purpose and without
fee is hereby granted, provided that the above copyright
notice appear in all copies and that both that copyright
notice and this permission notice appear in supporting
documentation, and that the name of Silicon Graphics not be
used in advertising or publicity pertaining to distribution
of the software without specific prior written permission.
Silicon Graphics makes no representation about the suitability
of this software for any purpose. It is provided "as is"
without any express or implied warranty.
+ *
SILICON GRAPHICS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT SHALL SILICON
GRAPHICS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION  WITH
THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
  ********************************************************/
 
-#include <stdio.h>
-#include <ctype.h>
-
-#include "rules.h"
-#include "path.h"
-
-static bool
-input_line_get(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)) {
-                        WARN("The '!' is legal only at start of line\n");
-                        ACTION("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,
-    KEYMAP,
-
-#define COMPONENT_MASK \
-    ((1 << KEYCODES) | (1 << SYMBOLS) | (1 << TYPES) | (1 << COMPAT) | \
-     (1 << GEOMETRY) | (1 << KEYMAP))
-
-    MAX_WORDS
+enum rules_mlvo {
+    MLVO_MODEL,
+    MLVO_LAYOUT,
+    MLVO_VARIANT,
+    MLVO_OPTION,
+    _MLVO_NUM_ENTRIES
 };
 
-static const char *cname[] = {
-    [MODEL] = "model",
-    [LAYOUT] = "layout",
-    [VARIANT] = "variant",
-    [OPTION] = "option",
-
-    [KEYCODES] = "keycodes",
-    [SYMBOLS] = "symbols",
-    [TYPES] = "types",
-    [COMPAT] = "compat",
-    [GEOMETRY] = "geometry",
-    [KEYMAP] = "keymap",
+#define SVAL_LIT(literal) { literal, sizeof(literal) - 1 }
+
+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"),
 };
 
-struct multi_defs {
-    const char *model;
-    const char *layout[XkbNumKbdGroups + 1];
-    const char *variant[XkbNumKbdGroups + 1];
-    char *options;
+enum rules_kccgst {
+    KCCGST_KEYCODES,
+    KCCGST_TYPES,
+    KCCGST_COMPAT,
+    KCCGST_SYMBOLS,
+    KCCGST_GEOMETRY,
+    _KCCGST_NUM_ENTRIES
 };
 
-struct mapping {
-    /* Sequential id for the mappings. */
-    int number;
-    size_t num_maps;
-
-    struct {
-        int word;
-        int index;
-    } map[MAX_WORDS];
+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 var_desc {
-    char *name;
-    char *desc;
+/* 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 group {
-    int number;
-    char *name;
-    char *words;
+/*
+ * 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;
 };
 
-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 group {
+    struct sval name;
+    darray_sval elements;
 };
 
-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;
-    char *keymap;
-    unsigned flags;
+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;
 };
 
-struct rules {
-    darray(struct rule) rules;
-    darray(struct group) groups;
+enum mlvo_match_type {
+    MLVO_MATCH_NORMAL = 0,
+    MLVO_MATCH_WILDCARD,
+    MLVO_MATCH_GROUP,
 };
 
-/***====================================================================***/
+struct rule {
+    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;
+};
 
 /*
- * Resolve numeric index, such as "[4]" in layout[4]. Missing index
- * means zero.
+ * 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()).
  */
-static char *
-get_index(char *str, int *ndx)
-{
-    int empty = 0, consumed = 0, num;
-
-    sscanf(str, "[%n%d]%n", &empty, &num, &consumed);
-    if (consumed > 0) {
-        *ndx = num;
-        str += consumed;
-    } else if (empty > 0) {
-        *ndx = -1;
-    } else {
-        *ndx = 0;
-    }
+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];
+};
 
-    return str;
+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;
 }
 
-/*
- * 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(darray_char *line, struct mapping *mapping)
+static darray_matched_sval
+split_comma_separated_mlvo(const char *s)
 {
-    char *tok;
-    char *str = &darray_item(*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;
+    darray_matched_sval arr = darray_new();
 
     /*
-     * Remember the last sequential mapping id (incremented if the match
-     * is successful).
+     * Make sure the array returned by this function always includes at
+     * least one value, e.g. "" -> { "" } and "," -> { "", "" }.
      */
-    tmp = mapping->number;
-    memset(mapping, 0, sizeof(*mapping));
-    mapping->number = tmp;
 
-    while ((tok = strtok_r(str, " ", &strtok_buf)) != NULL) {
-        found = false;
-        str = NULL;
-
-        if (strcmp(tok, "=") == 0)
-            continue;
-
-        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) {
-                        WARN("Illegal %s index: %d\n", cname[i], ndx);
-                        WARN("Can only index layout and variant\n");
-                        break;
-                    }
-
-                    if (ndx < 1 || ndx > XkbNumKbdGroups) {
-                        WARN("Illegal %s index: %d\n", cname[i], ndx);
-                        WARN("Index must be in range 1..%d\n", XkbNumKbdGroups);
-                        break;
-                    }
-                } else {
-                    ndx = 0;
-                }
-
-                found = true;
+    if (!s) {
+        struct matched_sval val = { .sval = { NULL, 0 } };
+        darray_append(arr, val);
+        return arr;
+    }
 
-                if (present & (1 << i)) {
-                    if ((i == LAYOUT && layout_ndx_present & (1 << ndx)) ||
-                        (i == VARIANT && variant_ndx_present & (1 << ndx))) {
-                        WARN("Component \"%s\" listed twice\n", tok);
-                        ACTION("Second definition ignored\n");
-                        break;
-                    }
-                }
+    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++;
+    }
 
-                present |= (1 << i);
-                if (i == LAYOUT)
-                    layout_ndx_present |= 1 << ndx;
-                if (i == VARIANT)
-                    variant_ndx_present |= 1 << ndx;
+    return arr;
+}
 
-                mapping->map[mapping->num_maps].word = i;
-                mapping->map[mapping->num_maps].index = ndx;
-                mapping->num_maps++;
-                break;
-            }
-        }
+static struct matcher *
+matcher_new(struct xkb_context *ctx,
+            const struct xkb_rule_names *rmlvo)
+{
+    struct matcher *m = calloc(1, sizeof(*m));
+    if (!m)
+        return NULL;
 
-        if (!found) {
-            WARN("Unknown component \"%s\"\n", tok);
-            ACTION("ignored\n");
-        }
-    }
+    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);
 
-    if ((present & PART_MASK) == 0) {
-        WARN("Mapping needs at least one MLVO part\n");
-        ACTION("Illegal mapping ignored\n");
-        mapping->num_maps = 0;
-        return;
-    }
+    return m;
+}
 
-    if ((present & COMPONENT_MASK) == 0) {
-        WARN("Mapping needs at least one component\n");
-        ACTION("Illegal mapping ignored\n");
-        mapping->num_maps = 0;
+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);
+}
 
-    if (((present & COMPONENT_MASK) & (1 << KEYMAP)) &&
-        ((present & COMPONENT_MASK) != (1 << KEYMAP))) {
-        WARN("Keymap cannot appear with other components\n");
-        ACTION("Illegal mapping ignored\n");
-        mapping->num_maps = 0;
-        return;
-    }
+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);
+}
 
-    mapping->number++;
+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);
 }
 
-/*
- * Match a line such as:
- * ! $pcmodels = pc101 pc102 pc104 pc105
- */
 static bool
-match_group_line(darray_char *line, struct group *group)
-{
-    int i;
-    char *name = strchr(&darray_item(*line, 0), '$');
-    char *words = strchr(name, ' ');
+read_rules_file(struct xkb_context *ctx,
+                struct matcher *matcher,
+                unsigned include_depth,
+                FILE *file,
+                const char *path);
 
-    if (!words)
-        return false;
+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;
 
-    *words++ = '\0';
+    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;
 
-    for (; *words; words++) {
-        if (*words != '=' && *words != ' ')
-            break;
+    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 (*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++;
+    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));
         }
     }
-    group->number = i;
-
-    return true;
+    if (!scanner_buf_append(&s, '\0')) {
+        scanner_err(&s, "include path is too long");
+        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);
+    }
 }
 
-/* Match lines following a mapping (see match_mapping_line comment). */
-static bool
-match_rule_line(darray_char *line, struct mapping *mapping,
-                struct rule *rule)
+static void
+matcher_mapping_start_new(struct matcher *m)
 {
-    char *str, *tok;
-    int nread, i;
-    char *strtok_buf;
-    bool append = false;
-    const char *names[MAX_WORDS] = { NULL };
-
-    if (mapping->num_maps == 0) {
-        WARN("Must have a mapping before first line of data\n");
-        ACTION("Illegal line of data ignored\n");
-        return false;
-    }
+    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;
+}
 
-    str = &darray_item(*line, 0);
+static int
+extract_layout_index(const char *s, size_t max_len, xkb_layout_index_t *out)
+{
+    /* 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;
+}
 
-    for (nread = 0; (tok = strtok_r(str, " ", &strtok_buf)) != NULL; nread++) {
-        str = NULL;
+static void
+matcher_mapping_set_mlvo(struct matcher *m, struct scanner *s,
+                         struct sval ident)
+{
+    enum rules_mlvo mlvo;
+    struct sval mlvo_sval;
 
-        if (strcmp(tok, "=") == 0) {
-            nread--;
-            continue;
-        }
+    for (mlvo = 0; mlvo < _MLVO_NUM_ENTRIES; mlvo++) {
+        mlvo_sval = rules_mlvo_svals[mlvo];
 
-        if (nread > mapping->num_maps) {
-            WARN("Too many words on a line\n");
-            ACTION("Extra word \"%s\" ignored\n", tok);
-            continue;
-        }
+        if (svaleq_prefix(mlvo_sval, ident))
+            break;
+    }
 
-        names[mapping->map[nread].word] = tok;
-        if (*tok == '+' || *tok == '|')
-            append = true;
+    /* 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;
     }
 
-    if (nread < mapping->num_maps) {
-        WARN("Too few words on a line: %s\n", &darray_item(*line, 0));
-        ACTION("line 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;
     }
 
-    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->keymap = uDupString(names[KEYMAP]);
-
-    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;
+    /* 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 (mlvo == MLVO_LAYOUT) {
+            m->mapping.layout_idx = idx;
+        }
+        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(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(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(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;
-
-    apply(rule->keycodes, &kccgst->keycodes);
-    apply(rule->symbols, &kccgst->symbols);
-    apply(rule->types, &kccgst->types);
-    apply(rule->compat, &kccgst->compat);
-    apply(rule->keymap, &kccgst->keymap);
+    struct sval dummy = { NULL, 0 };
+    matcher_rule_set_mlvo_common(m, s, dummy, MLVO_MATCH_WILDCARD);
 }
 
-/*
- * 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)
+static void
+matcher_rule_set_mlvo_group(struct matcher *m, struct scanner *s,
+                            struct sval ident)
 {
-   int i;
-   const char *word;
-   struct group *iter, *group = NULL;
-
-   darray_foreach(iter, rules->groups) {
-       if (strcmp(iter->name, group_name) == 0) {
-           group = iter;
-           break;
-       }
-   }
-
-   if (!group)
-       return false;
-
-   word = group->words;
-   for (i = 0; i < group->number; i++, word += strlen(word) + 1)
-       if (strcmp(word, name) == 0)
-           return true;
-
-   return false;
+    matcher_rule_set_mlvo_common(m, s, ident, MLVO_MATCH_GROUP);
 }
 
-/* Match @needle out of @sep-seperated @haystack. */
-static bool
-match_one_of(const char *haystack, const char *needle, char sep)
+static void
+matcher_rule_set_mlvo(struct matcher *m, struct scanner *s,
+                      struct sval ident)
 {
-    const char *s = strstr(haystack, needle);
-
-    if (s == NULL)
-        return false;
-
-    if (s != haystack && *s != sep)
-        return false;
-
-    s += strlen(needle);
-    if (*s != '\0' && *s != sep)
-        return false;
-
-    return true;
+    matcher_rule_set_mlvo_common(m, s, ident, MLVO_MATCH_NORMAL);
 }
 
-static int
-apply_rule_if_matches(struct rules *rules, struct rule *rule,
-                      struct multi_defs *mdefs,
-                      struct xkb_component_names *kccgst)
+static void
+matcher_rule_set_kccgst(struct matcher *m, struct scanner *s,
+                        struct sval ident)
 {
-    bool pending = false;
-
-    if (rule->model) {
-        if (mdefs->model == NULL)
-            return 0;
-
-        if (strcmp(rule->model, "*") == 0) {
-            pending = true;
-        }
-        else if (rule->model[0] == '$') {
-            if (!match_group_member(rules, rule->model, mdefs->model))
-                return 0;
-        }
-        else if (strcmp(rule->model, mdefs->model) != 0) {
-            return 0;
-        }
-    }
-
-    if (rule->option) {
-        if (mdefs->options == NULL)
-            return 0;
-
-        if (!match_one_of(mdefs->options, rule->option, ','))
-            return 0;
+    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++;
+}
 
-    if (rule->layout) {
-        if (mdefs->layout[rule->layout_num] == NULL)
-            return 0;
+static bool
+match_group(struct matcher *m, struct sval group_name, struct sval to)
+{
+    struct group *group;
+    struct sval *element;
+    bool found = false;
 
-        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;
-        }
-        else if (strcmp(rule->layout,
-                        mdefs->layout[rule->layout_num]) != 0) {
-            return 0;
+    darray_foreach(group, m->groups) {
+        if (svaleq(group->name, group_name)) {
+            found = true;
+            break;
         }
     }
 
-    if (rule->variant) {
-        if (mdefs->variant[rule->variant_num] == NULL)
-            return 0;
-
-        if (strcmp(rule->variant, "*") == 0) {
-            pending = true;
-        } else if (rule->variant[0] == '$') {
-            if (!match_group_member(rules, rule->variant,
-                                    mdefs->variant[rule->variant_num]))
-                return 0;
-        }
-        else if (strcmp(rule->variant,
-                        mdefs->variant[rule->variant_num]) != 0) {
-            return 0;
-        }
+    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;
     }
 
-    if (pending) {
-        rule->flags |= RULE_FLAG_PENDING_MATCH;
-    } else {
-        /* Exact match, apply it now. */
-        apply_rule(rule, kccgst);
-    }
+    darray_foreach(element, group->elements)
+        if (svaleq(to, *element))
+            return true;
 
-    return rule->number;
+    return false;
 }
 
-static void
-clear_partial_matches(struct rules *rules)
+static bool
+match_value(struct matcher *m, struct sval val, struct sval to,
+            enum mlvo_match_type match_type)
 {
-    struct rule *rule;
-
-    darray_foreach(rule, rules->rules)
-        rule->flags &= ~RULE_FLAG_PENDING_MATCH;
+    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 void
-apply_partial_matches(struct rules *rules, 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)
 {
-    struct rule *rule;
-
-    darray_foreach(rule, rules->rules)
-        if (rule->flags & RULE_FLAG_PENDING_MATCH)
-            apply_rule(rule, kccgst);
+    bool matched = match_value(m, val, to->sval, match_type);
+    if (matched)
+        to->matched = true;
+    return matched;
 }
 
-static void
-apply_matching_rules(struct rules *rules, struct multi_defs *mdefs,
-                     struct xkb_component_names *kccgst, unsigned int flags)
+/*
+ * 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)
 {
-    int skip = -1;
-    struct rule *rule;
-
-    darray_foreach(rule, rules->rules) {
-        if ((rule->flags & flags) != flags)
-            continue;
+    const char *str = value.start;
+    darray_char expanded = darray_new();
+    char ch;
+    bool expanded_plus, to_plus;
 
-        if ((flags & RULE_FLAG_OPTION) == 0 && rule->number == skip)
+    /*
+     * 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;
+        }
+        if (++i >= value.len) goto error;
 
-        skip = apply_rule_if_matches(rules, rule, mdefs, kccgst);
-    }
-}
+        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;
+        }
 
-static char *
-substitute_vars(char *name, struct multi_defs *mdefs)
-{
-    char *str, *outstr, *var;
-    char *orig = name;
-    size_t len, extra_len;
-    char pfx, sfx;
-    int ndx;
+        /* 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 (!name)
-        return NULL;
+        /* Check for index. */
+        idx = XKB_LAYOUT_INVALID;
+        if (i < value.len && str[i] == '[') {
+            int consumed;
 
-    str = strchr(name, '%');
-    if (str == NULL)
-        return name;
+            if (mlv != MLVO_LAYOUT && mlv != MLVO_VARIANT) {
+                scanner_err(s, "invalid index in %%-expansion; may only index layout or variant");
+                goto error;
+            }
 
-    len = strlen(name);
+            consumed = extract_layout_index(str + i, value.len - i, &idx);
+            if (consumed == -1) goto error;
+            i += consumed;
+        }
 
-    while (str != NULL) {
-        pfx = str[1];
-        extra_len = 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 (pfx == '+' || pfx == '|' || pfx == '_' || pfx == '-') {
-            extra_len = 1;
-            str++;
+        /* 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 (pfx == '(') {
-            extra_len = 2;
-            str++;
+        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);
         }
-
-        var = str + 1;
-        str = get_index(var + 1, &ndx);
-        if (ndx == -1) {
-            str = strchr(str, '%');
-            continue;
+        else if (mlv == MLVO_MODEL) {
+            expanded_value = &m->rmlvo.model;
         }
 
-        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++;
+        /* If we didn't get one, skip silently. */
+        if (!expanded_value || expanded_value->sval.len == 0)
+            continue;
 
-        str = strchr(&str[0], '%');
+        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;
     }
 
-    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;
+    /*
+     * 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
+     */
 
-                strcpy(outstr, mdefs->layout[ndx]);
-                outstr += strlen(mdefs->layout[ndx]);
+    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 == '|');
 
-                if (sfx)
-                    *outstr++ = sfx;
-            }
-            else if (*var == 'm' && mdefs->model) {
-                if (pfx)
-                    *outstr++ = pfx;
+    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);
 
-                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;
+    darray_free(expanded);
+    return true;
 
-                strcpy(outstr, mdefs->variant[ndx]);
-                outstr += strlen(mdefs->variant[ndx]);
+error:
+    darray_free(expanded);
+    scanner_err(s, "invalid %%-expansion in value; not used");
+    return false;
+}
 
-                if (sfx)
-                    *outstr++ = sfx;
-            }
+static void
+matcher_rule_verify(struct matcher *m, struct scanner *s)
+{
+    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;
+    }
+}
 
-            if (pfx == '(' && *str == ')')
-                str++;
+static void
+matcher_rule_apply_if_matches(struct matcher *m, struct scanner *s)
+{
+    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 {
-            *outstr++= *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);
+        }
+        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);
+        }
+        else if (mlvo == MLVO_OPTION) {
+            darray_foreach(to, m->rmlvo.options) {
+                matched = match_value_and_mark(m, value, to, match_type);
+                if (matched)
+                    break;
+            }
         }
-    }
 
-    *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);
-    kccgst->keymap = substitute_vars(kccgst->keymap, &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) || kccgst->keymap;
-}
+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(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(file, &line)) {
-        if (match_line(&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);
-        free(rule->keymap);
+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, XkmRulesFile, &path);
-    if (!file) {
-        ERROR("could not find \"%s\" rules in XKB path\n", rmlvo->rules);
-        ERROR("%d include paths searched:\n",
-              xkb_context_num_include_paths(ctx));
-        for (i = 0; i < xkb_context_num_include_paths(ctx); i++)
-            ERROR("\t%s\n", xkb_context_include_path_get(ctx, i));
-        return NULL;
-    }
-
-    rules = load_rules(file);
-    if (!rules) {
-        ERROR("failed to load XKB rules \"%s\"\n", path);
-        goto err;
-    }
-
-    kccgst = calloc(1, sizeof(*kccgst));
-    if (!kccgst) {
-        ERROR("failed to allocate XKB components\n");
-        goto err;
-    }
-
-    if (!get_components(rules, rmlvo, kccgst)) {
-        free(kccgst->keymap);
-        free(kccgst->keycodes);
-        free(kccgst->types);
-        free(kccgst->compat);
-        free(kccgst->symbols);
-        free(kccgst);
-        kccgst = NULL;
-        ERROR("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;
 }